5f2203b75ac19413a3ad729d837f3674bf070b52
[MacVim.git] / src / MacVim / MMAppController.m
blob5f2203b75ac19413a3ad729d837f3674bf070b52
1 /* vi:set ts=8 sts=4 sw=4 ft=objc:
2  *
3  * VIM - Vi IMproved            by Bram Moolenaar
4  *                              MacVim GUI port by Bjorn Winckler
5  *
6  * Do ":help uganda"  in Vim to read copying and usage conditions.
7  * Do ":help credits" in Vim to see a list of people who contributed.
8  * See README.txt for an overview of the Vim source code.
9  */
11  * MMAppController
12  *
13  * MMAppController is the delegate of NSApp and as such handles file open
14  * requests, application termination, etc.  It sets up a named NSConnection on
15  * which it listens to incoming connections from Vim processes.  It also
16  * coordinates all MMVimControllers and takes care of the main menu.
17  *
18  * A new Vim process is started by calling launchVimProcessWithArguments:.
19  * When the Vim process is initialized it notifies the app controller by
20  * sending a connectBackend:pid: message.  At this point a new MMVimController
21  * is allocated.  Afterwards, the Vim process communicates directly with its
22  * MMVimController.
23  *
24  * A Vim process started from the command line connects directly by sending the
25  * connectBackend:pid: message (launchVimProcessWithArguments: is never called
26  * in this case).
27  *
28  * The main menu is handled as follows.  Each Vim controller keeps its own main
29  * menu.  All menus except the "MacVim" menu are controlled by the Vim process.
30  * The app controller also keeps a reference to the "default main menu" which
31  * is set up in MainMenu.nib.  When no editor window is open the default main
32  * menu is used.  When a new editor window becomes main its main menu becomes
33  * the new main menu, this is done in -[MMAppController setMainMenu:].
34  *   NOTE: Certain heuristics are used to find the "MacVim", "Windows", "File",
35  * and "Services" menu.  If MainMenu.nib changes these heuristics may have to
36  * change as well.  For specifics see the find... methods defined in the NSMenu
37  * category "MMExtras".
38  */
40 #import "MMAppController.h"
41 #import "MMPreferenceController.h"
42 #import "MMVimController.h"
43 #import "MMWindowController.h"
44 #import "Miscellaneous.h"
46 #ifdef MM_ENABLE_PLUGINS
47 #import "MMPlugInManager.h"
48 #endif
50 #import <unistd.h>
51 #import <CoreServices/CoreServices.h>
54 #define MM_HANDLE_XCODE_MOD_EVENT 0
58 // Default timeout intervals on all connections.
59 static NSTimeInterval MMRequestTimeout = 5;
60 static NSTimeInterval MMReplyTimeout = 5;
62 static NSString *MMWebsiteString = @"http://code.google.com/p/macvim/";
64 #if (MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5)
65 // Latency (in s) between FS event occuring and being reported to MacVim.
66 // Should be small so that MacVim is notified of changes to the ~/.vim
67 // directory more or less immediately.
68 static CFTimeInterval MMEventStreamLatency = 0.1;
69 #endif
71 static float MMCascadeHorizontalOffset = 21;
72 static float MMCascadeVerticalOffset = 23;
75 #pragma pack(push,1)
76 // The alignment and sizes of these fields are based on trial-and-error.  It
77 // may be necessary to adjust them to fit if Xcode ever changes this struct.
78 typedef struct
80     int16_t unused1;      // 0 (not used)
81     int16_t lineNum;      // line to select (< 0 to specify range)
82     int32_t startRange;   // start of selection range (if line < 0)
83     int32_t endRange;     // end of selection range (if line < 0)
84     int32_t unused2;      // 0 (not used)
85     int32_t theDate;      // modification date/time
86 } MMXcodeSelectionRange;
87 #pragma pack(pop)
90 // This is a private AppKit API gleaned from class-dump.
91 @interface NSKeyBindingManager : NSObject
92 + (id)sharedKeyBindingManager;
93 - (id)dictionary;
94 - (void)setDictionary:(id)arg1;
95 @end
98 @interface MMAppController (MMServices)
99 - (void)openSelection:(NSPasteboard *)pboard userData:(NSString *)userData
100                 error:(NSString **)error;
101 - (void)openFile:(NSPasteboard *)pboard userData:(NSString *)userData
102            error:(NSString **)error;
103 - (void)newFileHere:(NSPasteboard *)pboard userData:(NSString *)userData
104               error:(NSString **)error;
105 @end
108 @interface MMAppController (Private)
109 - (MMVimController *)topmostVimController;
110 - (int)launchVimProcessWithArguments:(NSArray *)args
111                     workingDirectory:(NSString *)cwd;
112 - (NSArray *)filterFilesAndNotify:(NSArray *)files;
113 - (NSArray *)filterOpenFiles:(NSArray *)filenames
114                openFilesDict:(NSDictionary **)openFiles;
115 #if MM_HANDLE_XCODE_MOD_EVENT
116 - (void)handleXcodeModEvent:(NSAppleEventDescriptor *)event
117                  replyEvent:(NSAppleEventDescriptor *)reply;
118 #endif
119 - (void)handleGetURLEvent:(NSAppleEventDescriptor *)event
120                replyEvent:(NSAppleEventDescriptor *)reply;
121 - (NSMutableDictionary *)extractArgumentsFromOdocEvent:
122     (NSAppleEventDescriptor *)desc;
123 - (void)scheduleVimControllerPreloadAfterDelay:(NSTimeInterval)delay;
124 - (void)cancelVimControllerPreloadRequests;
125 - (void)preloadVimController:(id)sender;
126 - (int)maxPreloadCacheSize;
127 - (MMVimController *)takeVimControllerFromCache;
128 - (void)clearPreloadCacheWithCount:(int)count;
129 - (void)rebuildPreloadCache;
130 - (NSDate *)rcFilesModificationDate;
131 - (BOOL)openVimControllerWithArguments:(NSDictionary *)arguments;
132 - (void)activateWhenNextWindowOpens;
133 - (void)startWatchingVimDir;
134 - (void)stopWatchingVimDir;
135 - (void)handleFSEvent;
136 - (void)loadDefaultFont;
137 - (int)executeInLoginShell:(NSString *)path arguments:(NSArray *)args;
138 - (void)reapChildProcesses:(id)sender;
139 - (void)processInputQueues:(id)sender;
140 - (void)addVimController:(MMVimController *)vc;
141 - (NSDictionary *)convertVimControllerArguments:(NSDictionary *)args
142                                   toCommandLine:(NSArray **)cmdline;
143 - (NSString *)workingDirectoryForArguments:(NSDictionary *)args;
144 - (NSScreen *)screenContainingPoint:(NSPoint)pt;
146 #ifdef MM_ENABLE_PLUGINS
147 - (void)removePlugInMenu;
148 - (void)addPlugInMenuToMenu:(NSMenu *)mainMenu;
149 #endif
150 @end
154 #if (MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5)
155     static void
156 fsEventCallback(ConstFSEventStreamRef streamRef,
157                 void *clientCallBackInfo,
158                 size_t numEvents,
159                 void *eventPaths,
160                 const FSEventStreamEventFlags eventFlags[],
161                 const FSEventStreamEventId eventIds[])
163     [[MMAppController sharedInstance] handleFSEvent];
165 #endif
167 @implementation MMAppController
169 + (void)initialize
171     static BOOL initDone = NO;
172     if (initDone) return;
173     initDone = YES;
175     ASLInit();
177     // HACK! The following user default must be reset, else Ctrl-q (or
178     // whichever key is specified by the default) will be blocked by the input
179     // manager (interpretKeyEvents: swallows that key).  (We can't use
180     // NSUserDefaults since it only allows us to write to the registration
181     // domain and this preference has "higher precedence" than that so such a
182     // change would have no effect.)
183     CFPreferencesSetAppValue(CFSTR("NSQuotedKeystrokeBinding"),
184                              CFSTR(""),
185                              kCFPreferencesCurrentApplication);
187     // Also disable NSRepeatCountBinding -- it is not enabled by default, but
188     // it does not make much sense to support it since Vim has its own way of
189     // dealing with repeat counts.
190     CFPreferencesSetAppValue(CFSTR("NSRepeatCountBinding"),
191                              CFSTR(""),
192                              kCFPreferencesCurrentApplication);
193     
194     NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:
195         [NSNumber numberWithBool:NO],   MMNoWindowKey,
196         [NSNumber numberWithInt:64],    MMTabMinWidthKey,
197         [NSNumber numberWithInt:6*64],  MMTabMaxWidthKey,
198         [NSNumber numberWithInt:132],   MMTabOptimumWidthKey,
199         [NSNumber numberWithBool:YES],  MMShowAddTabButtonKey,
200         [NSNumber numberWithInt:2],     MMTextInsetLeftKey,
201         [NSNumber numberWithInt:1],     MMTextInsetRightKey,
202         [NSNumber numberWithInt:1],     MMTextInsetTopKey,
203         [NSNumber numberWithInt:1],     MMTextInsetBottomKey,
204         @"MMTypesetter",                MMTypesetterKey,
205         [NSNumber numberWithFloat:1],   MMCellWidthMultiplierKey,
206         [NSNumber numberWithFloat:-1],  MMBaselineOffsetKey,
207         [NSNumber numberWithBool:YES],  MMTranslateCtrlClickKey,
208         [NSNumber numberWithInt:0],     MMOpenInCurrentWindowKey,
209         [NSNumber numberWithBool:NO],   MMNoFontSubstitutionKey,
210         [NSNumber numberWithBool:YES],  MMLoginShellKey,
211         [NSNumber numberWithInt:0],     MMRendererKey,
212         [NSNumber numberWithInt:MMUntitledWindowAlways],
213                                         MMUntitledWindowKey,
214         [NSNumber numberWithBool:NO],   MMTexturedWindowKey,
215         [NSNumber numberWithBool:NO],   MMZoomBothKey,
216         @"",                            MMLoginShellCommandKey,
217         @"",                            MMLoginShellArgumentKey,
218         [NSNumber numberWithBool:YES],  MMDialogsTrackPwdKey,
219 #ifdef MM_ENABLE_PLUGINS
220         [NSNumber numberWithBool:YES],  MMShowLeftPlugInContainerKey,
221 #endif
222         [NSNumber numberWithInt:3],     MMOpenLayoutKey,
223         [NSNumber numberWithBool:NO],   MMVerticalSplitKey,
224         [NSNumber numberWithInt:0],     MMPreloadCacheSizeKey,
225         [NSNumber numberWithInt:0],     MMLastWindowClosedBehaviorKey,
226         [NSNumber numberWithBool:YES],  MMLoadDefaultFontKey,
227 #ifdef INCLUDE_OLD_IM_CODE
228         [NSNumber numberWithBool:YES],  MMUseInlineImKey,
229 #endif // INCLUDE_OLD_IM_CODE
230         nil];
232     [[NSUserDefaults standardUserDefaults] registerDefaults:dict];
234     NSArray *types = [NSArray arrayWithObject:NSStringPboardType];
235     [NSApp registerServicesMenuSendTypes:types returnTypes:types];
237     // NOTE: Set the current directory to user's home directory, otherwise it
238     // will default to the root directory.  (This matters since new Vim
239     // processes inherit MacVim's environment variables.)
240     [[NSFileManager defaultManager] changeCurrentDirectoryPath:
241             NSHomeDirectory()];
244 - (id)init
246     if (!(self = [super init])) return nil;
248     [self loadDefaultFont];
250     vimControllers = [NSMutableArray new];
251     cachedVimControllers = [NSMutableArray new];
252     preloadPid = -1;
253     pidArguments = [NSMutableDictionary new];
254     inputQueues = [NSMutableDictionary new];
256 #ifdef MM_ENABLE_PLUGINS
257     NSString *plugInTitle = NSLocalizedString(@"Plug-In",
258                                               @"Plug-In menu title");
259     plugInMenuItem = [[NSMenuItem alloc] initWithTitle:plugInTitle
260                                                 action:NULL
261                                          keyEquivalent:@""];
262     NSMenu *submenu = [[NSMenu alloc] initWithTitle:plugInTitle];
263     [plugInMenuItem setSubmenu:submenu];
264     [submenu release];
265 #endif
267     // NOTE: Do not use the default connection since the Logitech Control
268     // Center (LCC) input manager steals and this would cause MacVim to
269     // never open any windows.  (This is a bug in LCC but since they are
270     // unlikely to fix it, we graciously give them the default connection.)
271     connection = [[NSConnection alloc] initWithReceivePort:[NSPort port]
272                                                   sendPort:nil];
273     [connection setRootObject:self];
274     [connection setRequestTimeout:MMRequestTimeout];
275     [connection setReplyTimeout:MMReplyTimeout];
277     // NOTE!  If the name of the connection changes here it must also be
278     // updated in MMBackend.m.
279     NSString *name = [NSString stringWithFormat:@"%@-connection",
280              [[NSBundle mainBundle] bundlePath]];
281     if (![connection registerName:name]) {
282         ASLogCrit(@"Failed to register connection with name '%@'", name);
283         [connection release];  connection = nil;
284     }
286     return self;
289 - (void)dealloc
291     ASLogDebug(@"");
293     [connection release];  connection = nil;
294     [inputQueues release];  inputQueues = nil;
295     [pidArguments release];  pidArguments = nil;
296     [vimControllers release];  vimControllers = nil;
297     [cachedVimControllers release];  cachedVimControllers = nil;
298     [openSelectionString release];  openSelectionString = nil;
299     [recentFilesMenuItem release];  recentFilesMenuItem = nil;
300     [defaultMainMenu release];  defaultMainMenu = nil;
301 #ifdef MM_ENABLE_PLUGINS
302     [plugInMenuItem release];  plugInMenuItem = nil;
303 #endif
304     [appMenuItemTemplate release];  appMenuItemTemplate = nil;
306     [super dealloc];
309 - (void)applicationWillFinishLaunching:(NSNotification *)notification
311     // Remember the default menu so that it can be restored if the user closes
312     // all editor windows.
313     defaultMainMenu = [[NSApp mainMenu] retain];
315     // Store a copy of the default app menu so we can use this as a template
316     // for all other menus.  We make a copy here because the "Services" menu
317     // will not yet have been populated at this time.  If we don't we get
318     // problems trying to set key equivalents later on because they might clash
319     // with items on the "Services" menu.
320     appMenuItemTemplate = [defaultMainMenu itemAtIndex:0];
321     appMenuItemTemplate = [appMenuItemTemplate copy];
323     // Set up the "Open Recent" menu. See
324     //   http://lapcatsoftware.com/blog/2007/07/10/
325     //     working-without-a-nib-part-5-open-recent-menu/
326     // and
327     //   http://www.cocoabuilder.com/archive/message/cocoa/2007/8/15/187793
328     // for more information.
329     //
330     // The menu itself is created in MainMenu.nib but we still seem to have to
331     // hack around a bit to get it to work.  (This has to be done in
332     // applicationWillFinishLaunching at the latest, otherwise it doesn't
333     // work.)
334     NSMenu *fileMenu = [defaultMainMenu findFileMenu];
335     if (fileMenu) {
336         int idx = [fileMenu indexOfItemWithAction:@selector(fileOpen:)];
337         if (idx >= 0 && idx+1 < [fileMenu numberOfItems])
339         recentFilesMenuItem = [fileMenu itemWithTitle:@"Open Recent"];
340         [[recentFilesMenuItem submenu] performSelector:@selector(_setMenuName:)
341                                         withObject:@"NSRecentDocumentsMenu"];
343         // Note: The "Recent Files" menu must be moved around since there is no
344         // -[NSApp setRecentFilesMenu:] method.  We keep a reference to it to
345         // facilitate this move (see setMainMenu: below).
346         [recentFilesMenuItem retain];
347     }
349 #if MM_HANDLE_XCODE_MOD_EVENT
350     [[NSAppleEventManager sharedAppleEventManager]
351             setEventHandler:self
352                 andSelector:@selector(handleXcodeModEvent:replyEvent:)
353               forEventClass:'KAHL'
354                  andEventID:'MOD '];
355 #endif
357     // Register 'mvim://' URL handler
358     [[NSAppleEventManager sharedAppleEventManager]
359             setEventHandler:self
360                 andSelector:@selector(handleGetURLEvent:replyEvent:)
361               forEventClass:kInternetEventClass
362                  andEventID:kAEGetURL];
364     // Disable the default Cocoa "Key Bindings" since they interfere with the
365     // way Vim handles keyboard input.  Cocoa reads bindings from
366     //     /System/Library/Frameworks/AppKit.framework/Resources/
367     //                                                  StandardKeyBinding.dict
368     // and
369     //     ~/Library/KeyBindings/DefaultKeyBinding.dict
370     // To avoid having the user accidentally break keyboard handling (by
371     // modifying the latter in some unexpected way) in MacVim we load our own
372     // key binding dictionary from Resource/KeyBinding.plist.  We can't disable
373     // the bindings completely since it would break keyboard handling in
374     // dialogs so the our custom dictionary contains all the entries from the
375     // former location.
376     //
377     // It is possible to disable key bindings completely by not calling
378     // interpretKeyEvents: in keyDown: but this also disables key bindings used
379     // by certain input methods.  E.g.  Ctrl-Shift-; would no longer work in
380     // the Kotoeri input manager.
381     //
382     // To solve this problem we access a private API and set the key binding
383     // dictionary to our own custom dictionary here.  At this time Cocoa will
384     // have already read the above mentioned dictionaries so it (hopefully)
385     // won't try to change the key binding dictionary again after this point.
386     NSKeyBindingManager *mgr = [NSKeyBindingManager sharedKeyBindingManager];
387     NSBundle *mainBundle = [NSBundle mainBundle];
388     NSString *path = [mainBundle pathForResource:@"KeyBinding"
389                                           ofType:@"plist"];
390     NSDictionary *dict = [NSDictionary dictionaryWithContentsOfFile:path];
391     if (mgr && dict) {
392         [mgr setDictionary:dict];
393     } else {
394         ASLogNotice(@"Failed to override the Cocoa key bindings.  Keyboard "
395                 "input may behave strangely as a result (path=%@).", path);
396     }
399 - (void)applicationDidFinishLaunching:(NSNotification *)notification
401     [NSApp setServicesProvider:self];
402 #ifdef MM_ENABLE_PLUGINS
403     [[MMPlugInManager sharedManager] loadAllPlugIns];
404 #endif
406     if ([self maxPreloadCacheSize] > 0) {
407         [self scheduleVimControllerPreloadAfterDelay:2];
408         [self startWatchingVimDir];
409     }
411     ASLogInfo(@"MacVim finished launching");
414 - (BOOL)applicationShouldOpenUntitledFile:(NSApplication *)sender
416     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
417     NSAppleEventManager *aem = [NSAppleEventManager sharedAppleEventManager];
418     NSAppleEventDescriptor *desc = [aem currentAppleEvent];
420     // The user default MMUntitledWindow can be set to control whether an
421     // untitled window should open on 'Open' and 'Reopen' events.
422     int untitledWindowFlag = [ud integerForKey:MMUntitledWindowKey];
424     BOOL isAppOpenEvent = [desc eventID] == kAEOpenApplication;
425     if (isAppOpenEvent && (untitledWindowFlag & MMUntitledWindowOnOpen) == 0)
426         return NO;
428     BOOL isAppReopenEvent = [desc eventID] == kAEReopenApplication;
429     if (isAppReopenEvent
430             && (untitledWindowFlag & MMUntitledWindowOnReopen) == 0)
431         return NO;
433     // When a process is started from the command line, the 'Open' event may
434     // contain a parameter to surpress the opening of an untitled window.
435     desc = [desc paramDescriptorForKeyword:keyAEPropData];
436     desc = [desc paramDescriptorForKeyword:keyMMUntitledWindow];
437     if (desc && ![desc booleanValue])
438         return NO;
440     // Never open an untitled window if there is at least one open window or if
441     // there are processes that are currently launching.
442     if ([vimControllers count] > 0 || [pidArguments count] > 0)
443         return NO;
445     // NOTE!  This way it possible to start the app with the command-line
446     // argument '-nowindow yes' and no window will be opened by default but
447     // this argument will only be heeded when the application is opening.
448     if (isAppOpenEvent && [ud boolForKey:MMNoWindowKey] == YES)
449         return NO;
451     return YES;
454 - (BOOL)applicationOpenUntitledFile:(NSApplication *)sender
456     ASLogDebug(@"Opening untitled window...");
457     [self newWindow:self];
458     return YES;
461 - (void)application:(NSApplication *)sender openFiles:(NSArray *)filenames
463     ASLogInfo(@"Opening files %@", filenames);
465     // Extract ODB/Xcode/Spotlight parameters from the current Apple event,
466     // sort the filenames, and then let openFiles:withArguments: do the heavy
467     // lifting.
469     if (!(filenames && [filenames count] > 0))
470         return;
472     // Sort filenames since the Finder doesn't take care in preserving the
473     // order in which files are selected anyway (and "sorted" is more
474     // predictable than "random").
475     if ([filenames count] > 1)
476         filenames = [filenames sortedArrayUsingSelector:
477                 @selector(localizedCompare:)];
479     // Extract ODB/Xcode/Spotlight parameters from the current Apple event
480     NSMutableDictionary *arguments = [self extractArgumentsFromOdocEvent:
481             [[NSAppleEventManager sharedAppleEventManager] currentAppleEvent]];
483     if ([self openFiles:filenames withArguments:arguments]) {
484         [NSApp replyToOpenOrPrint:NSApplicationDelegateReplySuccess];
485     } else {
486         // TODO: Notify user of failure?
487         [NSApp replyToOpenOrPrint:NSApplicationDelegateReplyFailure];
488     }
491 - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender
493     return (MMTerminateWhenLastWindowClosed ==
494             [[NSUserDefaults standardUserDefaults]
495                 integerForKey:MMLastWindowClosedBehaviorKey]);
498 - (NSApplicationTerminateReply)applicationShouldTerminate:
499     (NSApplication *)sender
501     // TODO: Follow Apple's guidelines for 'Graceful Application Termination'
502     // (in particular, allow user to review changes and save).
503     int reply = NSTerminateNow;
504     BOOL modifiedBuffers = NO;
506     // Go through windows, checking for modified buffers.  (Each Vim process
507     // tells MacVim when any buffer has been modified and MacVim sets the
508     // 'documentEdited' flag of the window correspondingly.)
509     NSEnumerator *e = [[NSApp windows] objectEnumerator];
510     id window;
511     while ((window = [e nextObject])) {
512         if ([window isDocumentEdited]) {
513             modifiedBuffers = YES;
514             break;
515         }
516     }
518     if (modifiedBuffers) {
519         NSAlert *alert = [[NSAlert alloc] init];
520         [alert setAlertStyle:NSWarningAlertStyle];
521         [alert addButtonWithTitle:NSLocalizedString(@"Quit",
522                 @"Dialog button")];
523         [alert addButtonWithTitle:NSLocalizedString(@"Cancel",
524                 @"Dialog button")];
525         [alert setMessageText:NSLocalizedString(@"Quit without saving?",
526                 @"Quit dialog with changed buffers, title")];
527         [alert setInformativeText:NSLocalizedString(
528                 @"There are modified buffers, "
529                 "if you quit now all changes will be lost.  Quit anyway?",
530                 @"Quit dialog with changed buffers, text")];
532         if ([alert runModal] != NSAlertFirstButtonReturn)
533             reply = NSTerminateCancel;
535         [alert release];
536     } else {
537         // No unmodified buffers, but give a warning if there are multiple
538         // windows and/or tabs open.
539         int numWindows = [vimControllers count];
540         int numTabs = 0;
542         // Count the number of open tabs
543         e = [vimControllers objectEnumerator];
544         id vc;
545         while ((vc = [e nextObject]))
546             numTabs += [[vc objectForVimStateKey:@"numTabs"] intValue];
548         if (numWindows > 1 || numTabs > 1) {
549             NSAlert *alert = [[NSAlert alloc] init];
550             [alert setAlertStyle:NSWarningAlertStyle];
551             [alert addButtonWithTitle:NSLocalizedString(@"Quit",
552                     @"Dialog button")];
553             [alert addButtonWithTitle:NSLocalizedString(@"Cancel",
554                     @"Dialog button")];
555             [alert setMessageText:NSLocalizedString(
556                     @"Are you sure you want to quit MacVim?",
557                     @"Quit dialog with no changed buffers, title")];
559             NSString *info = nil;
560             if (numWindows > 1) {
561                 if (numTabs > numWindows)
562                     info = [NSString stringWithFormat:NSLocalizedString(
563                             @"There are %d windows open in MacVim, with a "
564                             "total of %d tabs. Do you want to quit anyway?",
565                             @"Quit dialog with no changed buffers, text"),
566                          numWindows, numTabs];
567                 else
568                     info = [NSString stringWithFormat:NSLocalizedString(
569                             @"There are %d windows open in MacVim. "
570                             "Do you want to quit anyway?",
571                             @"Quit dialog with no changed buffers, text"),
572                         numWindows];
574             } else {
575                 info = [NSString stringWithFormat:NSLocalizedString(
576                         @"There are %d tabs open in MacVim. "
577                         "Do you want to quit anyway?",
578                         @"Quit dialog with no changed buffers, text"), 
579                      numTabs];
580             }
582             [alert setInformativeText:info];
584             if ([alert runModal] != NSAlertFirstButtonReturn)
585                 reply = NSTerminateCancel;
587             [alert release];
588         }
589     }
592     // Tell all Vim processes to terminate now (otherwise they'll leave swap
593     // files behind).
594     if (NSTerminateNow == reply) {
595         e = [vimControllers objectEnumerator];
596         id vc;
597         while ((vc = [e nextObject])) {
598             ASLogDebug(@"Terminate pid=%d", [vc pid]);
599             [vc sendMessage:TerminateNowMsgID data:nil];
600         }
602         e = [cachedVimControllers objectEnumerator];
603         while ((vc = [e nextObject])) {
604             ASLogDebug(@"Terminate pid=%d (cached)", [vc pid]);
605             [vc sendMessage:TerminateNowMsgID data:nil];
606         }
608         // If a Vim process is being preloaded as we quit we have to forcibly
609         // kill it since we have not established a connection yet.
610         if (preloadPid > 0) {
611             ASLogDebug(@"Kill incomplete preloaded process pid=%d", preloadPid);
612             kill(preloadPid, SIGKILL);
613         }
615         // If a Vim process was loading as we quit we also have to kill it.
616         e = [[pidArguments allKeys] objectEnumerator];
617         NSNumber *pidKey;
618         while ((pidKey = [e nextObject])) {
619             ASLogDebug(@"Kill incomplete process pid=%d", [pidKey intValue]);
620             kill([pidKey intValue], SIGKILL);
621         }
623         // Sleep a little to allow all the Vim processes to exit.
624         usleep(10000);
625     }
627     return reply;
630 - (void)applicationWillTerminate:(NSNotification *)notification
632     ASLogInfo(@"Terminating MacVim...");
634     [self stopWatchingVimDir];
636 #ifdef MM_ENABLE_PLUGINS
637     [[MMPlugInManager sharedManager] unloadAllPlugIns];
638 #endif
640 #if MM_HANDLE_XCODE_MOD_EVENT
641     [[NSAppleEventManager sharedAppleEventManager]
642             removeEventHandlerForEventClass:'KAHL'
643                                  andEventID:'MOD '];
644 #endif
646     // This will invalidate all connections (since they were spawned from this
647     // connection).
648     [connection invalidate];
650     // Deactivate the font we loaded from the app bundle.
651     // NOTE: This can take quite a while (~500 ms), so termination will be
652     // noticeably faster if loading of the default font is disabled.
653     if (fontContainerRef) {
654         ATSFontDeactivate(fontContainerRef, NULL, kATSOptionFlagsDefault);
655         fontContainerRef = 0;
656     }
658     [NSApp setDelegate:nil];
660     // Try to wait for all child processes to avoid leaving zombies behind (but
661     // don't wait around for too long).
662     NSDate *timeOutDate = [NSDate dateWithTimeIntervalSinceNow:2];
663     while ([timeOutDate timeIntervalSinceNow] > 0) {
664         [self reapChildProcesses:nil];
665         if (numChildProcesses <= 0)
666             break;
668         ASLogDebug(@"%d processes still left, hold on...", numChildProcesses);
670         // Run in NSConnectionReplyMode while waiting instead of calling e.g.
671         // usleep().  Otherwise incoming messages may clog up the DO queues and
672         // the outgoing TerminateNowMsgID sent earlier never reaches the Vim
673         // process.
674         // This has at least one side-effect, namely we may receive the
675         // annoying "dropping incoming DO message".  (E.g. this may happen if
676         // you quickly hit Cmd-n several times in a row and then immediately
677         // press Cmd-q, Enter.)
678         while (CFRunLoopRunInMode((CFStringRef)NSConnectionReplyMode,
679                 0.05, true) == kCFRunLoopRunHandledSource)
680             ;   // do nothing
681     }
683     if (numChildProcesses > 0) {
684         ASLogNotice(@"%d zombies left behind", numChildProcesses);
685     }
688 + (MMAppController *)sharedInstance
690     // Note: The app controller is a singleton which is instantiated in
691     // MainMenu.nib where it is also connected as the delegate of NSApp.
692     id delegate = [NSApp delegate];
693     return [delegate isKindOfClass:self] ? (MMAppController*)delegate : nil;
696 - (NSMenu *)defaultMainMenu
698     return defaultMainMenu;
701 - (NSMenuItem *)appMenuItemTemplate
703     return appMenuItemTemplate;
706 - (void)removeVimController:(id)controller
708     ASLogDebug(@"Remove Vim controller pid=%d id=%d (processingFlag=%d)",
709                [controller pid], [controller vimControllerId], processingFlag);
711     NSUInteger idx = [vimControllers indexOfObject:controller];
712     if (NSNotFound == idx) {
713         ASLogDebug(@"Controller not found, probably due to duplicate removal");
714         return;
715     }
717     [controller retain];
718     [vimControllers removeObjectAtIndex:idx];
719     [controller cleanup];
720     [controller release];
722     if (![vimControllers count]) {
723         // The last editor window just closed so restore the main menu back to
724         // its default state (which is defined in MainMenu.nib).
725         [self setMainMenu:defaultMainMenu];
727         BOOL hide = (MMHideWhenLastWindowClosed ==
728                     [[NSUserDefaults standardUserDefaults]
729                         integerForKey:MMLastWindowClosedBehaviorKey]);
730         if (hide)
731             [NSApp hide:self];
732     }
734     // There is a small delay before the Vim process actually exits so wait a
735     // little before trying to reap the child process.  If the process still
736     // hasn't exited after this wait it won't be reaped until the next time
737     // reapChildProcesses: is called (but this should be harmless).
738     [self performSelector:@selector(reapChildProcesses:)
739                withObject:nil
740                afterDelay:0.1];
743 - (void)windowControllerWillOpen:(MMWindowController *)windowController
745     NSPoint topLeft = NSZeroPoint;
746     NSWindow *cascadeFrom = [[[self topmostVimController] windowController]
747                                                                     window];
748     NSWindow *win = [windowController window];
750     if (!win) return;
752     // Heuristic to determine where to position the window:
753     //   1. Use the default top left position (set using :winpos in .[g]vimrc)
754     //   2. Cascade from an existing window
755     //   3. Use autosaved position
756     // If all of the above fail, then the window position is not changed.
757     if ([windowController getDefaultTopLeft:&topLeft]) {
758         // Make sure the window is not cascaded (note that topLeft was set in
759         // the above call).
760         cascadeFrom = nil;
761     } else if (cascadeFrom) {
762         NSRect frame = [cascadeFrom frame];
763         topLeft = NSMakePoint(frame.origin.x, NSMaxY(frame));
764     } else {
765         NSString *topLeftString = [[NSUserDefaults standardUserDefaults]
766             stringForKey:MMTopLeftPointKey];
767         if (topLeftString)
768             topLeft = NSPointFromString(topLeftString);
769     }
771     if (!NSEqualPoints(topLeft, NSZeroPoint)) {
772         // Try to tile from the correct screen in case the user has multiple
773         // monitors ([win screen] always seems to return the "main" screen).
774         NSScreen *screen = [self screenContainingPoint:topLeft];
775         if (!screen)
776             screen = [win screen];
778         if (cascadeFrom) {
779             // Do manual cascading instead of using
780             // -[MMWindow cascadeTopLeftFromPoint:] since it is rather
781             // unpredictable.
782             topLeft.x += MMCascadeHorizontalOffset;
783             topLeft.y -= MMCascadeVerticalOffset;
784         }
786         if (screen) {
787             // Constrain the window so that it is entirely visible on the
788             // screen.  If it sticks out on the right, move it all the way
789             // left.  If it sticks out on the bottom, move it all the way up.
790             // (Assumption: the cascading offsets are positive.)
791             NSRect screenFrame = [screen frame];
792             NSSize winSize = [win frame].size;
793             NSRect winFrame =
794                 { { topLeft.x, topLeft.y - winSize.height }, winSize };
796             if (NSMaxX(winFrame) > NSMaxX(screenFrame))
797                 topLeft.x = NSMinX(screenFrame);
798             if (NSMinY(winFrame) < NSMinY(screenFrame))
799                 topLeft.y = NSMaxY(screenFrame);
800         } else {
801             ASLogNotice(@"Window not on screen, don't constrain position");
802         }
804         [win setFrameTopLeftPoint:topLeft];
805     }
807     if (1 == [vimControllers count]) {
808         // The first window autosaves its position.  (The autosaving
809         // features of Cocoa are not used because we need more control over
810         // what is autosaved and when it is restored.)
811         [windowController setWindowAutosaveKey:MMTopLeftPointKey];
812     }
814     if (openSelectionString) {
815         // TODO: Pass this as a parameter instead!  Get rid of
816         // 'openSelectionString' etc.
817         //
818         // There is some text to paste into this window as a result of the
819         // services menu "Open selection ..." being used.
820         [[windowController vimController] dropString:openSelectionString];
821         [openSelectionString release];
822         openSelectionString = nil;
823     }
825     if (shouldActivateWhenNextWindowOpens) {
826         [NSApp activateIgnoringOtherApps:YES];
827         shouldActivateWhenNextWindowOpens = NO;
828     }
831 - (void)setMainMenu:(NSMenu *)mainMenu
833     if ([NSApp mainMenu] == mainMenu) return;
835     // If the new menu has a "Recent Files" dummy item, then swap the real item
836     // for the dummy.  We are forced to do this since Cocoa initializes the
837     // "Recent Files" menu and there is no way to simply point Cocoa to a new
838     // item each time the menus are swapped.
839     NSMenu *fileMenu = [mainMenu findFileMenu];
840     if (recentFilesMenuItem && fileMenu) {
841         int dummyIdx =
842                 [fileMenu indexOfItemWithAction:@selector(recentFilesDummy:)];
843         if (dummyIdx >= 0) {
844             NSMenuItem *dummyItem = [[fileMenu itemAtIndex:dummyIdx] retain];
845             [fileMenu removeItemAtIndex:dummyIdx];
847             NSMenu *recentFilesParentMenu = [recentFilesMenuItem menu];
848             int idx = [recentFilesParentMenu indexOfItem:recentFilesMenuItem];
849             if (idx >= 0) {
850                 [[recentFilesMenuItem retain] autorelease];
851                 [recentFilesParentMenu removeItemAtIndex:idx];
852                 [recentFilesParentMenu insertItem:dummyItem atIndex:idx];
853             }
855             [fileMenu insertItem:recentFilesMenuItem atIndex:dummyIdx];
856             [dummyItem release];
857         }
858     }
860     // Now set the new menu.  Notice that we keep one menu for each editor
861     // window since each editor can have its own set of menus.  When swapping
862     // menus we have to tell Cocoa where the new "MacVim", "Windows", and
863     // "Services" menu are.
864     [NSApp setMainMenu:mainMenu];
866     // Setting the "MacVim" (or "Application") menu ensures that it is typeset
867     // in boldface.  (The setAppleMenu: method used to be public but is now
868     // private so this will have to be considered a bit of a hack!)
869     NSMenu *appMenu = [mainMenu findApplicationMenu];
870     [NSApp performSelector:@selector(setAppleMenu:) withObject:appMenu];
872     NSMenu *servicesMenu = [mainMenu findServicesMenu];
873     [NSApp setServicesMenu:servicesMenu];
875     NSMenu *windowsMenu = [mainMenu findWindowsMenu];
876     if (windowsMenu) {
877         // Cocoa isn't clever enough to get rid of items it has added to the
878         // "Windows" menu so we have to do it ourselves otherwise there will be
879         // multiple menu items for each window in the "Windows" menu.
880         //   This code assumes that the only items Cocoa add are ones which
881         // send off the action makeKeyAndOrderFront:.  (Cocoa will not add
882         // another separator item if the last item on the "Windows" menu
883         // already is a separator, so we needen't worry about separators.)
884         int i, count = [windowsMenu numberOfItems];
885         for (i = count-1; i >= 0; --i) {
886             NSMenuItem *item = [windowsMenu itemAtIndex:i];
887             if ([item action] == @selector(makeKeyAndOrderFront:))
888                 [windowsMenu removeItem:item];
889         }
890     }
891     [NSApp setWindowsMenu:windowsMenu];
893 #ifdef MM_ENABLE_PLUGINS
894     // Move plugin menu from old to new main menu.
895     [self removePlugInMenu];
896     [self addPlugInMenuToMenu:mainMenu];
897 #endif
900 - (NSArray *)filterOpenFiles:(NSArray *)filenames
902     return [self filterOpenFiles:filenames openFilesDict:nil];
905 - (BOOL)openFiles:(NSArray *)filenames withArguments:(NSDictionary *)args
907     // Opening files works like this:
908     //  a) filter out any already open files
909     //  b) open any remaining files
910     //
911     // A file is opened in an untitled window if there is one (it may be
912     // currently launching, or it may already be visible), otherwise a new
913     // window is opened.
914     //
915     // Each launching Vim process has a dictionary of arguments that are passed
916     // to the process when in checks in (via connectBackend:pid:).  The
917     // arguments for each launching process can be looked up by its PID (in the
918     // pidArguments dictionary).
920     NSMutableDictionary *arguments = (args ? [[args mutableCopy] autorelease]
921                                            : [NSMutableDictionary dictionary]);
923     filenames = normalizeFilenames(filenames);
925     //
926     // a) Filter out any already open files
927     //
928     NSString *firstFile = [filenames objectAtIndex:0];
929     MMVimController *firstController = nil;
930     NSDictionary *openFilesDict = nil;
931     filenames = [self filterOpenFiles:filenames openFilesDict:&openFilesDict];
933     // Pass arguments to vim controllers that had files open.
934     id key;
935     NSEnumerator *e = [openFilesDict keyEnumerator];
937     // (Indicate that we do not wish to open any files at the moment.)
938     [arguments setObject:[NSNumber numberWithBool:YES] forKey:@"dontOpen"];
940     while ((key = [e nextObject])) {
941         NSArray *files = [openFilesDict objectForKey:key];
942         [arguments setObject:files forKey:@"filenames"];
944         MMVimController *vc = [key pointerValue];
945         [vc passArguments:arguments];
947         // If this controller holds the first file, then remember it for later.
948         if ([files containsObject:firstFile])
949             firstController = vc;
950     }
952     // The meaning of "layout" is defined by the WIN_* defines in main.c.
953     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
954     int layout = [ud integerForKey:MMOpenLayoutKey];
955     BOOL splitVert = [ud boolForKey:MMVerticalSplitKey];
956     BOOL openInCurrentWindow = [ud boolForKey:MMOpenInCurrentWindowKey];
958     if (splitVert && MMLayoutHorizontalSplit == layout)
959         layout = MMLayoutVerticalSplit;
960     if (layout < 0 || (layout > MMLayoutTabs && openInCurrentWindow))
961         layout = MMLayoutTabs;
963     if ([filenames count] == 0) {
964         // Raise the window containing the first file that was already open,
965         // and make sure that the tab containing that file is selected.  Only
966         // do this when there are no more files to open, otherwise sometimes
967         // the window with 'firstFile' will be raised, other times it might be
968         // the window that will open with the files in the 'filenames' array.
969         firstFile = [firstFile stringByEscapingSpecialFilenameCharacters];
971         NSString *bufCmd = @"tab sb";
972         switch (layout) {
973             case MMLayoutHorizontalSplit: bufCmd = @"sb"; break;
974             case MMLayoutVerticalSplit:   bufCmd = @"vert sb"; break;
975             case MMLayoutArglist:         bufCmd = @"b"; break;
976         }
978         NSString *input = [NSString stringWithFormat:@"<C-\\><C-N>"
979                 ":let oldswb=&swb|let &swb=\"useopen,usetab\"|"
980                 "%@ %@|let &swb=oldswb|unl oldswb|"
981                 "cal foreground()<CR>", bufCmd, firstFile];
983         [firstController addVimInput:input];
985         return YES;
986     }
988     // Add filenames to "Recent Files" menu, unless they are being edited
989     // remotely (using ODB).
990     if ([arguments objectForKey:@"remoteID"] == nil) {
991         [[NSDocumentController sharedDocumentController]
992                 noteNewRecentFilePaths:filenames];
993     }
995     //
996     // b) Open any remaining files
997     //
999     [arguments setObject:[NSNumber numberWithInt:layout] forKey:@"layout"];
1000     [arguments setObject:filenames forKey:@"filenames"];
1001     // (Indicate that files should be opened from now on.)
1002     [arguments setObject:[NSNumber numberWithBool:NO] forKey:@"dontOpen"];
1004     MMVimController *vc;
1005     if (openInCurrentWindow && (vc = [self topmostVimController])) {
1006         // Open files in an already open window.
1007         [[[vc windowController] window] makeKeyAndOrderFront:self];
1008         [vc passArguments:arguments];
1009         return YES;
1010     }
1012     BOOL openOk = YES;
1013     int numFiles = [filenames count];
1014     if (MMLayoutWindows == layout && numFiles > 1) {
1015         // Open one file at a time in a new window, but don't open too many at
1016         // once (at most cap+1 windows will open).  If the user has increased
1017         // the preload cache size we'll take that as a hint that more windows
1018         // should be able to open at once.
1019         int cap = [self maxPreloadCacheSize] - 1;
1020         if (cap < 4) cap = 4;
1021         if (cap > numFiles) cap = numFiles;
1023         int i;
1024         for (i = 0; i < cap; ++i) {
1025             NSArray *a = [NSArray arrayWithObject:[filenames objectAtIndex:i]];
1026             [arguments setObject:a forKey:@"filenames"];
1028             // NOTE: We have to copy the args since we'll mutate them in the
1029             // next loop and the below call may retain the arguments while
1030             // waiting for a process to start.
1031             NSDictionary *args = [[arguments copy] autorelease];
1033             openOk = [self openVimControllerWithArguments:args];
1034             if (!openOk) break;
1035         }
1037         // Open remaining files in tabs in a new window.
1038         if (openOk && numFiles > cap) {
1039             NSRange range = { i, numFiles-cap };
1040             NSArray *a = [filenames subarrayWithRange:range];
1041             [arguments setObject:a forKey:@"filenames"];
1042             [arguments setObject:[NSNumber numberWithInt:MMLayoutTabs]
1043                           forKey:@"layout"];
1045             openOk = [self openVimControllerWithArguments:arguments];
1046         }
1047     } else {
1048         // Open all files at once.
1049         openOk = [self openVimControllerWithArguments:arguments];
1050     }
1052     return openOk;
1055 #ifdef MM_ENABLE_PLUGINS
1056 - (void)addItemToPlugInMenu:(NSMenuItem *)item
1058     NSMenu *menu = [plugInMenuItem submenu];
1059     [menu addItem:item];
1060     if ([menu numberOfItems] == 1)
1061         [self addPlugInMenuToMenu:[NSApp mainMenu]];
1064 - (void)removeItemFromPlugInMenu:(NSMenuItem *)item
1066     NSMenu *menu = [plugInMenuItem submenu];
1067     [menu removeItem:item];
1068     if ([menu numberOfItems] == 0)
1069         [self removePlugInMenu];
1071 #endif
1073 - (IBAction)newWindow:(id)sender
1075     ASLogDebug(@"Open new window");
1077     // A cached controller requires no loading times and results in the new
1078     // window popping up instantaneously.  If the cache is empty it may take
1079     // 1-2 seconds to start a new Vim process.
1080     MMVimController *vc = [self takeVimControllerFromCache];
1081     if (vc) {
1082         [[vc backendProxy] acknowledgeConnection];
1083     } else {
1084         [self launchVimProcessWithArguments:nil workingDirectory:nil];
1085     }
1088 - (IBAction)newWindowAndActivate:(id)sender
1090     [self activateWhenNextWindowOpens];
1091     [self newWindow:sender];
1094 - (IBAction)fileOpen:(id)sender
1096     ASLogDebug(@"Show file open panel");
1098     NSString *dir = nil;
1099     BOOL trackPwd = [[NSUserDefaults standardUserDefaults]
1100             boolForKey:MMDialogsTrackPwdKey];
1101     if (trackPwd) {
1102         MMVimController *vc = [self keyVimController];
1103         if (vc) dir = [vc objectForVimStateKey:@"pwd"];
1104     }
1106     NSOpenPanel *panel = [NSOpenPanel openPanel];
1107     [panel setAllowsMultipleSelection:YES];
1108     [panel setAccessoryView:showHiddenFilesView()];
1110     int result = [panel runModalForDirectory:dir file:nil types:nil];
1111     if (NSOKButton == result)
1112         [self application:NSApp openFiles:[panel filenames]];
1115 - (IBAction)selectNextWindow:(id)sender
1117     ASLogDebug(@"Select next window");
1119     unsigned i, count = [vimControllers count];
1120     if (!count) return;
1122     NSWindow *keyWindow = [NSApp keyWindow];
1123     for (i = 0; i < count; ++i) {
1124         MMVimController *vc = [vimControllers objectAtIndex:i];
1125         if ([[[vc windowController] window] isEqual:keyWindow])
1126             break;
1127     }
1129     if (i < count) {
1130         if (++i >= count)
1131             i = 0;
1132         MMVimController *vc = [vimControllers objectAtIndex:i];
1133         [[vc windowController] showWindow:self];
1134     }
1137 - (IBAction)selectPreviousWindow:(id)sender
1139     ASLogDebug(@"Select previous window");
1141     unsigned i, count = [vimControllers count];
1142     if (!count) return;
1144     NSWindow *keyWindow = [NSApp keyWindow];
1145     for (i = 0; i < count; ++i) {
1146         MMVimController *vc = [vimControllers objectAtIndex:i];
1147         if ([[[vc windowController] window] isEqual:keyWindow])
1148             break;
1149     }
1151     if (i < count) {
1152         if (i > 0) {
1153             --i;
1154         } else {
1155             i = count - 1;
1156         }
1157         MMVimController *vc = [vimControllers objectAtIndex:i];
1158         [[vc windowController] showWindow:self];
1159     }
1162 - (IBAction)orderFrontPreferencePanel:(id)sender
1164     ASLogDebug(@"Show preferences panel");
1165     [[MMPreferenceController sharedPrefsWindowController] showWindow:self];
1168 - (IBAction)openWebsite:(id)sender
1170     ASLogDebug(@"Open MacVim website");
1171     [[NSWorkspace sharedWorkspace] openURL:
1172             [NSURL URLWithString:MMWebsiteString]];
1175 - (IBAction)showVimHelp:(id)sender
1177     ASLogDebug(@"Open window with Vim help");
1178     // Open a new window with the help window maximized.
1179     [self launchVimProcessWithArguments:[NSArray arrayWithObjects:
1180                                     @"-c", @":h gui_mac", @"-c", @":res", nil]
1181                        workingDirectory:nil];
1184 - (IBAction)zoomAll:(id)sender
1186     ASLogDebug(@"Zoom all windows");
1187     [NSApp makeWindowsPerform:@selector(performZoom:) inOrder:YES];
1190 - (IBAction)atsuiButtonClicked:(id)sender
1192     ASLogDebug(@"Toggle ATSUI renderer");
1193     NSInteger renderer = MMRendererDefault;
1194     BOOL enable = ([sender state] == NSOnState);
1196     if (enable) {
1197 #if MM_ENABLE_ATSUI
1198         renderer = MMRendererATSUI;
1199 #else
1200         renderer = MMRendererCoreText;
1201 #endif
1202     }
1204     // Update the user default MMRenderer and synchronize the change so that
1205     // any new Vim process will pick up on the changed setting.
1206     CFPreferencesSetAppValue(
1207             (CFStringRef)MMRendererKey,
1208             (CFPropertyListRef)[NSNumber numberWithInt:renderer],
1209             kCFPreferencesCurrentApplication);
1210     CFPreferencesAppSynchronize(kCFPreferencesCurrentApplication);
1212     ASLogInfo(@"Use renderer=%d", renderer);
1214     // This action is called when the user clicks the "use ATSUI renderer"
1215     // button in the advanced preferences pane.
1216     [self rebuildPreloadCache];
1219 - (IBAction)loginShellButtonClicked:(id)sender
1221     ASLogDebug(@"Toggle login shell option");
1222     // This action is called when the user clicks the "use login shell" button
1223     // in the advanced preferences pane.
1224     [self rebuildPreloadCache];
1227 - (IBAction)quickstartButtonClicked:(id)sender
1229     ASLogDebug(@"Toggle Quickstart option");
1230     if ([self maxPreloadCacheSize] > 0) {
1231         [self scheduleVimControllerPreloadAfterDelay:1.0];
1232         [self startWatchingVimDir];
1233     } else {
1234         [self cancelVimControllerPreloadRequests];
1235         [self clearPreloadCacheWithCount:-1];
1236         [self stopWatchingVimDir];
1237     }
1240 - (MMVimController *)keyVimController
1242     NSWindow *keyWindow = [NSApp keyWindow];
1243     if (keyWindow) {
1244         unsigned i, count = [vimControllers count];
1245         for (i = 0; i < count; ++i) {
1246             MMVimController *vc = [vimControllers objectAtIndex:i];
1247             if ([[[vc windowController] window] isEqual:keyWindow])
1248                 return vc;
1249         }
1250     }
1252     return nil;
1255 - (unsigned)connectBackend:(byref in id <MMBackendProtocol>)proxy pid:(int)pid
1257     ASLogDebug(@"pid=%d", pid);
1259     [(NSDistantObject*)proxy setProtocolForProxy:@protocol(MMBackendProtocol)];
1261     // NOTE: Allocate the vim controller now but don't add it to the list of
1262     // controllers since this is a distributed object call and as such can
1263     // arrive at unpredictable times (e.g. while iterating the list of vim
1264     // controllers).
1265     // (What if input arrives before the vim controller is added to the list of
1266     // controllers?  This should not be a problem since the input isn't
1267     // processed immediately (see processInput:forIdentifier:).)
1268     // Also, since the app may be multithreaded (e.g. as a result of showing
1269     // the open panel) we have to ensure this call happens on the main thread,
1270     // else there is a race condition that may lead to a crash.
1271     MMVimController *vc = [[MMVimController alloc] initWithBackend:proxy
1272                                                                pid:pid];
1273     [self performSelectorOnMainThread:@selector(addVimController:)
1274                            withObject:vc
1275                         waitUntilDone:NO
1276                                 modes:[NSArray arrayWithObject:
1277                                        NSDefaultRunLoopMode]];
1279     [vc release];
1281     return [vc vimControllerId];
1284 - (oneway void)processInput:(in bycopy NSArray *)queue
1285               forIdentifier:(unsigned)identifier
1287     // NOTE: Input is not handled immediately since this is a distributed
1288     // object call and as such can arrive at unpredictable times.  Instead,
1289     // queue the input and process it when the run loop is updated.
1291     if (!(queue && identifier)) {
1292         ASLogWarn(@"Bad input for identifier=%d", identifier);
1293         return;
1294     }
1296     ASLogDebug(@"QUEUE for identifier=%d: <<< %@>>>", identifier,
1297                debugStringForMessageQueue(queue));
1299     NSNumber *key = [NSNumber numberWithUnsignedInt:identifier];
1300     NSArray *q = [inputQueues objectForKey:key];
1301     if (q) {
1302         q = [q arrayByAddingObjectsFromArray:queue];
1303         [inputQueues setObject:q forKey:key];
1304     } else {
1305         [inputQueues setObject:queue forKey:key];
1306     }
1308     // NOTE: We must use "event tracking mode" as well as "default mode",
1309     // otherwise the input queue will not be processed e.g. during live
1310     // resizing.
1311     // Also, since the app may be multithreaded (e.g. as a result of showing
1312     // the open panel) we have to ensure this call happens on the main thread,
1313     // else there is a race condition that may lead to a crash.
1314     [self performSelectorOnMainThread:@selector(processInputQueues:)
1315                            withObject:nil
1316                         waitUntilDone:NO
1317                                 modes:[NSArray arrayWithObjects:
1318                                        NSDefaultRunLoopMode,
1319                                        NSEventTrackingRunLoopMode, nil]];
1322 - (NSArray *)serverList
1324     NSMutableArray *array = [NSMutableArray array];
1326     unsigned i, count = [vimControllers count];
1327     for (i = 0; i < count; ++i) {
1328         MMVimController *controller = [vimControllers objectAtIndex:i];
1329         if ([controller serverName])
1330             [array addObject:[controller serverName]];
1331     }
1333     return array;
1336 @end // MMAppController
1341 @implementation MMAppController (MMServices)
1343 - (void)openSelection:(NSPasteboard *)pboard userData:(NSString *)userData
1344                 error:(NSString **)error
1346     if (![[pboard types] containsObject:NSStringPboardType]) {
1347         ASLogNotice(@"Pasteboard contains no NSStringPboardType");
1348         return;
1349     }
1351     ASLogInfo(@"Open new window containing current selection");
1353     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
1354     BOOL openInCurrentWindow = [ud boolForKey:MMOpenInCurrentWindowKey];
1355     MMVimController *vc;
1357     if (openInCurrentWindow && (vc = [self topmostVimController])) {
1358         [vc sendMessage:AddNewTabMsgID data:nil];
1359         [vc dropString:[pboard stringForType:NSStringPboardType]];
1360     } else {
1361         // Save the text, open a new window, and paste the text when the next
1362         // window opens.  (If this is called several times in a row, then all
1363         // but the last call may be ignored.)
1364         if (openSelectionString) [openSelectionString release];
1365         openSelectionString = [[pboard stringForType:NSStringPboardType] copy];
1367         [self newWindow:self];
1368     }
1371 - (void)openFile:(NSPasteboard *)pboard userData:(NSString *)userData
1372            error:(NSString **)error
1374     if (![[pboard types] containsObject:NSStringPboardType]) {
1375         ASLogNotice(@"Pasteboard contains no NSStringPboardType");
1376         return;
1377     }
1379     // TODO: Parse multiple filenames and create array with names.
1380     NSString *string = [pboard stringForType:NSStringPboardType];
1381     string = [string stringByTrimmingCharactersInSet:
1382             [NSCharacterSet whitespaceAndNewlineCharacterSet]];
1383     string = [string stringByStandardizingPath];
1385     ASLogInfo(@"Open new window with selected file: %@", string);
1387     NSArray *filenames = [self filterFilesAndNotify:
1388             [NSArray arrayWithObject:string]];
1389     if ([filenames count] == 0)
1390         return;
1392     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
1393     BOOL openInCurrentWindow = [ud boolForKey:MMOpenInCurrentWindowKey];
1394     MMVimController *vc;
1396     if (openInCurrentWindow && (vc = [self topmostVimController])) {
1397         [vc dropFiles:filenames forceOpen:YES];
1398     } else {
1399         [self openFiles:filenames withArguments:nil];
1400     }
1403 - (void)newFileHere:(NSPasteboard *)pboard userData:(NSString *)userData
1404               error:(NSString **)error
1406     if (![[pboard types] containsObject:NSFilenamesPboardType]) {
1407         ASLogNotice(@"Pasteboard contains no NSFilenamesPboardType");
1408         return;
1409     }
1411     NSArray *filenames = [pboard propertyListForType:NSFilenamesPboardType];
1412     NSString *path = [filenames lastObject];
1414     BOOL dirIndicator;
1415     if (![[NSFileManager defaultManager] fileExistsAtPath:path
1416                                               isDirectory:&dirIndicator]) {
1417         ASLogNotice(@"Invalid path. Cannot open new document at: %@", path);
1418         return;
1419     }
1421     ASLogInfo(@"Open new file at path=%@", path);
1423     if (!dirIndicator)
1424         path = [path stringByDeletingLastPathComponent];
1426     path = [path stringByEscapingSpecialFilenameCharacters];
1428     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
1429     BOOL openInCurrentWindow = [ud boolForKey:MMOpenInCurrentWindowKey];
1430     MMVimController *vc;
1432     if (openInCurrentWindow && (vc = [self topmostVimController])) {
1433         NSString *input = [NSString stringWithFormat:@"<C-\\><C-N>"
1434                 ":tabe|cd %@<CR>", path];
1435         [vc addVimInput:input];
1436     } else {
1437         [self launchVimProcessWithArguments:nil workingDirectory:path];
1438     }
1441 @end // MMAppController (MMServices)
1446 @implementation MMAppController (Private)
1448 - (MMVimController *)topmostVimController
1450     // Find the topmost visible window which has an associated vim controller.
1451     NSEnumerator *e = [[NSApp orderedWindows] objectEnumerator];
1452     id window;
1453     while ((window = [e nextObject]) && [window isVisible]) {
1454         unsigned i, count = [vimControllers count];
1455         for (i = 0; i < count; ++i) {
1456             MMVimController *vc = [vimControllers objectAtIndex:i];
1457             if ([[[vc windowController] window] isEqual:window])
1458                 return vc;
1459         }
1460     }
1462     return nil;
1465 - (int)launchVimProcessWithArguments:(NSArray *)args
1466                     workingDirectory:(NSString *)cwd
1468     int pid = -1;
1469     NSString *path = [[NSBundle mainBundle] pathForAuxiliaryExecutable:@"Vim"];
1471     if (!path) {
1472         ASLogCrit(@"Vim executable could not be found inside app bundle!");
1473         return -1;
1474     }
1476     // Change current working directory so that the child process picks it up.
1477     NSFileManager *fm = [NSFileManager defaultManager];
1478     NSString *restoreCwd = nil;
1479     if (cwd) {
1480         restoreCwd = [fm currentDirectoryPath];
1481         [fm changeCurrentDirectoryPath:cwd];
1482     }
1484     NSArray *taskArgs = [NSArray arrayWithObjects:@"-g", @"-f", nil];
1485     if (args)
1486         taskArgs = [taskArgs arrayByAddingObjectsFromArray:args];
1488     BOOL useLoginShell = [[NSUserDefaults standardUserDefaults]
1489             boolForKey:MMLoginShellKey];
1490     if (useLoginShell) {
1491         // Run process with a login shell, roughly:
1492         //   echo "exec Vim -g -f args" | ARGV0=-`basename $SHELL` $SHELL [-l]
1493         pid = [self executeInLoginShell:path arguments:taskArgs];
1494     } else {
1495         // Run process directly:
1496         //   Vim -g -f args
1497         NSTask *task = [NSTask launchedTaskWithLaunchPath:path
1498                                                 arguments:taskArgs];
1499         pid = task ? [task processIdentifier] : -1;
1500     }
1502     if (-1 != pid) {
1503         // The 'pidArguments' dictionary keeps arguments to be passed to the
1504         // process when it connects (this is in contrast to arguments which are
1505         // passed on the command line, like '-f' and '-g').
1506         // If this method is called with nil arguments we take this as a hint
1507         // that this is an "untitled window" being launched and add a null
1508         // object to the 'pidArguments' dictionary.  This way we can detect if
1509         // an untitled window is being launched by looking for null objects in
1510         // this dictionary.
1511         // If this method is called with non-nil arguments then it is assumed
1512         // that the caller takes care of adding items to 'pidArguments' as
1513         // necessary (only some arguments are passed on connect, e.g. files to
1514         // open).
1515         if (!args)
1516             [pidArguments setObject:[NSNull null]
1517                              forKey:[NSNumber numberWithInt:pid]];
1518     } else {
1519         ASLogWarn(@"Failed to launch Vim process: args=%@, useLoginShell=%d",
1520                   args, useLoginShell);
1521     }
1523     // Now that child has launched, restore the current working directory.
1524     if (restoreCwd)
1525         [fm changeCurrentDirectoryPath:restoreCwd];
1527     return pid;
1530 - (NSArray *)filterFilesAndNotify:(NSArray *)filenames
1532     // Go trough 'filenames' array and make sure each file exists.  Present
1533     // warning dialog if some file was missing.
1535     NSString *firstMissingFile = nil;
1536     NSMutableArray *files = [NSMutableArray array];
1537     unsigned i, count = [filenames count];
1539     for (i = 0; i < count; ++i) {
1540         NSString *name = [filenames objectAtIndex:i];
1541         if ([[NSFileManager defaultManager] fileExistsAtPath:name]) {
1542             [files addObject:name];
1543         } else if (!firstMissingFile) {
1544             firstMissingFile = name;
1545         }
1546     }
1548     if (firstMissingFile) {
1549         NSAlert *alert = [[NSAlert alloc] init];
1550         [alert addButtonWithTitle:NSLocalizedString(@"OK",
1551                 @"Dialog button")];
1553         NSString *text;
1554         if ([files count] >= count-1) {
1555             [alert setMessageText:NSLocalizedString(@"File not found",
1556                     @"File not found dialog, title")];
1557             text = [NSString stringWithFormat:NSLocalizedString(
1558                     @"Could not open file with name %@.",
1559                     @"File not found dialog, text"), firstMissingFile];
1560         } else {
1561             [alert setMessageText:NSLocalizedString(@"Multiple files not found",
1562                     @"File not found dialog, title")];
1563             text = [NSString stringWithFormat:NSLocalizedString(
1564                     @"Could not open file with name %@, and %d other files.",
1565                     @"File not found dialog, text"),
1566                 firstMissingFile, count-[files count]-1];
1567         }
1569         [alert setInformativeText:text];
1570         [alert setAlertStyle:NSWarningAlertStyle];
1572         [alert runModal];
1573         [alert release];
1575         [NSApp replyToOpenOrPrint:NSApplicationDelegateReplyFailure];
1576     }
1578     return files;
1581 - (NSArray *)filterOpenFiles:(NSArray *)filenames
1582                openFilesDict:(NSDictionary **)openFiles
1584     // Filter out any files in the 'filenames' array that are open and return
1585     // all files that are not already open.  On return, the 'openFiles'
1586     // parameter (if non-nil) will point to a dictionary of open files, indexed
1587     // by Vim controller.
1589     NSMutableDictionary *dict = [NSMutableDictionary dictionary];
1590     NSMutableArray *files = [filenames mutableCopy];
1592     // TODO: Escape special characters in 'files'?
1593     NSString *expr = [NSString stringWithFormat:
1594             @"map([\"%@\"],\"bufloaded(v:val)\")",
1595             [files componentsJoinedByString:@"\",\""]];
1597     unsigned i, count = [vimControllers count];
1598     for (i = 0; i < count && [files count] > 0; ++i) {
1599         MMVimController *vc = [vimControllers objectAtIndex:i];
1601         // Query Vim for which files in the 'files' array are open.
1602         NSString *eval = [vc evaluateVimExpression:expr];
1603         if (!eval) continue;
1605         NSIndexSet *idxSet = [NSIndexSet indexSetWithVimList:eval];
1606         if ([idxSet count] > 0) {
1607             [dict setObject:[files objectsAtIndexes:idxSet]
1608                      forKey:[NSValue valueWithPointer:vc]];
1610             // Remove all the files that were open in this Vim process and
1611             // create a new expression to evaluate.
1612             [files removeObjectsAtIndexes:idxSet];
1613             expr = [NSString stringWithFormat:
1614                     @"map([\"%@\"],\"bufloaded(v:val)\")",
1615                     [files componentsJoinedByString:@"\",\""]];
1616         }
1617     }
1619     if (openFiles != nil)
1620         *openFiles = dict;
1622     return [files autorelease];
1625 #if MM_HANDLE_XCODE_MOD_EVENT
1626 - (void)handleXcodeModEvent:(NSAppleEventDescriptor *)event
1627                  replyEvent:(NSAppleEventDescriptor *)reply
1629 #if 0
1630     // Xcode sends this event to query MacVim which open files have been
1631     // modified.
1632     ASLogDebug(@"reply:%@", reply);
1633     ASLogDebug(@"event:%@", event);
1635     NSEnumerator *e = [vimControllers objectEnumerator];
1636     id vc;
1637     while ((vc = [e nextObject])) {
1638         DescType type = [reply descriptorType];
1639         unsigned len = [[type data] length];
1640         NSMutableData *data = [NSMutableData data];
1642         [data appendBytes:&type length:sizeof(DescType)];
1643         [data appendBytes:&len length:sizeof(unsigned)];
1644         [data appendBytes:[reply data] length:len];
1646         [vc sendMessage:XcodeModMsgID data:data];
1647     }
1648 #endif
1650 #endif
1652 - (void)handleGetURLEvent:(NSAppleEventDescriptor *)event
1653                replyEvent:(NSAppleEventDescriptor *)reply
1655     NSString *urlString = [[event paramDescriptorForKeyword:keyDirectObject]
1656         stringValue];
1657     NSURL *url = [NSURL URLWithString:urlString];
1659     // We try to be compatible with TextMate's URL scheme here, as documented
1660     // at http://blog.macromates.com/2007/the-textmate-url-scheme/ . Currently,
1661     // this means that:
1662     //
1663     // The format is: mvim://open?<arguments> where arguments can be:
1664     //
1665     // * url â€” the actual file to open (i.e. a file://… URL), if you leave
1666     //         out this argument, the frontmost document is implied.
1667     // * line â€” line number to go to (one based).
1668     // * column â€” column number to go to (one based).
1669     //
1670     // Example: mvim://open?url=file:///etc/profile&line=20
1672     if ([[url host] isEqualToString:@"open"]) {
1673         NSMutableDictionary *dict = [NSMutableDictionary dictionary];
1675         // Parse query ("url=file://...&line=14") into a dictionary
1676         NSArray *queries = [[url query] componentsSeparatedByString:@"&"];
1677         NSEnumerator *enumerator = [queries objectEnumerator];
1678         NSString *param;
1679         while ((param = [enumerator nextObject])) {
1680             NSArray *arr = [param componentsSeparatedByString:@"="];
1681             if ([arr count] == 2) {
1682                 [dict setValue:[[arr lastObject]
1683                             stringByReplacingPercentEscapesUsingEncoding:
1684                                 NSUTF8StringEncoding]
1685                         forKey:[[arr objectAtIndex:0]
1686                             stringByReplacingPercentEscapesUsingEncoding:
1687                                 NSUTF8StringEncoding]];
1688             }
1689         }
1691         // Actually open the file.
1692         NSString *file = [dict objectForKey:@"url"];
1693         if (file != nil) {
1694             NSURL *fileUrl= [NSURL URLWithString:file];
1695             // TextMate only opens files that already exist.
1696             if ([fileUrl isFileURL]
1697                     && [[NSFileManager defaultManager] fileExistsAtPath:
1698                            [fileUrl path]]) {
1699                 // Strip 'file://' path, else application:openFiles: might think
1700                 // the file is not yet open.
1701                 NSArray *filenames = [NSArray arrayWithObject:[fileUrl path]];
1703                 // Look for the line and column options.
1704                 NSDictionary *args = nil;
1705                 NSString *line = [dict objectForKey:@"line"];
1706                 if (line) {
1707                     NSString *column = [dict objectForKey:@"column"];
1708                     if (column)
1709                         args = [NSDictionary dictionaryWithObjectsAndKeys:
1710                                 line, @"cursorLine",
1711                                 column, @"cursorColumn",
1712                                 nil];
1713                     else
1714                         args = [NSDictionary dictionaryWithObject:line
1715                                 forKey:@"cursorLine"];
1716                 }
1718                 [self openFiles:filenames withArguments:args];
1719             }
1720         }
1721     } else {
1722         NSAlert *alert = [[NSAlert alloc] init];
1723         [alert addButtonWithTitle:NSLocalizedString(@"OK",
1724             @"Dialog button")];
1726         [alert setMessageText:NSLocalizedString(@"Unknown URL Scheme",
1727             @"Unknown URL Scheme dialog, title")];
1728         [alert setInformativeText:[NSString stringWithFormat:NSLocalizedString(
1729             @"This version of MacVim does not support \"%@\""
1730             @" in its URL scheme.",
1731             @"Unknown URL Scheme dialog, text"),
1732             [url host]]];
1734         [alert setAlertStyle:NSWarningAlertStyle];
1735         [alert runModal];
1736         [alert release];
1737     }
1740 - (NSMutableDictionary *)extractArgumentsFromOdocEvent:
1741     (NSAppleEventDescriptor *)desc
1743     NSMutableDictionary *dict = [NSMutableDictionary dictionary];
1745     // 1. Extract ODB parameters (if any)
1746     NSAppleEventDescriptor *odbdesc = desc;
1747     if (![odbdesc paramDescriptorForKeyword:keyFileSender]) {
1748         // The ODB paramaters may hide inside the 'keyAEPropData' descriptor.
1749         odbdesc = [odbdesc paramDescriptorForKeyword:keyAEPropData];
1750         if (![odbdesc paramDescriptorForKeyword:keyFileSender])
1751             odbdesc = nil;
1752     }
1754     if (odbdesc) {
1755         NSAppleEventDescriptor *p =
1756                 [odbdesc paramDescriptorForKeyword:keyFileSender];
1757         if (p)
1758             [dict setObject:[NSNumber numberWithUnsignedInt:[p typeCodeValue]]
1759                      forKey:@"remoteID"];
1761         p = [odbdesc paramDescriptorForKeyword:keyFileCustomPath];
1762         if (p)
1763             [dict setObject:[p stringValue] forKey:@"remotePath"];
1765         p = [odbdesc paramDescriptorForKeyword:keyFileSenderToken];
1766         if (p) {
1767             [dict setObject:[NSNumber numberWithUnsignedLong:[p descriptorType]]
1768                      forKey:@"remoteTokenDescType"];
1769             [dict setObject:[p data] forKey:@"remoteTokenData"];
1770         }
1771     }
1773     // 2. Extract Xcode parameters (if any)
1774     NSAppleEventDescriptor *xcodedesc =
1775             [desc paramDescriptorForKeyword:keyAEPosition];
1776     if (xcodedesc) {
1777         NSRange range;
1778         NSData *data = [xcodedesc data];
1779         NSUInteger length = [data length];
1781         if (length == sizeof(MMXcodeSelectionRange)) {
1782             MMXcodeSelectionRange *sr = (MMXcodeSelectionRange*)[data bytes];
1783             ASLogDebug(@"Xcode selection range (%d,%d,%d,%d,%d,%d)",
1784                     sr->unused1, sr->lineNum, sr->startRange, sr->endRange,
1785                     sr->unused2, sr->theDate);
1787             if (sr->lineNum < 0) {
1788                 // Should select a range of lines.
1789                 range.location = sr->startRange + 1;
1790                 range.length = sr->endRange - sr->startRange + 1;
1791             } else {
1792                 // Should only move cursor to a line.
1793                 range.location = sr->lineNum + 1;
1794                 range.length = 0;
1795             }
1797             [dict setObject:NSStringFromRange(range) forKey:@"selectionRange"];
1798         } else {
1799             ASLogErr(@"Xcode selection range size mismatch! got=%d expected=%d",
1800                     length, sizeof(MMXcodeSelectionRange));
1801         }
1802     }
1804     // 3. Extract Spotlight search text (if any)
1805     NSAppleEventDescriptor *spotlightdesc = 
1806             [desc paramDescriptorForKeyword:keyAESearchText];
1807     if (spotlightdesc)
1808         [dict setObject:[spotlightdesc stringValue] forKey:@"searchText"];
1810     return dict;
1813 #ifdef MM_ENABLE_PLUGINS
1814 - (void)removePlugInMenu
1816     if ([plugInMenuItem menu])
1817         [[plugInMenuItem menu] removeItem:plugInMenuItem];
1820 - (void)addPlugInMenuToMenu:(NSMenu *)mainMenu
1822     NSMenu *windowsMenu = [mainMenu findWindowsMenu];
1824     if ([[plugInMenuItem submenu] numberOfItems] > 0) {
1825         int idx = windowsMenu ? [mainMenu indexOfItemWithSubmenu:windowsMenu]
1826                               : -1;
1827         if (idx > 0) {
1828             [mainMenu insertItem:plugInMenuItem atIndex:idx];
1829         } else {
1830             [mainMenu addItem:plugInMenuItem];
1831         }
1832     }
1834 #endif
1836 - (void)scheduleVimControllerPreloadAfterDelay:(NSTimeInterval)delay
1838     [self performSelector:@selector(preloadVimController:)
1839                withObject:nil
1840                afterDelay:delay];
1843 - (void)cancelVimControllerPreloadRequests
1845     [NSObject cancelPreviousPerformRequestsWithTarget:self
1846             selector:@selector(preloadVimController:)
1847               object:nil];
1850 - (void)preloadVimController:(id)sender
1852     // We only allow preloading of one Vim process at a time (to avoid hogging
1853     // CPU), so schedule another preload in a little while if necessary.
1854     if (-1 != preloadPid) {
1855         [self scheduleVimControllerPreloadAfterDelay:2];
1856         return;
1857     }
1859     if ([cachedVimControllers count] >= [self maxPreloadCacheSize])
1860         return;
1862     preloadPid = [self launchVimProcessWithArguments:
1863                                     [NSArray arrayWithObject:@"--mmwaitforack"]
1864                                     workingDirectory:nil];
1867 - (int)maxPreloadCacheSize
1869     // The maximum number of Vim processes to keep in the cache can be
1870     // controlled via the user default "MMPreloadCacheSize".
1871     int maxCacheSize = [[NSUserDefaults standardUserDefaults]
1872             integerForKey:MMPreloadCacheSizeKey];
1873     if (maxCacheSize < 0) maxCacheSize = 0;
1874     else if (maxCacheSize > 10) maxCacheSize = 10;
1876     return maxCacheSize;
1879 - (MMVimController *)takeVimControllerFromCache
1881     // NOTE: After calling this message the backend corresponding to the
1882     // returned vim controller must be sent an acknowledgeConnection message,
1883     // else the vim process will be stuck.
1884     //
1885     // This method may return nil even though the cache might be non-empty; the
1886     // caller should handle this by starting a new Vim process.
1888     int i, count = [cachedVimControllers count];
1889     if (0 == count) return nil;
1891     // Locate the first Vim controller with up-to-date rc-files sourced.
1892     NSDate *rcDate = [self rcFilesModificationDate];
1893     for (i = 0; i < count; ++i) {
1894         MMVimController *vc = [cachedVimControllers objectAtIndex:i];
1895         NSDate *date = [vc creationDate];
1896         if ([date compare:rcDate] != NSOrderedAscending)
1897             break;
1898     }
1900     if (i > 0) {
1901         // Clear out cache entries whose vimrc/gvimrc files were sourced before
1902         // the latest modification date for those files.  This ensures that the
1903         // latest rc-files are always sourced for new windows.
1904         [self clearPreloadCacheWithCount:i];
1905     }
1907     if ([cachedVimControllers count] == 0) {
1908         [self scheduleVimControllerPreloadAfterDelay:2.0];
1909         return nil;
1910     }
1912     MMVimController *vc = [cachedVimControllers objectAtIndex:0];
1913     [vimControllers addObject:vc];
1914     [cachedVimControllers removeObjectAtIndex:0];
1915     [vc setIsPreloading:NO];
1917     // If the Vim process has finished loading then the window will displayed
1918     // now, otherwise it will be displayed when the OpenWindowMsgID message is
1919     // received.
1920     [[vc windowController] showWindow];
1922     // Since we've taken one controller from the cache we take the opportunity
1923     // to preload another.
1924     [self scheduleVimControllerPreloadAfterDelay:1];
1926     return vc;
1929 - (void)clearPreloadCacheWithCount:(int)count
1931     // Remove the 'count' first entries in the preload cache.  It is assumed
1932     // that objects are added/removed from the cache in a FIFO manner so that
1933     // this effectively clears the 'count' oldest entries.
1934     // If 'count' is negative, then the entire cache is cleared.
1936     if ([cachedVimControllers count] == 0 || count == 0)
1937         return;
1939     if (count < 0)
1940         count = [cachedVimControllers count];
1942     // Make sure the preloaded Vim processes get killed or they'll just hang
1943     // around being useless until MacVim is terminated.
1944     NSEnumerator *e = [cachedVimControllers objectEnumerator];
1945     MMVimController *vc;
1946     int n = count;
1947     while ((vc = [e nextObject]) && n-- > 0) {
1948         [[NSNotificationCenter defaultCenter] removeObserver:vc];
1949         [vc sendMessage:TerminateNowMsgID data:nil];
1951         // Since the preloaded processes were killed "prematurely" we have to
1952         // manually tell them to cleanup (it is not enough to simply release
1953         // them since deallocation and cleanup are separated).
1954         [vc cleanup];
1955     }
1957     n = count;
1958     while (n-- > 0 && [cachedVimControllers count] > 0)
1959         [cachedVimControllers removeObjectAtIndex:0];
1961     // There is a small delay before the Vim process actually exits so wait a
1962     // little before trying to reap the child process.  If the process still
1963     // hasn't exited after this wait it won't be reaped until the next time
1964     // reapChildProcesses: is called (but this should be harmless).
1965     [self performSelector:@selector(reapChildProcesses:)
1966                withObject:nil
1967                afterDelay:0.1];
1970 - (void)rebuildPreloadCache
1972     if ([self maxPreloadCacheSize] > 0) {
1973         [self clearPreloadCacheWithCount:-1];
1974         [self cancelVimControllerPreloadRequests];
1975         [self scheduleVimControllerPreloadAfterDelay:1.0];
1976     }
1980 // HACK: fileAttributesAtPath was deprecated in 10.5
1981 #if (MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5)
1982 #define MM_fileAttributes(fm,p) [fm attributesOfItemAtPath:p error:NULL]
1983 #else
1984 #define MM_fileAttributes(fm,p) [fm fileAttributesAtPath:p traverseLink:YES]
1985 #endif
1986 - (NSDate *)rcFilesModificationDate
1988     // Check modification dates for ~/.vimrc and ~/.gvimrc and return the
1989     // latest modification date.  If ~/.vimrc does not exist, check ~/_vimrc
1990     // and similarly for gvimrc.
1991     // Returns distantPath if no rc files were found.
1993     NSDate *date = [NSDate distantPast];
1994     NSFileManager *fm = [NSFileManager defaultManager];
1996     NSString *path = [@"~/.vimrc" stringByExpandingTildeInPath];
1997     NSDictionary *attr = MM_fileAttributes(fm, path);
1998     if (!attr) {
1999         path = [@"~/_vimrc" stringByExpandingTildeInPath];
2000         attr = MM_fileAttributes(fm, path);
2001     }
2002     NSDate *modDate = [attr objectForKey:NSFileModificationDate];
2003     if (modDate)
2004         date = modDate;
2006     path = [@"~/.gvimrc" stringByExpandingTildeInPath];
2007     attr = MM_fileAttributes(fm, path);
2008     if (!attr) {
2009         path = [@"~/_gvimrc" stringByExpandingTildeInPath];
2010         attr = MM_fileAttributes(fm, path);
2011     }
2012     modDate = [attr objectForKey:NSFileModificationDate];
2013     if (modDate)
2014         date = [date laterDate:modDate];
2016     return date;
2018 #undef MM_fileAttributes
2020 - (BOOL)openVimControllerWithArguments:(NSDictionary *)arguments
2022     MMVimController *vc = [self takeVimControllerFromCache];
2023     if (vc) {
2024         // Open files in a new window using a cached vim controller.  This
2025         // requires virtually no loading time so the new window will pop up
2026         // instantaneously.
2027         [vc passArguments:arguments];
2028         [[vc backendProxy] acknowledgeConnection];
2029     } else {
2030         NSArray *cmdline = nil;
2031         NSString *cwd = [self workingDirectoryForArguments:arguments];
2032         arguments = [self convertVimControllerArguments:arguments
2033                                           toCommandLine:&cmdline];
2034         int pid = [self launchVimProcessWithArguments:cmdline
2035                                      workingDirectory:cwd];
2036         if (-1 == pid)
2037             return NO;
2039         // TODO: If the Vim process fails to start, or if it changes PID,
2040         // then the memory allocated for these parameters will leak.
2041         // Ensure that this cannot happen or somehow detect it.
2043         if ([arguments count] > 0)
2044             [pidArguments setObject:arguments
2045                              forKey:[NSNumber numberWithInt:pid]];
2046     }
2048     return YES;
2051 - (void)activateWhenNextWindowOpens
2053     ASLogDebug(@"Activate MacVim when next window opens");
2054     shouldActivateWhenNextWindowOpens = YES;
2057 - (void)startWatchingVimDir
2059 #if (MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5)
2060     if (fsEventStream)
2061         return;
2062     if (NULL == FSEventStreamStart)
2063         return; // FSEvent functions are weakly linked
2065     NSString *path = [@"~/.vim" stringByExpandingTildeInPath];
2066     NSArray *pathsToWatch = [NSArray arrayWithObject:path];
2068     fsEventStream = FSEventStreamCreate(NULL, &fsEventCallback, NULL,
2069             (CFArrayRef)pathsToWatch, kFSEventStreamEventIdSinceNow,
2070             MMEventStreamLatency, kFSEventStreamCreateFlagNone);
2072     FSEventStreamScheduleWithRunLoop(fsEventStream,
2073             [[NSRunLoop currentRunLoop] getCFRunLoop],
2074             kCFRunLoopDefaultMode);
2076     FSEventStreamStart(fsEventStream);
2077     ASLogDebug(@"Started FS event stream");
2078 #endif
2081 - (void)stopWatchingVimDir
2083 #if (MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5)
2084     if (NULL == FSEventStreamStop)
2085         return; // FSEvent functions are weakly linked
2087     if (fsEventStream) {
2088         FSEventStreamStop(fsEventStream);
2089         FSEventStreamInvalidate(fsEventStream);
2090         FSEventStreamRelease(fsEventStream);
2091         fsEventStream = NULL;
2092         ASLogDebug(@"Stopped FS event stream");
2093     }
2094 #endif
2098 - (void)handleFSEvent
2100     [self clearPreloadCacheWithCount:-1];
2102     // Several FS events may arrive in quick succession so make sure to cancel
2103     // any previous preload requests before making a new one.
2104     [self cancelVimControllerPreloadRequests];
2105     [self scheduleVimControllerPreloadAfterDelay:0.5];
2108 - (void)loadDefaultFont
2110     // It is possible to set a user default to avoid loading the default font
2111     // (this cuts down on startup time).
2112     if (![[NSUserDefaults standardUserDefaults] boolForKey:MMLoadDefaultFontKey]
2113             || fontContainerRef) {
2114         ASLogInfo(@"Skip loading of the default font...");
2115         return;
2116     }
2118     ASLogInfo(@"Loading the default font...");
2120     // Load all fonts in the Resouces folder of the app bundle.
2121     NSString *fontsFolder = [[NSBundle mainBundle] resourcePath];
2122     if (fontsFolder) {
2123         NSURL *fontsURL = [NSURL fileURLWithPath:fontsFolder];
2124         if (fontsURL) {
2125             FSRef fsRef;
2126             CFURLGetFSRef((CFURLRef)fontsURL, &fsRef);
2128 #if (MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5)
2129             // This is the font activation API for OS X 10.5.  Only compile
2130             // this code if we're building on OS X 10.5 or later.
2131             if (NULL != ATSFontActivateFromFileReference) { // Weakly linked
2132                 ATSFontActivateFromFileReference(&fsRef, kATSFontContextLocal,
2133                                                  kATSFontFormatUnspecified,
2134                                                  NULL, kATSOptionFlagsDefault,
2135                                                  &fontContainerRef);
2136             }
2137 #endif
2138 #if (MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_5)
2139             // The following font activation API was deprecated in OS X 10.5.
2140             // Don't compile this code unless we're targeting OS X 10.4.
2141             FSSpec fsSpec;
2142             if (fontContainerRef == 0 &&
2143                     FSGetCatalogInfo(&fsRef, kFSCatInfoNone, NULL, NULL,
2144                                      &fsSpec, NULL) == noErr) {
2145                 ATSFontActivateFromFileSpecification(&fsSpec,
2146                         kATSFontContextLocal, kATSFontFormatUnspecified, NULL,
2147                         kATSOptionFlagsDefault, &fontContainerRef);
2148             }
2149 #endif
2150         }
2151     }
2153     if (!fontContainerRef) {
2154         ASLogNotice(@"Failed to activate the default font (the app bundle "
2155                     "may be incomplete)");
2156     }
2159 - (int)executeInLoginShell:(NSString *)path arguments:(NSArray *)args
2161     // Start a login shell and execute the command 'path' with arguments 'args'
2162     // in the shell.  This ensures that user environment variables are set even
2163     // when MacVim was started from the Finder.
2165     int pid = -1;
2166     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
2168     // Determine which shell to use to execute the command.  The user
2169     // may decide which shell to use by setting a user default or the
2170     // $SHELL environment variable.
2171     NSString *shell = [ud stringForKey:MMLoginShellCommandKey];
2172     if (!shell || [shell length] == 0)
2173         shell = [[[NSProcessInfo processInfo] environment]
2174             objectForKey:@"SHELL"];
2175     if (!shell)
2176         shell = @"/bin/bash";
2178     // Bash needs the '-l' flag to launch a login shell.  The user may add
2179     // flags by setting a user default.
2180     NSString *shellArgument = [ud stringForKey:MMLoginShellArgumentKey];
2181     if (!shellArgument || [shellArgument length] == 0) {
2182         if ([[shell lastPathComponent] isEqual:@"bash"])
2183             shellArgument = @"-l";
2184         else
2185             shellArgument = nil;
2186     }
2188     // Build input string to pipe to the login shell.
2189     NSMutableString *input = [NSMutableString stringWithFormat:
2190             @"exec \"%@\"", path];
2191     if (args) {
2192         // Append all arguments, making sure they are properly quoted, even
2193         // when they contain single quotes.
2194         NSEnumerator *e = [args objectEnumerator];
2195         id obj;
2197         while ((obj = [e nextObject])) {
2198             NSMutableString *arg = [NSMutableString stringWithString:obj];
2199             [arg replaceOccurrencesOfString:@"'" withString:@"'\"'\"'"
2200                                     options:NSLiteralSearch
2201                                       range:NSMakeRange(0, [arg length])];
2202             [input appendFormat:@" '%@'", arg];
2203         }
2204     }
2206     // Build the argument vector used to start the login shell.
2207     NSString *shellArg0 = [NSString stringWithFormat:@"-%@",
2208              [shell lastPathComponent]];
2209     char *shellArgv[3] = { (char *)[shellArg0 UTF8String], NULL, NULL };
2210     if (shellArgument)
2211         shellArgv[1] = (char *)[shellArgument UTF8String];
2213     // Get the C string representation of the shell path before the fork since
2214     // we must not call Foundation functions after a fork.
2215     const char *shellPath = [shell fileSystemRepresentation];
2217     // Fork and execute the process.
2218     int ds[2];
2219     if (pipe(ds)) return -1;
2221     pid = fork();
2222     if (pid == -1) {
2223         return -1;
2224     } else if (pid == 0) {
2225         // Child process
2227         if (close(ds[1]) == -1) exit(255);
2228         if (dup2(ds[0], 0) == -1) exit(255);
2230         // Without the following call warning messages like this appear on the
2231         // console:
2232         //     com.apple.launchd[69] : Stray process with PGID equal to this
2233         //                             dead job: PID 1589 PPID 1 Vim
2234         setsid();
2236         execv(shellPath, shellArgv);
2238         // Never reached unless execv fails
2239         exit(255);
2240     } else {
2241         // Parent process
2242         if (close(ds[0]) == -1) return -1;
2244         // Send input to execute to the child process
2245         [input appendString:@"\n"];
2246         int bytes = [input lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
2248         if (write(ds[1], [input UTF8String], bytes) != bytes) return -1;
2249         if (close(ds[1]) == -1) return -1;
2251         ++numChildProcesses;
2252         ASLogDebug(@"new process pid=%d (count=%d)", pid, numChildProcesses);
2253     }
2255     return pid;
2258 - (void)reapChildProcesses:(id)sender
2260     // NOTE: numChildProcesses (currently) only counts the number of Vim
2261     // processes that have been started with executeInLoginShell::.  If other
2262     // processes are spawned this code may need to be adjusted (or
2263     // numChildProcesses needs to be incremented when such a process is
2264     // started).
2265     while (numChildProcesses > 0) {
2266         int status = 0;
2267         int pid = waitpid(-1, &status, WNOHANG);
2268         if (pid <= 0)
2269             break;
2271         ASLogDebug(@"Wait for pid=%d complete", pid);
2272         --numChildProcesses;
2273     }
2276 - (void)processInputQueues:(id)sender
2278     // NOTE: Because we use distributed objects it is quite possible for this
2279     // function to be re-entered.  This can cause all sorts of unexpected
2280     // problems so we guard against it here so that the rest of the code does
2281     // not need to worry about it.
2283     // The processing flag is > 0 if this function is already on the call
2284     // stack; < 0 if this function was also re-entered.
2285     if (processingFlag != 0) {
2286         ASLogDebug(@"BUSY!");
2287         processingFlag = -1;
2288         return;
2289     }
2291     // NOTE: Be _very_ careful that no exceptions can be raised between here
2292     // and the point at which 'processingFlag' is reset.  Otherwise the above
2293     // test could end up always failing and no input queues would ever be
2294     // processed!
2295     processingFlag = 1;
2297     // NOTE: New input may arrive while we're busy processing; we deal with
2298     // this by putting the current queue aside and creating a new input queue
2299     // for future input.
2300     NSDictionary *queues = inputQueues;
2301     inputQueues = [NSMutableDictionary new];
2303     // Pass each input queue on to the vim controller with matching
2304     // identifier (and note that it could be cached).
2305     NSEnumerator *e = [queues keyEnumerator];
2306     NSNumber *key;
2307     while ((key = [e nextObject])) {
2308         unsigned ukey = [key unsignedIntValue];
2309         int i = 0, count = [vimControllers count];
2310         for (i = 0; i < count; ++i) {
2311             MMVimController *vc = [vimControllers objectAtIndex:i];
2312             if (ukey == [vc vimControllerId]) {
2313                 [vc processInputQueue:[queues objectForKey:key]]; // !exceptions
2314                 break;
2315             }
2316         }
2318         if (i < count) continue;
2320         count = [cachedVimControllers count];
2321         for (i = 0; i < count; ++i) {
2322             MMVimController *vc = [cachedVimControllers objectAtIndex:i];
2323             if (ukey == [vc vimControllerId]) {
2324                 [vc processInputQueue:[queues objectForKey:key]]; // !exceptions
2325                 break;
2326             }
2327         }
2329         if (i == count) {
2330             ASLogWarn(@"No Vim controller for identifier=%d", ukey);
2331         }
2332     }
2334     [queues release];
2336     // If new input arrived while we were processing it would have been
2337     // blocked so we have to schedule it to be processed again.
2338     if (processingFlag < 0)
2339         [self performSelectorOnMainThread:@selector(processInputQueues:)
2340                                withObject:nil
2341                             waitUntilDone:NO
2342                                     modes:[NSArray arrayWithObjects:
2343                                            NSDefaultRunLoopMode,
2344                                            NSEventTrackingRunLoopMode, nil]];
2346     processingFlag = 0;
2349 - (void)addVimController:(MMVimController *)vc
2351     ASLogDebug(@"Add Vim controller pid=%d id=%d",
2352             [vc pid], [vc vimControllerId]);
2354     int pid = [vc pid];
2355     NSNumber *pidKey = [NSNumber numberWithInt:pid];
2357     if (preloadPid == pid) {
2358         // This controller was preloaded, so add it to the cache and
2359         // schedule another vim process to be preloaded.
2360         preloadPid = -1;
2361         [vc setIsPreloading:YES];
2362         [cachedVimControllers addObject:vc];
2363         [self scheduleVimControllerPreloadAfterDelay:1];
2364     } else {
2365         [vimControllers addObject:vc];
2367         id args = [pidArguments objectForKey:pidKey];
2368         if (args && [NSNull null] != args)
2369             [vc passArguments:args];
2371         // HACK!  MacVim does not get activated if it is launched from the
2372         // terminal, so we forcibly activate here unless it is an untitled
2373         // window opening.  Untitled windows are treated differently, else
2374         // MacVim would steal the focus if another app was activated while the
2375         // untitled window was loading.
2376         if (!args || args != [NSNull null])
2377             [self activateWhenNextWindowOpens];
2379         if (args)
2380             [pidArguments removeObjectForKey:pidKey];
2381     }
2384 - (NSDictionary *)convertVimControllerArguments:(NSDictionary *)args
2385                                   toCommandLine:(NSArray **)cmdline
2387     // Take all arguments out of 'args' and put them on an array suitable to
2388     // pass as arguments to launchVimProcessWithArguments:.  The untouched
2389     // dictionary items are returned in a new autoreleased dictionary.
2391     if (cmdline)
2392         *cmdline = nil;
2394     NSArray *filenames = [args objectForKey:@"filenames"];
2395     int numFiles = filenames ? [filenames count] : 0;
2396     BOOL openFiles = ![[args objectForKey:@"dontOpen"] boolValue];
2398     if (numFiles <= 0 || !openFiles)
2399         return args;
2401     NSMutableArray *a = [NSMutableArray array];
2402     NSMutableDictionary *d = [[args mutableCopy] autorelease];
2404     // Search for text using "+/text".
2405     NSString *searchText = [args objectForKey:@"searchText"];
2406     if (searchText) {
2407         // TODO: If the search pattern is not found an error is shown when
2408         // starting.  Figure out a way to get rid of this message (The help
2409         // says to use ':silent exe "normal /pat\<CR>"' but this does not
2410         // work.)
2411         [a addObject:[NSString stringWithFormat:@"+/%@", searchText]];
2413         [d removeObjectForKey:@"searchText"];
2414     }
2416     // Position cursor using "+line" or "-c :cal cursor(line,column)".
2417     NSString *lineString = [args objectForKey:@"cursorLine"];
2418     if (lineString && [lineString intValue] > 0) {
2419         NSString *columnString = [args objectForKey:@"cursorColumn"];
2420         if (columnString && [columnString intValue] > 0) {
2421             [a addObject:@"-c"];
2422             [a addObject:[NSString stringWithFormat:@":cal cursor(%@,%@)",
2423                           lineString, columnString]];
2425             [d removeObjectForKey:@"cursorColumn"];
2426         } else {
2427             [a addObject:[NSString stringWithFormat:@"+%@", lineString]];
2428         }
2430         [d removeObjectForKey:@"cursorLine"];
2431     }
2433     // Set selection using normal mode commands.
2434     NSString *rangeString = [args objectForKey:@"selectionRange"];
2435     if (rangeString) {
2436         NSRange r = NSRangeFromString(rangeString);
2437         [a addObject:@"-c"];
2438         if (r.length > 0) {
2439             // Select given range.
2440             [a addObject:[NSString stringWithFormat:@"norm %dGV%dGz.0",
2441                                                 NSMaxRange(r), r.location]];
2442         } else {
2443             // Position cursor on start of range.
2444             [a addObject:[NSString stringWithFormat:@"norm %dGz.0",
2445                                                                 r.location]];
2446         }
2448         [d removeObjectForKey:@"selectionRange"];
2449     }
2451     // Choose file layout using "-[o|O|p]".
2452     int layout = [[args objectForKey:@"layout"] intValue];
2453     switch (layout) {
2454         case MMLayoutHorizontalSplit: [a addObject:@"-o"]; break;
2455         case MMLayoutVerticalSplit:   [a addObject:@"-O"]; break;
2456         case MMLayoutTabs:            [a addObject:@"-p"]; break;
2457     }
2458     [d removeObjectForKey:@"layout"];
2461     // Last of all add the names of all files to open (DO NOT add more args
2462     // after this point).
2463     [a addObjectsFromArray:filenames];
2465     if ([args objectForKey:@"remoteID"]) {
2466         // These files should be edited remotely so keep the filenames on the
2467         // argument list -- they will need to be passed back to Vim when it
2468         // checks in.  Also set the 'dontOpen' flag or the files will be
2469         // opened twice.
2470         [d setObject:[NSNumber numberWithBool:YES] forKey:@"dontOpen"];
2471     } else {
2472         [d removeObjectForKey:@"dontOpen"];
2473         [d removeObjectForKey:@"filenames"];
2474     }
2476     if (cmdline)
2477         *cmdline = a;
2479     return d;
2482 - (NSString *)workingDirectoryForArguments:(NSDictionary *)args
2484     // Find the "filenames" argument and pick the first path that actually
2485     // exists and return it.
2486     // TODO: Return common parent directory in the case of multiple files?
2487     NSFileManager *fm = [NSFileManager defaultManager];
2488     NSArray *filenames = [args objectForKey:@"filenames"];
2489     NSUInteger i, count = [filenames count];
2490     for (i = 0; i < count; ++i) {
2491         BOOL isdir;
2492         NSString *file = [filenames objectAtIndex:i];
2493         if ([fm fileExistsAtPath:file isDirectory:&isdir])
2494             return isdir ? file : [file stringByDeletingLastPathComponent];
2495     }
2497     return nil;
2500 - (NSScreen *)screenContainingPoint:(NSPoint)pt
2502     NSArray *screens = [NSScreen screens];
2503     NSUInteger i, count = [screens count];
2504     for (i = 0; i < count; ++i) {
2505         NSScreen *screen = [screens objectAtIndex:i];
2506         NSRect frame = [screen frame];
2507         if (NSPointInRect(pt, frame))
2508             return screen;
2509     }
2511     return nil;
2514 @end // MMAppController (Private)