Avoid race condition (e.g. when closing windows)
[MacVim.git] / src / MacVim / MMAppController.m
blob2f2abc322be3444da20890c84a20f93f7bd4600e
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_4)
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
72 #pragma options align=mac68k
73 typedef struct
75     short unused1;      // 0 (not used)
76     short lineNum;      // line to select (< 0 to specify range)
77     long  startRange;   // start of selection range (if line < 0)
78     long  endRange;     // end of selection range (if line < 0)
79     long  unused2;      // 0 (not used)
80     long  theDate;      // modification date/time
81 } MMSelectionRange;
82 #pragma options align=reset
85 // This is a private AppKit API gleaned from class-dump.
86 @interface NSKeyBindingManager : NSObject
87 + (id)sharedKeyBindingManager;
88 - (id)dictionary;
89 - (void)setDictionary:(id)arg1;
90 @end
93 @interface MMAppController (MMServices)
94 - (void)openSelection:(NSPasteboard *)pboard userData:(NSString *)userData
95                 error:(NSString **)error;
96 - (void)openFile:(NSPasteboard *)pboard userData:(NSString *)userData
97            error:(NSString **)error;
98 - (void)newFileHere:(NSPasteboard *)pboard userData:(NSString *)userData
99               error:(NSString **)error;
100 @end
103 @interface MMAppController (Private)
104 - (MMVimController *)topmostVimController;
105 - (int)launchVimProcessWithArguments:(NSArray *)args;
106 - (NSArray *)filterFilesAndNotify:(NSArray *)files;
107 - (NSArray *)filterOpenFiles:(NSArray *)filenames
108                openFilesDict:(NSDictionary **)openFiles;
109 #if MM_HANDLE_XCODE_MOD_EVENT
110 - (void)handleXcodeModEvent:(NSAppleEventDescriptor *)event
111                  replyEvent:(NSAppleEventDescriptor *)reply;
112 #endif
113 - (void)handleGetURLEvent:(NSAppleEventDescriptor *)event
114                replyEvent:(NSAppleEventDescriptor *)reply;
115 - (int)findLaunchingProcessWithoutArguments;
116 - (MMVimController *)findUnusedEditor;
117 - (NSMutableDictionary *)extractArgumentsFromOdocEvent:
118     (NSAppleEventDescriptor *)desc;
119 - (void)scheduleVimControllerPreloadAfterDelay:(NSTimeInterval)delay;
120 - (void)cancelVimControllerPreloadRequests;
121 - (void)preloadVimController:(id)sender;
122 - (int)maxPreloadCacheSize;
123 - (MMVimController *)takeVimControllerFromCache;
124 - (void)clearPreloadCacheWithCount:(int)count;
125 - (void)rebuildPreloadCache;
126 - (NSDate *)rcFilesModificationDate;
127 - (BOOL)openVimControllerWithArguments:(NSDictionary *)arguments;
128 - (void)activateWhenNextWindowOpens;
129 - (void)startWatchingVimDir;
130 - (void)stopWatchingVimDir;
131 - (void)handleFSEvent;
132 - (void)loadDefaultFont;
133 - (int)executeInLoginShell:(NSString *)path arguments:(NSArray *)args;
134 - (void)reapChildProcesses:(id)sender;
135 - (void)processInputQueues:(id)sender;
136 - (void)addVimController:(MMVimController *)vc;
138 #ifdef MM_ENABLE_PLUGINS
139 - (void)removePlugInMenu;
140 - (void)addPlugInMenuToMenu:(NSMenu *)mainMenu;
141 #endif
142 @end
146 #if (MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_4)
147     static void
148 fsEventCallback(ConstFSEventStreamRef streamRef,
149                 void *clientCallBackInfo,
150                 size_t numEvents,
151                 void *eventPaths,
152                 const FSEventStreamEventFlags eventFlags[],
153                 const FSEventStreamEventId eventIds[])
155     [[MMAppController sharedInstance] handleFSEvent];
157 #endif
159 @implementation MMAppController
161 + (void)initialize
163     static BOOL initDone = NO;
164     if (initDone) return;
165     initDone = YES;
167     ASLInit();
169     // HACK! The following user default must be reset, else Ctrl-q (or
170     // whichever key is specified by the default) will be blocked by the input
171     // manager (interpretKeyEvents: swallows that key).  (We can't use
172     // NSUserDefaults since it only allows us to write to the registration
173     // domain and this preference has "higher precedence" than that so such a
174     // change would have no effect.)
175     CFPreferencesSetAppValue(CFSTR("NSQuotedKeystrokeBinding"),
176                              CFSTR(""),
177                              kCFPreferencesCurrentApplication);
178     
179     NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:
180         [NSNumber numberWithBool:NO],   MMNoWindowKey,
181         [NSNumber numberWithInt:64],    MMTabMinWidthKey,
182         [NSNumber numberWithInt:6*64],  MMTabMaxWidthKey,
183         [NSNumber numberWithInt:132],   MMTabOptimumWidthKey,
184         [NSNumber numberWithBool:YES],  MMShowAddTabButtonKey,
185         [NSNumber numberWithInt:2],     MMTextInsetLeftKey,
186         [NSNumber numberWithInt:1],     MMTextInsetRightKey,
187         [NSNumber numberWithInt:1],     MMTextInsetTopKey,
188         [NSNumber numberWithInt:1],     MMTextInsetBottomKey,
189         @"MMTypesetter",                MMTypesetterKey,
190         [NSNumber numberWithFloat:1],   MMCellWidthMultiplierKey,
191         [NSNumber numberWithFloat:-1],  MMBaselineOffsetKey,
192         [NSNumber numberWithBool:YES],  MMTranslateCtrlClickKey,
193         [NSNumber numberWithInt:0],     MMOpenInCurrentWindowKey,
194         [NSNumber numberWithBool:NO],   MMNoFontSubstitutionKey,
195         [NSNumber numberWithBool:YES],  MMLoginShellKey,
196         [NSNumber numberWithBool:NO],   MMAtsuiRendererKey,
197         [NSNumber numberWithInt:MMUntitledWindowAlways],
198                                         MMUntitledWindowKey,
199         [NSNumber numberWithBool:NO],   MMTexturedWindowKey,
200         [NSNumber numberWithBool:NO],   MMZoomBothKey,
201         @"",                            MMLoginShellCommandKey,
202         @"",                            MMLoginShellArgumentKey,
203         [NSNumber numberWithBool:YES],  MMDialogsTrackPwdKey,
204 #ifdef MM_ENABLE_PLUGINS
205         [NSNumber numberWithBool:YES],  MMShowLeftPlugInContainerKey,
206 #endif
207         [NSNumber numberWithInt:3],     MMOpenLayoutKey,
208         [NSNumber numberWithBool:NO],   MMVerticalSplitKey,
209         [NSNumber numberWithInt:0],     MMPreloadCacheSizeKey,
210         [NSNumber numberWithInt:0],     MMLastWindowClosedBehaviorKey,
211         [NSNumber numberWithBool:YES],  MMLoadDefaultFontKey,
212 #ifdef INCLUDE_OLD_IM_CODE
213         [NSNumber numberWithBool:YES],  MMUseInlineImKey,
214 #endif // INCLUDE_OLD_IM_CODE
215         nil];
217     [[NSUserDefaults standardUserDefaults] registerDefaults:dict];
219     NSArray *types = [NSArray arrayWithObject:NSStringPboardType];
220     [NSApp registerServicesMenuSendTypes:types returnTypes:types];
222     // NOTE: Set the current directory to user's home directory, otherwise it
223     // will default to the root directory.  (This matters since new Vim
224     // processes inherit MacVim's environment variables.)
225     [[NSFileManager defaultManager] changeCurrentDirectoryPath:
226             NSHomeDirectory()];
229 - (id)init
231     if (!(self = [super init])) return nil;
233     [self loadDefaultFont];
235     vimControllers = [NSMutableArray new];
236     cachedVimControllers = [NSMutableArray new];
237     preloadPid = -1;
238     pidArguments = [NSMutableDictionary new];
239     inputQueues = [NSMutableDictionary new];
241 #ifdef MM_ENABLE_PLUGINS
242     NSString *plugInTitle = NSLocalizedString(@"Plug-In",
243                                               @"Plug-In menu title");
244     plugInMenuItem = [[NSMenuItem alloc] initWithTitle:plugInTitle
245                                                 action:NULL
246                                          keyEquivalent:@""];
247     NSMenu *submenu = [[NSMenu alloc] initWithTitle:plugInTitle];
248     [plugInMenuItem setSubmenu:submenu];
249     [submenu release];
250 #endif
252     // NOTE: Do not use the default connection since the Logitech Control
253     // Center (LCC) input manager steals and this would cause MacVim to
254     // never open any windows.  (This is a bug in LCC but since they are
255     // unlikely to fix it, we graciously give them the default connection.)
256     connection = [[NSConnection alloc] initWithReceivePort:[NSPort port]
257                                                   sendPort:nil];
258     [connection setRootObject:self];
259     [connection setRequestTimeout:MMRequestTimeout];
260     [connection setReplyTimeout:MMReplyTimeout];
262     // NOTE!  If the name of the connection changes here it must also be
263     // updated in MMBackend.m.
264     NSString *name = [NSString stringWithFormat:@"%@-connection",
265              [[NSBundle mainBundle] bundlePath]];
266     if (![connection registerName:name]) {
267         ASLogCrit(@"Failed to register connection with name '%@'", name);
268         [connection release];  connection = nil;
269     }
271     return self;
274 - (void)dealloc
276     ASLogDebug(@"");
278     [connection release];  connection = nil;
279     [inputQueues release];  inputQueues = nil;
280     [pidArguments release];  pidArguments = nil;
281     [vimControllers release];  vimControllers = nil;
282     [cachedVimControllers release];  cachedVimControllers = nil;
283     [openSelectionString release];  openSelectionString = nil;
284     [recentFilesMenuItem release];  recentFilesMenuItem = nil;
285     [defaultMainMenu release];  defaultMainMenu = nil;
286 #ifdef MM_ENABLE_PLUGINS
287     [plugInMenuItem release];  plugInMenuItem = nil;
288 #endif
289     [appMenuItemTemplate release];  appMenuItemTemplate = nil;
291     [super dealloc];
294 - (void)applicationWillFinishLaunching:(NSNotification *)notification
296     // Remember the default menu so that it can be restored if the user closes
297     // all editor windows.
298     defaultMainMenu = [[NSApp mainMenu] retain];
300     // Store a copy of the default app menu so we can use this as a template
301     // for all other menus.  We make a copy here because the "Services" menu
302     // will not yet have been populated at this time.  If we don't we get
303     // problems trying to set key equivalents later on because they might clash
304     // with items on the "Services" menu.
305     appMenuItemTemplate = [defaultMainMenu itemAtIndex:0];
306     appMenuItemTemplate = [appMenuItemTemplate copy];
308     // Set up the "Open Recent" menu. See
309     //   http://lapcatsoftware.com/blog/2007/07/10/
310     //     working-without-a-nib-part-5-open-recent-menu/
311     // and
312     //   http://www.cocoabuilder.com/archive/message/cocoa/2007/8/15/187793
313     // for more information.
314     //
315     // The menu itself is created in MainMenu.nib but we still seem to have to
316     // hack around a bit to get it to work.  (This has to be done in
317     // applicationWillFinishLaunching at the latest, otherwise it doesn't
318     // work.)
319     NSMenu *fileMenu = [defaultMainMenu findFileMenu];
320     if (fileMenu) {
321         int idx = [fileMenu indexOfItemWithAction:@selector(fileOpen:)];
322         if (idx >= 0 && idx+1 < [fileMenu numberOfItems])
324         recentFilesMenuItem = [fileMenu itemWithTitle:@"Open Recent"];
325         [[recentFilesMenuItem submenu] performSelector:@selector(_setMenuName:)
326                                         withObject:@"NSRecentDocumentsMenu"];
328         // Note: The "Recent Files" menu must be moved around since there is no
329         // -[NSApp setRecentFilesMenu:] method.  We keep a reference to it to
330         // facilitate this move (see setMainMenu: below).
331         [recentFilesMenuItem retain];
332     }
334 #if MM_HANDLE_XCODE_MOD_EVENT
335     [[NSAppleEventManager sharedAppleEventManager]
336             setEventHandler:self
337                 andSelector:@selector(handleXcodeModEvent:replyEvent:)
338               forEventClass:'KAHL'
339                  andEventID:'MOD '];
340 #endif
342     // Register 'mvim://' URL handler
343     [[NSAppleEventManager sharedAppleEventManager]
344             setEventHandler:self
345                 andSelector:@selector(handleGetURLEvent:replyEvent:)
346               forEventClass:kInternetEventClass
347                  andEventID:kAEGetURL];
349     // Disable the default Cocoa "Key Bindings" since they interfere with the
350     // way Vim handles keyboard input.  Cocoa reads bindings from
351     //     /System/Library/Frameworks/AppKit.framework/Resources/
352     //                                                  StandardKeyBinding.dict
353     // and
354     //     ~/Library/KeyBindings/DefaultKeyBinding.dict
355     // To avoid having the user accidentally break keyboard handling (by
356     // modifying the latter in some unexpected way) in MacVim we load our own
357     // key binding dictionary from Resource/KeyBinding.plist.  We can't disable
358     // the bindings completely since it would break keyboard handling in
359     // dialogs so the our custom dictionary contains all the entries from the
360     // former location.
361     //
362     // It is possible to disable key bindings completely by not calling
363     // interpretKeyEvents: in keyDown: but this also disables key bindings used
364     // by certain input methods.  E.g.  Ctrl-Shift-; would no longer work in
365     // the Kotoeri input manager.
366     //
367     // To solve this problem we access a private API and set the key binding
368     // dictionary to our own custom dictionary here.  At this time Cocoa will
369     // have already read the above mentioned dictionaries so it (hopefully)
370     // won't try to change the key binding dictionary again after this point.
371     NSKeyBindingManager *mgr = [NSKeyBindingManager sharedKeyBindingManager];
372     NSBundle *mainBundle = [NSBundle mainBundle];
373     NSString *path = [mainBundle pathForResource:@"KeyBinding"
374                                           ofType:@"plist"];
375     NSDictionary *dict = [NSDictionary dictionaryWithContentsOfFile:path];
376     if (mgr && dict) {
377         [mgr setDictionary:dict];
378     } else {
379         ASLogNotice(@"Failed to override the Cocoa key bindings.  Keyboard "
380                 "input may behave strangely as a result (path=%@).", path);
381     }
384 - (void)applicationDidFinishLaunching:(NSNotification *)notification
386     [NSApp setServicesProvider:self];
387 #ifdef MM_ENABLE_PLUGINS
388     [[MMPlugInManager sharedManager] loadAllPlugIns];
389 #endif
391     if ([self maxPreloadCacheSize] > 0) {
392         [self scheduleVimControllerPreloadAfterDelay:2];
393         [self startWatchingVimDir];
394     }
396     ASLogInfo(@"MacVim finished launching");
399 - (BOOL)applicationShouldOpenUntitledFile:(NSApplication *)sender
401     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
402     NSAppleEventManager *aem = [NSAppleEventManager sharedAppleEventManager];
403     NSAppleEventDescriptor *desc = [aem currentAppleEvent];
405     // The user default MMUntitledWindow can be set to control whether an
406     // untitled window should open on 'Open' and 'Reopen' events.
407     int untitledWindowFlag = [ud integerForKey:MMUntitledWindowKey];
409     BOOL isAppOpenEvent = [desc eventID] == kAEOpenApplication;
410     if (isAppOpenEvent && (untitledWindowFlag & MMUntitledWindowOnOpen) == 0)
411         return NO;
413     BOOL isAppReopenEvent = [desc eventID] == kAEReopenApplication;
414     if (isAppReopenEvent
415             && (untitledWindowFlag & MMUntitledWindowOnReopen) == 0)
416         return NO;
418     // When a process is started from the command line, the 'Open' event may
419     // contain a parameter to surpress the opening of an untitled window.
420     desc = [desc paramDescriptorForKeyword:keyAEPropData];
421     desc = [desc paramDescriptorForKeyword:keyMMUntitledWindow];
422     if (desc && ![desc booleanValue])
423         return NO;
425     // Never open an untitled window if there is at least one open window or if
426     // there are processes that are currently launching.
427     if ([vimControllers count] > 0 || [pidArguments count] > 0)
428         return NO;
430     // NOTE!  This way it possible to start the app with the command-line
431     // argument '-nowindow yes' and no window will be opened by default but
432     // this argument will only be heeded when the application is opening.
433     if (isAppOpenEvent && [ud boolForKey:MMNoWindowKey] == YES)
434         return NO;
436     return YES;
439 - (BOOL)applicationOpenUntitledFile:(NSApplication *)sender
441     ASLogDebug(@"Opening untitled window...");
442     [self newWindow:self];
443     return YES;
446 - (void)application:(NSApplication *)sender openFiles:(NSArray *)filenames
448     ASLogInfo(@"Opening files %@", filenames);
450     // Extract ODB/Xcode/Spotlight parameters from the current Apple event,
451     // sort the filenames, and then let openFiles:withArguments: do the heavy
452     // lifting.
454     if (!(filenames && [filenames count] > 0))
455         return;
457     // Sort filenames since the Finder doesn't take care in preserving the
458     // order in which files are selected anyway (and "sorted" is more
459     // predictable than "random").
460     if ([filenames count] > 1)
461         filenames = [filenames sortedArrayUsingSelector:
462                 @selector(localizedCompare:)];
464     // Extract ODB/Xcode/Spotlight parameters from the current Apple event
465     NSMutableDictionary *arguments = [self extractArgumentsFromOdocEvent:
466             [[NSAppleEventManager sharedAppleEventManager] currentAppleEvent]];
468     if ([self openFiles:filenames withArguments:arguments]) {
469         [NSApp replyToOpenOrPrint:NSApplicationDelegateReplySuccess];
470     } else {
471         // TODO: Notify user of failure?
472         [NSApp replyToOpenOrPrint:NSApplicationDelegateReplyFailure];
473     }
476 - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender
478     return (MMTerminateWhenLastWindowClosed ==
479             [[NSUserDefaults standardUserDefaults]
480                 integerForKey:MMLastWindowClosedBehaviorKey]);
483 - (NSApplicationTerminateReply)applicationShouldTerminate:
484     (NSApplication *)sender
486     // TODO: Follow Apple's guidelines for 'Graceful Application Termination'
487     // (in particular, allow user to review changes and save).
488     int reply = NSTerminateNow;
489     BOOL modifiedBuffers = NO;
491     // Go through windows, checking for modified buffers.  (Each Vim process
492     // tells MacVim when any buffer has been modified and MacVim sets the
493     // 'documentEdited' flag of the window correspondingly.)
494     NSEnumerator *e = [[NSApp windows] objectEnumerator];
495     id window;
496     while ((window = [e nextObject])) {
497         if ([window isDocumentEdited]) {
498             modifiedBuffers = YES;
499             break;
500         }
501     }
503     if (modifiedBuffers) {
504         NSAlert *alert = [[NSAlert alloc] init];
505         [alert setAlertStyle:NSWarningAlertStyle];
506         [alert addButtonWithTitle:NSLocalizedString(@"Quit",
507                 @"Dialog button")];
508         [alert addButtonWithTitle:NSLocalizedString(@"Cancel",
509                 @"Dialog button")];
510         [alert setMessageText:NSLocalizedString(@"Quit without saving?",
511                 @"Quit dialog with changed buffers, title")];
512         [alert setInformativeText:NSLocalizedString(
513                 @"There are modified buffers, "
514                 "if you quit now all changes will be lost.  Quit anyway?",
515                 @"Quit dialog with changed buffers, text")];
517         if ([alert runModal] != NSAlertFirstButtonReturn)
518             reply = NSTerminateCancel;
520         [alert release];
521     } else {
522         // No unmodified buffers, but give a warning if there are multiple
523         // windows and/or tabs open.
524         int numWindows = [vimControllers count];
525         int numTabs = 0;
527         // Count the number of open tabs
528         e = [vimControllers objectEnumerator];
529         id vc;
530         while ((vc = [e nextObject]))
531             numTabs += [[vc objectForVimStateKey:@"numTabs"] intValue];
533         if (numWindows > 1 || numTabs > 1) {
534             NSAlert *alert = [[NSAlert alloc] init];
535             [alert setAlertStyle:NSWarningAlertStyle];
536             [alert addButtonWithTitle:NSLocalizedString(@"Quit",
537                     @"Dialog button")];
538             [alert addButtonWithTitle:NSLocalizedString(@"Cancel",
539                     @"Dialog button")];
540             [alert setMessageText:NSLocalizedString(
541                     @"Are you sure you want to quit MacVim?",
542                     @"Quit dialog with no changed buffers, title")];
544             NSString *info = nil;
545             if (numWindows > 1) {
546                 if (numTabs > numWindows)
547                     info = [NSString stringWithFormat:NSLocalizedString(
548                             @"There are %d windows open in MacVim, with a "
549                             "total of %d tabs. Do you want to quit anyway?",
550                             @"Quit dialog with no changed buffers, text"),
551                          numWindows, numTabs];
552                 else
553                     info = [NSString stringWithFormat:NSLocalizedString(
554                             @"There are %d windows open in MacVim. "
555                             "Do you want to quit anyway?",
556                             @"Quit dialog with no changed buffers, text"),
557                         numWindows];
559             } else {
560                 info = [NSString stringWithFormat:NSLocalizedString(
561                         @"There are %d tabs open in MacVim. "
562                         "Do you want to quit anyway?",
563                         @"Quit dialog with no changed buffers, text"), 
564                      numTabs];
565             }
567             [alert setInformativeText:info];
569             if ([alert runModal] != NSAlertFirstButtonReturn)
570                 reply = NSTerminateCancel;
572             [alert release];
573         }
574     }
577     // Tell all Vim processes to terminate now (otherwise they'll leave swap
578     // files behind).
579     if (NSTerminateNow == reply) {
580         e = [vimControllers objectEnumerator];
581         id vc;
582         while ((vc = [e nextObject])) {
583             ASLogDebug(@"Terminate pid=%d", [vc pid]);
584             [vc sendMessage:TerminateNowMsgID data:nil];
585         }
587         e = [cachedVimControllers objectEnumerator];
588         while ((vc = [e nextObject])) {
589             ASLogDebug(@"Terminate pid=%d (cached)", [vc pid]);
590             [vc sendMessage:TerminateNowMsgID data:nil];
591         }
593         // If a Vim process is being preloaded as we quit we have to forcibly
594         // kill it since we have not established a connection yet.
595         if (preloadPid > 0) {
596             ASLogDebug(@"Kill incomplete preloaded process pid=%d", preloadPid);
597             kill(preloadPid, SIGKILL);
598         }
600         // If a Vim process was loading as we quit we also have to kill it.
601         e = [[pidArguments allKeys] objectEnumerator];
602         NSNumber *pidKey;
603         while ((pidKey = [e nextObject])) {
604             ASLogDebug(@"Kill incomplete process pid=%d", [pidKey intValue]);
605             kill([pidKey intValue], SIGKILL);
606         }
608         // Sleep a little to allow all the Vim processes to exit.
609         usleep(10000);
610     }
612     return reply;
615 - (void)applicationWillTerminate:(NSNotification *)notification
617     ASLogInfo(@"Terminating MacVim...");
619     [self stopWatchingVimDir];
621 #ifdef MM_ENABLE_PLUGINS
622     [[MMPlugInManager sharedManager] unloadAllPlugIns];
623 #endif
625 #if MM_HANDLE_XCODE_MOD_EVENT
626     [[NSAppleEventManager sharedAppleEventManager]
627             removeEventHandlerForEventClass:'KAHL'
628                                  andEventID:'MOD '];
629 #endif
631     // This will invalidate all connections (since they were spawned from this
632     // connection).
633     [connection invalidate];
635     // Deactivate the font we loaded from the app bundle.
636     // NOTE: This can take quite a while (~500 ms), so termination will be
637     // noticeably faster if loading of the default font is disabled.
638     if (fontContainerRef) {
639         ATSFontDeactivate(fontContainerRef, NULL, kATSOptionFlagsDefault);
640         fontContainerRef = 0;
641     }
643     [NSApp setDelegate:nil];
645     // Try to wait for all child processes to avoid leaving zombies behind (but
646     // don't wait around for too long).
647     NSDate *timeOutDate = [NSDate dateWithTimeIntervalSinceNow:2];
648     while ([timeOutDate timeIntervalSinceNow] > 0) {
649         [self reapChildProcesses:nil];
650         if (numChildProcesses <= 0)
651             break;
653         ASLogDebug(@"%d processes still left, hold on...", numChildProcesses);
655         // Run in NSConnectionReplyMode while waiting instead of calling e.g.
656         // usleep().  Otherwise incoming messages may clog up the DO queues and
657         // the outgoing TerminateNowMsgID sent earlier never reaches the Vim
658         // process.
659         // This has at least one side-effect, namely we may receive the
660         // annoying "dropping incoming DO message".  (E.g. this may happen if
661         // you quickly hit Cmd-n several times in a row and then immediately
662         // press Cmd-q, Enter.)
663         while (CFRunLoopRunInMode((CFStringRef)NSConnectionReplyMode,
664                 0.05, true) == kCFRunLoopRunHandledSource)
665             ;   // do nothing
666     }
668     if (numChildProcesses > 0) {
669         ASLogNotice(@"%d zombies left behind", numChildProcesses);
670     }
673 + (MMAppController *)sharedInstance
675     // Note: The app controller is a singleton which is instantiated in
676     // MainMenu.nib where it is also connected as the delegate of NSApp.
677     id delegate = [NSApp delegate];
678     return [delegate isKindOfClass:self] ? (MMAppController*)delegate : nil;
681 - (NSMenu *)defaultMainMenu
683     return defaultMainMenu;
686 - (NSMenuItem *)appMenuItemTemplate
688     return appMenuItemTemplate;
691 - (void)removeVimController:(id)controller
693     ASLogDebug(@"Remove Vim controller pid=%d id=%d (processingFlag=%d)",
694                [controller pid], [controller identifier], processingFlag);
696     int idx = [vimControllers indexOfObject:controller];
697     if (NSNotFound == idx) {
698         ASLogDebug(@"Controller not found, probably due to duplicate removal");
699         return;
700     }
702     [controller retain];
703     [vimControllers removeObjectAtIndex:idx];
704     [controller cleanup];
705     [controller release];
707     if (![vimControllers count]) {
708         // The last editor window just closed so restore the main menu back to
709         // its default state (which is defined in MainMenu.nib).
710         [self setMainMenu:defaultMainMenu];
712         BOOL hide = (MMHideWhenLastWindowClosed ==
713                     [[NSUserDefaults standardUserDefaults]
714                         integerForKey:MMLastWindowClosedBehaviorKey]);
715         if (hide)
716             [NSApp hide:self];
717     }
719     // There is a small delay before the Vim process actually exits so wait a
720     // little before trying to reap the child process.  If the process still
721     // hasn't exited after this wait it won't be reaped until the next time
722     // reapChildProcesses: is called (but this should be harmless).
723     [self performSelector:@selector(reapChildProcesses:)
724                withObject:nil
725                afterDelay:0.1];
728 - (void)windowControllerWillOpen:(MMWindowController *)windowController
730     NSPoint topLeft = NSZeroPoint;
731     NSWindow *topWin = [[[self topmostVimController] windowController] window];
732     NSWindow *win = [windowController window];
734     if (!win) return;
736     // If there is a window belonging to a Vim process, cascade from it,
737     // otherwise use the autosaved window position (if any).
738     if (topWin) {
739         NSRect frame = [topWin frame];
740         topLeft = NSMakePoint(frame.origin.x, NSMaxY(frame));
741     } else {
742         NSString *topLeftString = [[NSUserDefaults standardUserDefaults]
743             stringForKey:MMTopLeftPointKey];
744         if (topLeftString)
745             topLeft = NSPointFromString(topLeftString);
746     }
748     if (!NSEqualPoints(topLeft, NSZeroPoint)) {
749         NSPoint oldTopLeft = topLeft;
750         if (topWin)
751             topLeft = [win cascadeTopLeftFromPoint:topLeft];
753         [win setFrameTopLeftPoint:topLeft];
755         if ([win screen]) {
756             NSPoint screenOrigin = [[win screen] frame].origin;
757             if ([win frame].origin.y < screenOrigin.y) {
758                 // Try to avoid shifting the new window downwards if it means
759                 // that the bottom of the window will be off the screen.  E.g.
760                 // if the user has set windows to open maximized in the
761                 // vertical direction then the new window will cascade
762                 // horizontally only.
763                 topLeft.y = oldTopLeft.y;
764                 [win setFrameTopLeftPoint:topLeft];
765             }
767             if ([win frame].origin.y < screenOrigin.y) {
768                 // Move the window to the top of the screen if the bottom of
769                 // the window is still obscured.
770                 topLeft.y = NSMaxY([[win screen] frame]);
771                 [win setFrameTopLeftPoint:topLeft];
772             }
773         } else {
774             ASLogNotice(@"Window not on screen, don't constrain position");
775         }
776     }
778     if (1 == [vimControllers count]) {
779         // The first window autosaves its position.  (The autosaving
780         // features of Cocoa are not used because we need more control over
781         // what is autosaved and when it is restored.)
782         [windowController setWindowAutosaveKey:MMTopLeftPointKey];
783     }
785     if (openSelectionString) {
786         // TODO: Pass this as a parameter instead!  Get rid of
787         // 'openSelectionString' etc.
788         //
789         // There is some text to paste into this window as a result of the
790         // services menu "Open selection ..." being used.
791         [[windowController vimController] dropString:openSelectionString];
792         [openSelectionString release];
793         openSelectionString = nil;
794     }
796     if (shouldActivateWhenNextWindowOpens) {
797         [NSApp activateIgnoringOtherApps:YES];
798         shouldActivateWhenNextWindowOpens = NO;
799     }
802 - (void)setMainMenu:(NSMenu *)mainMenu
804     if ([NSApp mainMenu] == mainMenu) return;
806     // If the new menu has a "Recent Files" dummy item, then swap the real item
807     // for the dummy.  We are forced to do this since Cocoa initializes the
808     // "Recent Files" menu and there is no way to simply point Cocoa to a new
809     // item each time the menus are swapped.
810     NSMenu *fileMenu = [mainMenu findFileMenu];
811     if (recentFilesMenuItem && fileMenu) {
812         int dummyIdx =
813                 [fileMenu indexOfItemWithAction:@selector(recentFilesDummy:)];
814         if (dummyIdx >= 0) {
815             NSMenuItem *dummyItem = [[fileMenu itemAtIndex:dummyIdx] retain];
816             [fileMenu removeItemAtIndex:dummyIdx];
818             NSMenu *recentFilesParentMenu = [recentFilesMenuItem menu];
819             int idx = [recentFilesParentMenu indexOfItem:recentFilesMenuItem];
820             if (idx >= 0) {
821                 [[recentFilesMenuItem retain] autorelease];
822                 [recentFilesParentMenu removeItemAtIndex:idx];
823                 [recentFilesParentMenu insertItem:dummyItem atIndex:idx];
824             }
826             [fileMenu insertItem:recentFilesMenuItem atIndex:dummyIdx];
827             [dummyItem release];
828         }
829     }
831     // Now set the new menu.  Notice that we keep one menu for each editor
832     // window since each editor can have its own set of menus.  When swapping
833     // menus we have to tell Cocoa where the new "MacVim", "Windows", and
834     // "Services" menu are.
835     [NSApp setMainMenu:mainMenu];
837     // Setting the "MacVim" (or "Application") menu ensures that it is typeset
838     // in boldface.  (The setAppleMenu: method used to be public but is now
839     // private so this will have to be considered a bit of a hack!)
840     NSMenu *appMenu = [mainMenu findApplicationMenu];
841     [NSApp performSelector:@selector(setAppleMenu:) withObject:appMenu];
843     NSMenu *servicesMenu = [mainMenu findServicesMenu];
844     [NSApp setServicesMenu:servicesMenu];
846     NSMenu *windowsMenu = [mainMenu findWindowsMenu];
847     if (windowsMenu) {
848         // Cocoa isn't clever enough to get rid of items it has added to the
849         // "Windows" menu so we have to do it ourselves otherwise there will be
850         // multiple menu items for each window in the "Windows" menu.
851         //   This code assumes that the only items Cocoa add are ones which
852         // send off the action makeKeyAndOrderFront:.  (Cocoa will not add
853         // another separator item if the last item on the "Windows" menu
854         // already is a separator, so we needen't worry about separators.)
855         int i, count = [windowsMenu numberOfItems];
856         for (i = count-1; i >= 0; --i) {
857             NSMenuItem *item = [windowsMenu itemAtIndex:i];
858             if ([item action] == @selector(makeKeyAndOrderFront:))
859                 [windowsMenu removeItem:item];
860         }
861     }
862     [NSApp setWindowsMenu:windowsMenu];
864 #ifdef MM_ENABLE_PLUGINS
865     // Move plugin menu from old to new main menu.
866     [self removePlugInMenu];
867     [self addPlugInMenuToMenu:mainMenu];
868 #endif
871 - (NSArray *)filterOpenFiles:(NSArray *)filenames
873     return [self filterOpenFiles:filenames openFilesDict:nil];
876 - (BOOL)openFiles:(NSArray *)filenames withArguments:(NSDictionary *)args
878     // Opening files works like this:
879     //  a) filter out any already open files
880     //  b) open any remaining files
881     //
882     // A file is opened in an untitled window if there is one (it may be
883     // currently launching, or it may already be visible), otherwise a new
884     // window is opened.
885     //
886     // Each launching Vim process has a dictionary of arguments that are passed
887     // to the process when in checks in (via connectBackend:pid:).  The
888     // arguments for each launching process can be looked up by its PID (in the
889     // pidArguments dictionary).
891     NSMutableDictionary *arguments = (args ? [[args mutableCopy] autorelease]
892                                            : [NSMutableDictionary dictionary]);
894     filenames = normalizeFilenames(filenames);
896     //
897     // a) Filter out any already open files
898     //
899     NSString *firstFile = [filenames objectAtIndex:0];
900     MMVimController *firstController = nil;
901     NSDictionary *openFilesDict = nil;
902     filenames = [self filterOpenFiles:filenames openFilesDict:&openFilesDict];
904     // Pass arguments to vim controllers that had files open.
905     id key;
906     NSEnumerator *e = [openFilesDict keyEnumerator];
908     // (Indicate that we do not wish to open any files at the moment.)
909     [arguments setObject:[NSNumber numberWithBool:YES] forKey:@"dontOpen"];
911     while ((key = [e nextObject])) {
912         NSArray *files = [openFilesDict objectForKey:key];
913         [arguments setObject:files forKey:@"filenames"];
915         MMVimController *vc = [key pointerValue];
916         [vc passArguments:arguments];
918         // If this controller holds the first file, then remember it for later.
919         if ([files containsObject:firstFile])
920             firstController = vc;
921     }
923     // The meaning of "layout" is defined by the WIN_* defines in main.c.
924     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
925     int layout = [ud integerForKey:MMOpenLayoutKey];
926     BOOL splitVert = [ud boolForKey:MMVerticalSplitKey];
927     BOOL openInCurrentWindow = [ud boolForKey:MMOpenInCurrentWindowKey];
929     if (splitVert && MMLayoutHorizontalSplit == layout)
930         layout = MMLayoutVerticalSplit;
931     if (layout < 0 || (layout > MMLayoutTabs && openInCurrentWindow))
932         layout = MMLayoutTabs;
934     if ([filenames count] == 0) {
935         // Raise the window containing the first file that was already open,
936         // and make sure that the tab containing that file is selected.  Only
937         // do this when there are no more files to open, otherwise sometimes
938         // the window with 'firstFile' will be raised, other times it might be
939         // the window that will open with the files in the 'filenames' array.
940         firstFile = [firstFile stringByEscapingSpecialFilenameCharacters];
942         NSString *bufCmd = @"tab sb";
943         switch (layout) {
944             case MMLayoutHorizontalSplit: bufCmd = @"sb"; break;
945             case MMLayoutVerticalSplit:   bufCmd = @"vert sb"; break;
946             case MMLayoutArglist:         bufCmd = @"b"; break;
947         }
949         NSString *input = [NSString stringWithFormat:@"<C-\\><C-N>"
950                 ":let oldswb=&swb|let &swb=\"useopen,usetab\"|"
951                 "%@ %@|let &swb=oldswb|unl oldswb|"
952                 "cal foreground()<CR>", bufCmd, firstFile];
954         [firstController addVimInput:input];
956         return YES;
957     }
959     // Add filenames to "Recent Files" menu, unless they are being edited
960     // remotely (using ODB).
961     if ([arguments objectForKey:@"remoteID"] == nil) {
962         [[NSDocumentController sharedDocumentController]
963                 noteNewRecentFilePaths:filenames];
964     }
966     //
967     // b) Open any remaining files
968     //
970     [arguments setObject:[NSNumber numberWithInt:layout] forKey:@"layout"];
971     [arguments setObject:filenames forKey:@"filenames"];
972     // (Indicate that files should be opened from now on.)
973     [arguments setObject:[NSNumber numberWithBool:NO] forKey:@"dontOpen"];
975     MMVimController *vc;
976     if (openInCurrentWindow && (vc = [self topmostVimController])) {
977         // Open files in an already open window.
978         [[[vc windowController] window] makeKeyAndOrderFront:self];
979         [vc passArguments:arguments];
980         return YES;
981     }
983     BOOL openOk = YES;
984     int numFiles = [filenames count];
985     if (MMLayoutWindows == layout && numFiles > 1) {
986         // Open one file at a time in a new window, but don't open too many at
987         // once (at most cap+1 windows will open).  If the user has increased
988         // the preload cache size we'll take that as a hint that more windows
989         // should be able to open at once.
990         int cap = [self maxPreloadCacheSize] - 1;
991         if (cap < 4) cap = 4;
992         if (cap > numFiles) cap = numFiles;
994         int i;
995         for (i = 0; i < cap; ++i) {
996             NSArray *a = [NSArray arrayWithObject:[filenames objectAtIndex:i]];
997             [arguments setObject:a forKey:@"filenames"];
999             // NOTE: We have to copy the args since we'll mutate them in the
1000             // next loop and the below call may retain the arguments while
1001             // waiting for a process to start.
1002             NSDictionary *args = [[arguments copy] autorelease];
1004             openOk = [self openVimControllerWithArguments:args];
1005             if (!openOk) break;
1006         }
1008         // Open remaining files in tabs in a new window.
1009         if (openOk && numFiles > cap) {
1010             NSRange range = { i, numFiles-cap };
1011             NSArray *a = [filenames subarrayWithRange:range];
1012             [arguments setObject:a forKey:@"filenames"];
1013             [arguments setObject:[NSNumber numberWithInt:MMLayoutTabs]
1014                           forKey:@"layout"];
1016             openOk = [self openVimControllerWithArguments:arguments];
1017         }
1018     } else {
1019         // Open all files at once.
1020         openOk = [self openVimControllerWithArguments:arguments];
1021     }
1023     return openOk;
1026 #ifdef MM_ENABLE_PLUGINS
1027 - (void)addItemToPlugInMenu:(NSMenuItem *)item
1029     NSMenu *menu = [plugInMenuItem submenu];
1030     [menu addItem:item];
1031     if ([menu numberOfItems] == 1)
1032         [self addPlugInMenuToMenu:[NSApp mainMenu]];
1035 - (void)removeItemFromPlugInMenu:(NSMenuItem *)item
1037     NSMenu *menu = [plugInMenuItem submenu];
1038     [menu removeItem:item];
1039     if ([menu numberOfItems] == 0)
1040         [self removePlugInMenu];
1042 #endif
1044 - (IBAction)newWindow:(id)sender
1046     ASLogDebug(@"Open new window");
1048     // A cached controller requires no loading times and results in the new
1049     // window popping up instantaneously.  If the cache is empty it may take
1050     // 1-2 seconds to start a new Vim process.
1051     MMVimController *vc = [self takeVimControllerFromCache];
1052     if (vc) {
1053         [[vc backendProxy] acknowledgeConnection];
1054     } else {
1055         [self launchVimProcessWithArguments:nil];
1056     }
1059 - (IBAction)newWindowAndActivate:(id)sender
1061     [self activateWhenNextWindowOpens];
1062     [self newWindow:sender];
1065 - (IBAction)fileOpen:(id)sender
1067     ASLogDebug(@"Show file open panel");
1069     NSString *dir = nil;
1070     BOOL trackPwd = [[NSUserDefaults standardUserDefaults]
1071             boolForKey:MMDialogsTrackPwdKey];
1072     if (trackPwd) {
1073         MMVimController *vc = [self keyVimController];
1074         if (vc) dir = [vc objectForVimStateKey:@"pwd"];
1075     }
1077     NSOpenPanel *panel = [NSOpenPanel openPanel];
1078     [panel setAllowsMultipleSelection:YES];
1079     [panel setAccessoryView:showHiddenFilesView()];
1081     int result = [panel runModalForDirectory:dir file:nil types:nil];
1082     if (NSOKButton == result)
1083         [self application:NSApp openFiles:[panel filenames]];
1086 - (IBAction)selectNextWindow:(id)sender
1088     ASLogDebug(@"Select next window");
1090     unsigned i, count = [vimControllers count];
1091     if (!count) return;
1093     NSWindow *keyWindow = [NSApp keyWindow];
1094     for (i = 0; i < count; ++i) {
1095         MMVimController *vc = [vimControllers objectAtIndex:i];
1096         if ([[[vc windowController] window] isEqual:keyWindow])
1097             break;
1098     }
1100     if (i < count) {
1101         if (++i >= count)
1102             i = 0;
1103         MMVimController *vc = [vimControllers objectAtIndex:i];
1104         [[vc windowController] showWindow:self];
1105     }
1108 - (IBAction)selectPreviousWindow:(id)sender
1110     ASLogDebug(@"Select previous window");
1112     unsigned i, count = [vimControllers count];
1113     if (!count) return;
1115     NSWindow *keyWindow = [NSApp keyWindow];
1116     for (i = 0; i < count; ++i) {
1117         MMVimController *vc = [vimControllers objectAtIndex:i];
1118         if ([[[vc windowController] window] isEqual:keyWindow])
1119             break;
1120     }
1122     if (i < count) {
1123         if (i > 0) {
1124             --i;
1125         } else {
1126             i = count - 1;
1127         }
1128         MMVimController *vc = [vimControllers objectAtIndex:i];
1129         [[vc windowController] showWindow:self];
1130     }
1133 - (IBAction)orderFrontPreferencePanel:(id)sender
1135     ASLogDebug(@"Show preferences panel");
1136     [[MMPreferenceController sharedPrefsWindowController] showWindow:self];
1139 - (IBAction)openWebsite:(id)sender
1141     ASLogDebug(@"Open MacVim website");
1142     [[NSWorkspace sharedWorkspace] openURL:
1143             [NSURL URLWithString:MMWebsiteString]];
1146 - (IBAction)showVimHelp:(id)sender
1148     ASLogDebug(@"Open window with Vim help");
1149     // Open a new window with the help window maximized.
1150     [self launchVimProcessWithArguments:[NSArray arrayWithObjects:
1151             @"-c", @":h gui_mac", @"-c", @":res", nil]];
1154 - (IBAction)zoomAll:(id)sender
1156     ASLogDebug(@"Zoom all windows");
1157     [NSApp makeWindowsPerform:@selector(performZoom:) inOrder:YES];
1160 - (IBAction)atsuiButtonClicked:(id)sender
1162     ASLogDebug(@"Toggle ATSUI renderer");
1163     // This action is called when the user clicks the "use ATSUI renderer"
1164     // button in the advanced preferences pane.
1165     [self rebuildPreloadCache];
1168 - (IBAction)loginShellButtonClicked:(id)sender
1170     ASLogDebug(@"Toggle login shell option");
1171     // This action is called when the user clicks the "use login shell" button
1172     // in the advanced preferences pane.
1173     [self rebuildPreloadCache];
1176 - (IBAction)quickstartButtonClicked:(id)sender
1178     ASLogDebug(@"Toggle Quickstart option");
1179     if ([self maxPreloadCacheSize] > 0) {
1180         [self scheduleVimControllerPreloadAfterDelay:1.0];
1181         [self startWatchingVimDir];
1182     } else {
1183         [self cancelVimControllerPreloadRequests];
1184         [self clearPreloadCacheWithCount:-1];
1185         [self stopWatchingVimDir];
1186     }
1189 - (MMVimController *)keyVimController
1191     NSWindow *keyWindow = [NSApp keyWindow];
1192     if (keyWindow) {
1193         unsigned i, count = [vimControllers count];
1194         for (i = 0; i < count; ++i) {
1195             MMVimController *vc = [vimControllers objectAtIndex:i];
1196             if ([[[vc windowController] window] isEqual:keyWindow])
1197                 return vc;
1198         }
1199     }
1201     return nil;
1204 - (unsigned)connectBackend:(byref in id <MMBackendProtocol>)proxy pid:(int)pid
1206     ASLogDebug(@"pid=%d", pid);
1208     [(NSDistantObject*)proxy setProtocolForProxy:@protocol(MMBackendProtocol)];
1210     // NOTE: Allocate the vim controller now but don't add it to the list of
1211     // controllers since this is a distributed object call and as such can
1212     // arrive at unpredictable times (e.g. while iterating the list of vim
1213     // controllers).
1214     // (What if input arrives before the vim controller is added to the list of
1215     // controllers?  This should not be a problem since the input isn't
1216     // processed immediately (see processInput:forIdentifier:).)
1217     // Also, since the app may be multithreaded (e.g. as a result of showing
1218     // the open panel) we have to ensure this call happens on the main thread,
1219     // else there is a race condition that may lead to a crash.
1220     MMVimController *vc = [[MMVimController alloc] initWithBackend:proxy
1221                                                                pid:pid];
1222     [self performSelectorOnMainThread:@selector(addVimController:)
1223                            withObject:vc
1224                         waitUntilDone:NO
1225                                 modes:[NSArray arrayWithObject:
1226                                        NSDefaultRunLoopMode]];
1228     [vc release];
1230     return [vc identifier];
1233 - (oneway void)processInput:(in bycopy NSArray *)queue
1234               forIdentifier:(unsigned)identifier
1236     // NOTE: Input is not handled immediately since this is a distributed
1237     // object call and as such can arrive at unpredictable times.  Instead,
1238     // queue the input and process it when the run loop is updated.
1240     if (!(queue && identifier)) {
1241         ASLogWarn(@"Bad input for identifier=%d", identifier);
1242         return;
1243     }
1245     ASLogDebug(@"QUEUE for identifier=%d: <<< %@>>>", identifier,
1246                debugStringForMessageQueue(queue));
1248     NSNumber *key = [NSNumber numberWithUnsignedInt:identifier];
1249     NSArray *q = [inputQueues objectForKey:key];
1250     if (q) {
1251         q = [q arrayByAddingObjectsFromArray:queue];
1252         [inputQueues setObject:q forKey:key];
1253     } else {
1254         [inputQueues setObject:queue forKey:key];
1255     }
1257     // NOTE: We must use "event tracking mode" as well as "default mode",
1258     // otherwise the input queue will not be processed e.g. during live
1259     // resizing.
1260     // Also, since the app may be multithreaded (e.g. as a result of showing
1261     // the open panel) we have to ensure this call happens on the main thread,
1262     // else there is a race condition that may lead to a crash.
1263     [self performSelectorOnMainThread:@selector(processInputQueues:)
1264                            withObject:nil
1265                         waitUntilDone:NO
1266                                 modes:[NSArray arrayWithObjects:
1267                                        NSDefaultRunLoopMode,
1268                                        NSEventTrackingRunLoopMode, nil]];
1271 - (NSArray *)serverList
1273     NSMutableArray *array = [NSMutableArray array];
1275     unsigned i, count = [vimControllers count];
1276     for (i = 0; i < count; ++i) {
1277         MMVimController *controller = [vimControllers objectAtIndex:i];
1278         if ([controller serverName])
1279             [array addObject:[controller serverName]];
1280     }
1282     return array;
1285 @end // MMAppController
1290 @implementation MMAppController (MMServices)
1292 - (void)openSelection:(NSPasteboard *)pboard userData:(NSString *)userData
1293                 error:(NSString **)error
1295     if (![[pboard types] containsObject:NSStringPboardType]) {
1296         ASLogNotice(@"Pasteboard contains no NSStringPboardType");
1297         return;
1298     }
1300     ASLogInfo(@"Open new window containing current selection");
1302     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
1303     BOOL openInCurrentWindow = [ud boolForKey:MMOpenInCurrentWindowKey];
1304     MMVimController *vc;
1306     if (openInCurrentWindow && (vc = [self topmostVimController])) {
1307         [vc sendMessage:AddNewTabMsgID data:nil];
1308         [vc dropString:[pboard stringForType:NSStringPboardType]];
1309     } else {
1310         // Save the text, open a new window, and paste the text when the next
1311         // window opens.  (If this is called several times in a row, then all
1312         // but the last call may be ignored.)
1313         if (openSelectionString) [openSelectionString release];
1314         openSelectionString = [[pboard stringForType:NSStringPboardType] copy];
1316         [self newWindow:self];
1317     }
1320 - (void)openFile:(NSPasteboard *)pboard userData:(NSString *)userData
1321            error:(NSString **)error
1323     if (![[pboard types] containsObject:NSStringPboardType]) {
1324         ASLogNotice(@"Pasteboard contains no NSStringPboardType");
1325         return;
1326     }
1328     // TODO: Parse multiple filenames and create array with names.
1329     NSString *string = [pboard stringForType:NSStringPboardType];
1330     string = [string stringByTrimmingCharactersInSet:
1331             [NSCharacterSet whitespaceAndNewlineCharacterSet]];
1332     string = [string stringByStandardizingPath];
1334     ASLogInfo(@"Open new window with selected file: %@", string);
1336     NSArray *filenames = [self filterFilesAndNotify:
1337             [NSArray arrayWithObject:string]];
1338     if ([filenames count] == 0)
1339         return;
1341     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
1342     BOOL openInCurrentWindow = [ud boolForKey:MMOpenInCurrentWindowKey];
1343     MMVimController *vc;
1345     if (openInCurrentWindow && (vc = [self topmostVimController])) {
1346         [vc dropFiles:filenames forceOpen:YES];
1347     } else {
1348         [self openFiles:filenames withArguments:nil];
1349     }
1352 - (void)newFileHere:(NSPasteboard *)pboard userData:(NSString *)userData
1353               error:(NSString **)error
1355     if (![[pboard types] containsObject:NSStringPboardType]) {
1356         ASLogNotice(@"Pasteboard contains no NSStringPboardType");
1357         return;
1358     }
1360     NSString *path = [pboard stringForType:NSStringPboardType];
1362     BOOL dirIndicator;
1363     if (![[NSFileManager defaultManager] fileExistsAtPath:path
1364                                               isDirectory:&dirIndicator]) {
1365         ASLogNotice(@"Invalid path. Cannot open new document at: %@", path);
1366         return;
1367     }
1369     ASLogInfo(@"Open new file at path=%@", path);
1371     if (!dirIndicator)
1372         path = [path stringByDeletingLastPathComponent];
1374     path = [path stringByEscapingSpecialFilenameCharacters];
1376     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
1377     BOOL openInCurrentWindow = [ud boolForKey:MMOpenInCurrentWindowKey];
1378     MMVimController *vc;
1380     if (openInCurrentWindow && (vc = [self topmostVimController])) {
1381         NSString *input = [NSString stringWithFormat:@"<C-\\><C-N>"
1382                 ":tabe|cd %@<CR>", path];
1383         [vc addVimInput:input];
1384     } else {
1385         NSString *input = [NSString stringWithFormat:@":cd %@", path];
1386         [self launchVimProcessWithArguments:[NSArray arrayWithObjects:
1387                                              @"-c", input, nil]];
1388     }
1391 @end // MMAppController (MMServices)
1396 @implementation MMAppController (Private)
1398 - (MMVimController *)topmostVimController
1400     // Find the topmost visible window which has an associated vim controller.
1401     NSEnumerator *e = [[NSApp orderedWindows] objectEnumerator];
1402     id window;
1403     while ((window = [e nextObject]) && [window isVisible]) {
1404         unsigned i, count = [vimControllers count];
1405         for (i = 0; i < count; ++i) {
1406             MMVimController *vc = [vimControllers objectAtIndex:i];
1407             if ([[[vc windowController] window] isEqual:window])
1408                 return vc;
1409         }
1410     }
1412     return nil;
1415 - (int)launchVimProcessWithArguments:(NSArray *)args
1417     int pid = -1;
1418     NSString *path = [[NSBundle mainBundle] pathForAuxiliaryExecutable:@"Vim"];
1420     if (!path) {
1421         ASLogCrit(@"Vim executable could not be found inside app bundle!");
1422         return -1;
1423     }
1425     NSArray *taskArgs = [NSArray arrayWithObjects:@"-g", @"-f", nil];
1426     if (args)
1427         taskArgs = [taskArgs arrayByAddingObjectsFromArray:args];
1429     BOOL useLoginShell = [[NSUserDefaults standardUserDefaults]
1430             boolForKey:MMLoginShellKey];
1431     if (useLoginShell) {
1432         // Run process with a login shell, roughly:
1433         //   echo "exec Vim -g -f args" | ARGV0=-`basename $SHELL` $SHELL [-l]
1434         pid = [self executeInLoginShell:path arguments:taskArgs];
1435     } else {
1436         // Run process directly:
1437         //   Vim -g -f args
1438         NSTask *task = [NSTask launchedTaskWithLaunchPath:path
1439                                                 arguments:taskArgs];
1440         pid = task ? [task processIdentifier] : -1;
1441     }
1443     if (-1 != pid) {
1444         // The 'pidArguments' dictionary keeps arguments to be passed to the
1445         // process when it connects (this is in contrast to arguments which are
1446         // passed on the command line, like '-f' and '-g').
1447         // If this method is called with nil arguments we take this as a hint
1448         // that this is an "untitled window" being launched and add a null
1449         // object to the 'pidArguments' dictionary.  This way we can detect if
1450         // an untitled window is being launched by looking for null objects in
1451         // this dictionary.
1452         // If this method is called with non-nil arguments then it is assumed
1453         // that the caller takes care of adding items to 'pidArguments' as
1454         // necessary (only some arguments are passed on connect, e.g. files to
1455         // open).
1456         if (!args)
1457             [pidArguments setObject:[NSNull null]
1458                              forKey:[NSNumber numberWithInt:pid]];
1459     } else {
1460         ASLogWarn(@"Failed to launch Vim process: args=%@, useLoginShell=%d",
1461                   args, useLoginShell);
1462     }
1464     return pid;
1467 - (NSArray *)filterFilesAndNotify:(NSArray *)filenames
1469     // Go trough 'filenames' array and make sure each file exists.  Present
1470     // warning dialog if some file was missing.
1472     NSString *firstMissingFile = nil;
1473     NSMutableArray *files = [NSMutableArray array];
1474     unsigned i, count = [filenames count];
1476     for (i = 0; i < count; ++i) {
1477         NSString *name = [filenames objectAtIndex:i];
1478         if ([[NSFileManager defaultManager] fileExistsAtPath:name]) {
1479             [files addObject:name];
1480         } else if (!firstMissingFile) {
1481             firstMissingFile = name;
1482         }
1483     }
1485     if (firstMissingFile) {
1486         NSAlert *alert = [[NSAlert alloc] init];
1487         [alert addButtonWithTitle:NSLocalizedString(@"OK",
1488                 @"Dialog button")];
1490         NSString *text;
1491         if ([files count] >= count-1) {
1492             [alert setMessageText:NSLocalizedString(@"File not found",
1493                     @"File not found dialog, title")];
1494             text = [NSString stringWithFormat:NSLocalizedString(
1495                     @"Could not open file with name %@.",
1496                     @"File not found dialog, text"), firstMissingFile];
1497         } else {
1498             [alert setMessageText:NSLocalizedString(@"Multiple files not found",
1499                     @"File not found dialog, title")];
1500             text = [NSString stringWithFormat:NSLocalizedString(
1501                     @"Could not open file with name %@, and %d other files.",
1502                     @"File not found dialog, text"),
1503                 firstMissingFile, count-[files count]-1];
1504         }
1506         [alert setInformativeText:text];
1507         [alert setAlertStyle:NSWarningAlertStyle];
1509         [alert runModal];
1510         [alert release];
1512         [NSApp replyToOpenOrPrint:NSApplicationDelegateReplyFailure];
1513     }
1515     return files;
1518 - (NSArray *)filterOpenFiles:(NSArray *)filenames
1519                openFilesDict:(NSDictionary **)openFiles
1521     // Filter out any files in the 'filenames' array that are open and return
1522     // all files that are not already open.  On return, the 'openFiles'
1523     // parameter (if non-nil) will point to a dictionary of open files, indexed
1524     // by Vim controller.
1526     NSMutableDictionary *dict = [NSMutableDictionary dictionary];
1527     NSMutableArray *files = [filenames mutableCopy];
1529     // TODO: Escape special characters in 'files'?
1530     NSString *expr = [NSString stringWithFormat:
1531             @"map([\"%@\"],\"bufloaded(v:val)\")",
1532             [files componentsJoinedByString:@"\",\""]];
1534     unsigned i, count = [vimControllers count];
1535     for (i = 0; i < count && [files count] > 0; ++i) {
1536         MMVimController *vc = [vimControllers objectAtIndex:i];
1538         // Query Vim for which files in the 'files' array are open.
1539         NSString *eval = [vc evaluateVimExpression:expr];
1540         if (!eval) continue;
1542         NSIndexSet *idxSet = [NSIndexSet indexSetWithVimList:eval];
1543         if ([idxSet count] > 0) {
1544             [dict setObject:[files objectsAtIndexes:idxSet]
1545                      forKey:[NSValue valueWithPointer:vc]];
1547             // Remove all the files that were open in this Vim process and
1548             // create a new expression to evaluate.
1549             [files removeObjectsAtIndexes:idxSet];
1550             expr = [NSString stringWithFormat:
1551                     @"map([\"%@\"],\"bufloaded(v:val)\")",
1552                     [files componentsJoinedByString:@"\",\""]];
1553         }
1554     }
1556     if (openFiles != nil)
1557         *openFiles = dict;
1559     return files;
1562 #if MM_HANDLE_XCODE_MOD_EVENT
1563 - (void)handleXcodeModEvent:(NSAppleEventDescriptor *)event
1564                  replyEvent:(NSAppleEventDescriptor *)reply
1566 #if 0
1567     // Xcode sends this event to query MacVim which open files have been
1568     // modified.
1569     ASLogDebug(@"reply:%@", reply);
1570     ASLogDebug(@"event:%@", event);
1572     NSEnumerator *e = [vimControllers objectEnumerator];
1573     id vc;
1574     while ((vc = [e nextObject])) {
1575         DescType type = [reply descriptorType];
1576         unsigned len = [[type data] length];
1577         NSMutableData *data = [NSMutableData data];
1579         [data appendBytes:&type length:sizeof(DescType)];
1580         [data appendBytes:&len length:sizeof(unsigned)];
1581         [data appendBytes:[reply data] length:len];
1583         [vc sendMessage:XcodeModMsgID data:data];
1584     }
1585 #endif
1587 #endif
1589 - (void)handleGetURLEvent:(NSAppleEventDescriptor *)event
1590                replyEvent:(NSAppleEventDescriptor *)reply
1592     NSString *urlString = [[event paramDescriptorForKeyword:keyDirectObject]
1593         stringValue];
1594     NSURL *url = [NSURL URLWithString:urlString];
1596     // We try to be compatible with TextMate's URL scheme here, as documented
1597     // at http://blog.macromates.com/2007/the-textmate-url-scheme/ . Currently,
1598     // this means that:
1599     //
1600     // The format is: mvim://open?<arguments> where arguments can be:
1601     //
1602     // * url â€” the actual file to open (i.e. a file://… URL), if you leave
1603     //         out this argument, the frontmost document is implied.
1604     // * line â€” line number to go to (one based).
1605     // * column â€” column number to go to (one based).
1606     //
1607     // Example: mvim://open?url=file:///etc/profile&line=20
1609     if ([[url host] isEqualToString:@"open"]) {
1610         NSMutableDictionary *dict = [NSMutableDictionary dictionary];
1612         // Parse query ("url=file://...&line=14") into a dictionary
1613         NSArray *queries = [[url query] componentsSeparatedByString:@"&"];
1614         NSEnumerator *enumerator = [queries objectEnumerator];
1615         NSString *param;
1616         while( param = [enumerator nextObject] ) {
1617             NSArray *arr = [param componentsSeparatedByString:@"="];
1618             if ([arr count] == 2) {
1619                 [dict setValue:[[arr lastObject]
1620                             stringByReplacingPercentEscapesUsingEncoding:
1621                                 NSUTF8StringEncoding]
1622                         forKey:[[arr objectAtIndex:0]
1623                             stringByReplacingPercentEscapesUsingEncoding:
1624                                 NSUTF8StringEncoding]];
1625             }
1626         }
1628         // Actually open the file.
1629         NSString *file = [dict objectForKey:@"url"];
1630         if (file != nil) {
1631             NSURL *fileUrl= [NSURL URLWithString:file];
1632             // TextMate only opens files that already exist.
1633             if ([fileUrl isFileURL]
1634                     && [[NSFileManager defaultManager] fileExistsAtPath:
1635                            [fileUrl path]]) {
1636                 // Strip 'file://' path, else application:openFiles: might think
1637                 // the file is not yet open.
1638                 NSArray *filenames = [NSArray arrayWithObject:[fileUrl path]];
1640                 // Look for the line and column options.
1641                 NSDictionary *args = nil;
1642                 NSString *line = [dict objectForKey:@"line"];
1643                 if (line) {
1644                     NSString *column = [dict objectForKey:@"column"];
1645                     if (column)
1646                         args = [NSDictionary dictionaryWithObjectsAndKeys:
1647                                 line, @"cursorLine",
1648                                 column, @"cursorColumn",
1649                                 nil];
1650                     else
1651                         args = [NSDictionary dictionaryWithObject:line
1652                                 forKey:@"cursorLine"];
1653                 }
1655                 [self openFiles:filenames withArguments:args];
1656             }
1657         }
1658     } else {
1659         NSAlert *alert = [[NSAlert alloc] init];
1660         [alert addButtonWithTitle:NSLocalizedString(@"OK",
1661             @"Dialog button")];
1663         [alert setMessageText:NSLocalizedString(@"Unknown URL Scheme",
1664             @"Unknown URL Scheme dialog, title")];
1665         [alert setInformativeText:[NSString stringWithFormat:NSLocalizedString(
1666             @"This version of MacVim does not support \"%@\""
1667             @" in its URL scheme.",
1668             @"Unknown URL Scheme dialog, text"),
1669             [url host]]];
1671         [alert setAlertStyle:NSWarningAlertStyle];
1672         [alert runModal];
1673         [alert release];
1674     }
1678 - (int)findLaunchingProcessWithoutArguments
1680     NSArray *keys = [pidArguments allKeysForObject:[NSNull null]];
1681     if ([keys count] > 0)
1682         return [[keys objectAtIndex:0] intValue];
1684     return -1;
1687 - (MMVimController *)findUnusedEditor
1689     NSEnumerator *e = [vimControllers objectEnumerator];
1690     id vc;
1691     while ((vc = [e nextObject])) {
1692         if ([[vc objectForVimStateKey:@"unusedEditor"] boolValue])
1693             return vc;
1694     }
1696     return nil;
1699 - (NSMutableDictionary *)extractArgumentsFromOdocEvent:
1700     (NSAppleEventDescriptor *)desc
1702     NSMutableDictionary *dict = [NSMutableDictionary dictionary];
1704     // 1. Extract ODB parameters (if any)
1705     NSAppleEventDescriptor *odbdesc = desc;
1706     if (![odbdesc paramDescriptorForKeyword:keyFileSender]) {
1707         // The ODB paramaters may hide inside the 'keyAEPropData' descriptor.
1708         odbdesc = [odbdesc paramDescriptorForKeyword:keyAEPropData];
1709         if (![odbdesc paramDescriptorForKeyword:keyFileSender])
1710             odbdesc = nil;
1711     }
1713     if (odbdesc) {
1714         NSAppleEventDescriptor *p =
1715                 [odbdesc paramDescriptorForKeyword:keyFileSender];
1716         if (p)
1717             [dict setObject:[NSNumber numberWithUnsignedInt:[p typeCodeValue]]
1718                      forKey:@"remoteID"];
1720         p = [odbdesc paramDescriptorForKeyword:keyFileCustomPath];
1721         if (p)
1722             [dict setObject:[p stringValue] forKey:@"remotePath"];
1724         p = [odbdesc paramDescriptorForKeyword:keyFileSenderToken];
1725         if (p) {
1726             [dict setObject:[NSNumber numberWithUnsignedLong:[p descriptorType]]
1727                      forKey:@"remoteTokenDescType"];
1728             [dict setObject:[p data] forKey:@"remoteTokenData"];
1729         }
1730     }
1732     // 2. Extract Xcode parameters (if any)
1733     NSAppleEventDescriptor *xcodedesc =
1734             [desc paramDescriptorForKeyword:keyAEPosition];
1735     if (xcodedesc) {
1736         NSRange range;
1737         MMSelectionRange *sr = (MMSelectionRange*)[[xcodedesc data] bytes];
1739         if (sr->lineNum < 0) {
1740             // Should select a range of lines.
1741             range.location = sr->startRange + 1;
1742             range.length = sr->endRange - sr->startRange + 1;
1743         } else {
1744             // Should only move cursor to a line.
1745             range.location = sr->lineNum + 1;
1746             range.length = 0;
1747         }
1749         [dict setObject:NSStringFromRange(range) forKey:@"selectionRange"];
1750     }
1752     // 3. Extract Spotlight search text (if any)
1753     NSAppleEventDescriptor *spotlightdesc = 
1754             [desc paramDescriptorForKeyword:keyAESearchText];
1755     if (spotlightdesc)
1756         [dict setObject:[spotlightdesc stringValue] forKey:@"searchText"];
1758     return dict;
1761 #ifdef MM_ENABLE_PLUGINS
1762 - (void)removePlugInMenu
1764     if ([plugInMenuItem menu])
1765         [[plugInMenuItem menu] removeItem:plugInMenuItem];
1768 - (void)addPlugInMenuToMenu:(NSMenu *)mainMenu
1770     NSMenu *windowsMenu = [mainMenu findWindowsMenu];
1772     if ([[plugInMenuItem submenu] numberOfItems] > 0) {
1773         int idx = windowsMenu ? [mainMenu indexOfItemWithSubmenu:windowsMenu]
1774                               : -1;
1775         if (idx > 0) {
1776             [mainMenu insertItem:plugInMenuItem atIndex:idx];
1777         } else {
1778             [mainMenu addItem:plugInMenuItem];
1779         }
1780     }
1782 #endif
1784 - (void)scheduleVimControllerPreloadAfterDelay:(NSTimeInterval)delay
1786     [self performSelector:@selector(preloadVimController:)
1787                withObject:nil
1788                afterDelay:delay];
1791 - (void)cancelVimControllerPreloadRequests
1793     [NSObject cancelPreviousPerformRequestsWithTarget:self
1794             selector:@selector(preloadVimController:)
1795               object:nil];
1798 - (void)preloadVimController:(id)sender
1800     // We only allow preloading of one Vim process at a time (to avoid hogging
1801     // CPU), so schedule another preload in a little while if necessary.
1802     if (-1 != preloadPid) {
1803         [self scheduleVimControllerPreloadAfterDelay:2];
1804         return;
1805     }
1807     if ([cachedVimControllers count] >= [self maxPreloadCacheSize])
1808         return;
1810     preloadPid = [self launchVimProcessWithArguments:
1811             [NSArray arrayWithObject:@"--mmwaitforack"]];
1814 - (int)maxPreloadCacheSize
1816     // The maximum number of Vim processes to keep in the cache can be
1817     // controlled via the user default "MMPreloadCacheSize".
1818     int maxCacheSize = [[NSUserDefaults standardUserDefaults]
1819             integerForKey:MMPreloadCacheSizeKey];
1820     if (maxCacheSize < 0) maxCacheSize = 0;
1821     else if (maxCacheSize > 10) maxCacheSize = 10;
1823     return maxCacheSize;
1826 - (MMVimController *)takeVimControllerFromCache
1828     // NOTE: After calling this message the backend corresponding to the
1829     // returned vim controller must be sent an acknowledgeConnection message,
1830     // else the vim process will be stuck.
1831     //
1832     // This method may return nil even though the cache might be non-empty; the
1833     // caller should handle this by starting a new Vim process.
1835     int i, count = [cachedVimControllers count];
1836     if (0 == count) return nil;
1838     // Locate the first Vim controller with up-to-date rc-files sourced.
1839     NSDate *rcDate = [self rcFilesModificationDate];
1840     for (i = 0; i < count; ++i) {
1841         MMVimController *vc = [cachedVimControllers objectAtIndex:i];
1842         NSDate *date = [vc creationDate];
1843         if ([date compare:rcDate] != NSOrderedAscending)
1844             break;
1845     }
1847     if (i > 0) {
1848         // Clear out cache entries whose vimrc/gvimrc files were sourced before
1849         // the latest modification date for those files.  This ensures that the
1850         // latest rc-files are always sourced for new windows.
1851         [self clearPreloadCacheWithCount:i];
1852     }
1854     if ([cachedVimControllers count] == 0) {
1855         [self scheduleVimControllerPreloadAfterDelay:2.0];
1856         return nil;
1857     }
1859     MMVimController *vc = [cachedVimControllers objectAtIndex:0];
1860     [vimControllers addObject:vc];
1861     [cachedVimControllers removeObjectAtIndex:0];
1862     [vc setIsPreloading:NO];
1864     // If the Vim process has finished loading then the window will displayed
1865     // now, otherwise it will be displayed when the OpenWindowMsgID message is
1866     // received.
1867     [[vc windowController] showWindow];
1869     // Since we've taken one controller from the cache we take the opportunity
1870     // to preload another.
1871     [self scheduleVimControllerPreloadAfterDelay:1];
1873     return vc;
1876 - (void)clearPreloadCacheWithCount:(int)count
1878     // Remove the 'count' first entries in the preload cache.  It is assumed
1879     // that objects are added/removed from the cache in a FIFO manner so that
1880     // this effectively clears the 'count' oldest entries.
1881     // If 'count' is negative, then the entire cache is cleared.
1883     if ([cachedVimControllers count] == 0 || count == 0)
1884         return;
1886     if (count < 0)
1887         count = [cachedVimControllers count];
1889     // Make sure the preloaded Vim processes get killed or they'll just hang
1890     // around being useless until MacVim is terminated.
1891     NSEnumerator *e = [cachedVimControllers objectEnumerator];
1892     MMVimController *vc;
1893     int n = count;
1894     while ((vc = [e nextObject]) && n-- > 0) {
1895         [[NSNotificationCenter defaultCenter] removeObserver:vc];
1896         [vc sendMessage:TerminateNowMsgID data:nil];
1898         // Since the preloaded processes were killed "prematurely" we have to
1899         // manually tell them to cleanup (it is not enough to simply release
1900         // them since deallocation and cleanup are separated).
1901         [vc cleanup];
1902     }
1904     n = count;
1905     while (n-- > 0 && [cachedVimControllers count] > 0)
1906         [cachedVimControllers removeObjectAtIndex:0];
1908     // There is a small delay before the Vim process actually exits so wait a
1909     // little before trying to reap the child process.  If the process still
1910     // hasn't exited after this wait it won't be reaped until the next time
1911     // reapChildProcesses: is called (but this should be harmless).
1912     [self performSelector:@selector(reapChildProcesses:)
1913                withObject:nil
1914                afterDelay:0.1];
1917 - (void)rebuildPreloadCache
1919     if ([self maxPreloadCacheSize] > 0) {
1920         [self clearPreloadCacheWithCount:-1];
1921         [self cancelVimControllerPreloadRequests];
1922         [self scheduleVimControllerPreloadAfterDelay:1.0];
1923     }
1926 - (NSDate *)rcFilesModificationDate
1928     // Check modification dates for ~/.vimrc and ~/.gvimrc and return the
1929     // latest modification date.  If ~/.vimrc does not exist, check ~/_vimrc
1930     // and similarly for gvimrc.
1931     // Returns distantPath if no rc files were found.
1933     NSDate *date = [NSDate distantPast];
1934     NSFileManager *fm = [NSFileManager defaultManager];
1936     NSString *path = [@"~/.vimrc" stringByExpandingTildeInPath];
1937     NSDictionary *attr = [fm fileAttributesAtPath:path traverseLink:YES];
1938     if (!attr) {
1939         path = [@"~/_vimrc" stringByExpandingTildeInPath];
1940         attr = [fm fileAttributesAtPath:path traverseLink:YES];
1941     }
1942     NSDate *modDate = [attr objectForKey:NSFileModificationDate];
1943     if (modDate)
1944         date = modDate;
1946     path = [@"~/.gvimrc" stringByExpandingTildeInPath];
1947     attr = [fm fileAttributesAtPath:path traverseLink:YES];
1948     if (!attr) {
1949         path = [@"~/_gvimrc" stringByExpandingTildeInPath];
1950         attr = [fm fileAttributesAtPath:path traverseLink:YES];
1951     }
1952     modDate = [attr objectForKey:NSFileModificationDate];
1953     if (modDate)
1954         date = [date laterDate:modDate];
1956     return date;
1959 - (BOOL)openVimControllerWithArguments:(NSDictionary *)arguments
1961     MMVimController *vc = [self findUnusedEditor];
1962     if (vc) {
1963         // Open files in an already open window.
1964         [[[vc windowController] window] makeKeyAndOrderFront:self];
1965         [vc passArguments:arguments];
1966     } else if ((vc = [self takeVimControllerFromCache])) {
1967         // Open files in a new window using a cached vim controller.  This
1968         // requires virtually no loading time so the new window will pop up
1969         // instantaneously.
1970         [vc passArguments:arguments];
1971         [[vc backendProxy] acknowledgeConnection];
1972     } else {
1973         // Open files in a launching Vim process or start a new process.  This
1974         // may take 1-2 seconds so there will be a visible delay before the
1975         // window appears on screen.
1976         int pid = [self findLaunchingProcessWithoutArguments];
1977         if (-1 == pid) {
1978             pid = [self launchVimProcessWithArguments:nil];
1979             if (-1 == pid)
1980                 return NO;
1981         }
1983         // TODO: If the Vim process fails to start, or if it changes PID,
1984         // then the memory allocated for these parameters will leak.
1985         // Ensure that this cannot happen or somehow detect it.
1987         if ([arguments count] > 0)
1988             [pidArguments setObject:arguments
1989                              forKey:[NSNumber numberWithInt:pid]];
1990     }
1992     return YES;
1995 - (void)activateWhenNextWindowOpens
1997     ASLogDebug(@"Activate MacVim when next window opens");
1998     shouldActivateWhenNextWindowOpens = YES;
2001 - (void)startWatchingVimDir
2003 #if (MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_4)
2004     if (fsEventStream)
2005         return;
2006     if (NULL == FSEventStreamStart)
2007         return; // FSEvent functions are weakly linked
2009     NSString *path = [@"~/.vim" stringByExpandingTildeInPath];
2010     NSArray *pathsToWatch = [NSArray arrayWithObject:path];
2012     fsEventStream = FSEventStreamCreate(NULL, &fsEventCallback, NULL,
2013             (CFArrayRef)pathsToWatch, kFSEventStreamEventIdSinceNow,
2014             MMEventStreamLatency, kFSEventStreamCreateFlagNone);
2016     FSEventStreamScheduleWithRunLoop(fsEventStream,
2017             [[NSRunLoop currentRunLoop] getCFRunLoop],
2018             kCFRunLoopDefaultMode);
2020     FSEventStreamStart(fsEventStream);
2021     ASLogDebug(@"Started FS event stream");
2022 #endif
2025 - (void)stopWatchingVimDir
2027 #if (MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_4)
2028     if (NULL == FSEventStreamStop)
2029         return; // FSEvent functions are weakly linked
2031     if (fsEventStream) {
2032         FSEventStreamStop(fsEventStream);
2033         FSEventStreamInvalidate(fsEventStream);
2034         FSEventStreamRelease(fsEventStream);
2035         fsEventStream = NULL;
2036         ASLogDebug(@"Stopped FS event stream");
2037     }
2038 #endif
2042 - (void)handleFSEvent
2044     [self clearPreloadCacheWithCount:-1];
2046     // Several FS events may arrive in quick succession so make sure to cancel
2047     // any previous preload requests before making a new one.
2048     [self cancelVimControllerPreloadRequests];
2049     [self scheduleVimControllerPreloadAfterDelay:0.5];
2052 - (void)loadDefaultFont
2054     // It is possible to set a user default to avoid loading the default font
2055     // (this cuts down on startup time).
2056     if (![[NSUserDefaults standardUserDefaults] boolForKey:MMLoadDefaultFontKey]
2057             || fontContainerRef) {
2058         ASLogInfo(@"Skip loading of the default font...");
2059         return;
2060     }
2062     ASLogInfo(@"Loading the default font...");
2064     // Load all fonts in the Resouces folder of the app bundle.
2065     NSString *fontsFolder = [[NSBundle mainBundle] resourcePath];
2066     if (fontsFolder) {
2067         NSURL *fontsURL = [NSURL fileURLWithPath:fontsFolder];
2068         if (fontsURL) {
2069             FSRef fsRef;
2070             CFURLGetFSRef((CFURLRef)fontsURL, &fsRef);
2072 #if (MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_4)
2073             // This is the font activation API for OS X 10.5.  Only compile
2074             // this code if we're building on OS X 10.5 or later.
2075             if (NULL != ATSFontActivateFromFileReference) { // Weakly linked
2076                 ATSFontActivateFromFileReference(&fsRef, kATSFontContextLocal,
2077                                                  kATSFontFormatUnspecified,
2078                                                  NULL, kATSOptionFlagsDefault,
2079                                                  &fontContainerRef);
2080             }
2081 #endif
2082 #if (MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_4)
2083             // The following font activation API was deprecated in OS X 10.5.
2084             // Don't compile this code unless we're targeting OS X 10.4.
2085             FSSpec fsSpec;
2086             if (fontContainerRef == 0 &&
2087                     FSGetCatalogInfo(&fsRef, kFSCatInfoNone, NULL, NULL,
2088                                      &fsSpec, NULL) == noErr) {
2089                 ATSFontActivateFromFileSpecification(&fsSpec,
2090                         kATSFontContextLocal, kATSFontFormatUnspecified, NULL,
2091                         kATSOptionFlagsDefault, &fontContainerRef);
2092             }
2093 #endif
2094         }
2095     }
2097     if (!fontContainerRef) {
2098         ASLogNotice(@"Failed to activate the default font (the app bundle "
2099                     "may be incomplete)");
2100     }
2103 - (int)executeInLoginShell:(NSString *)path arguments:(NSArray *)args
2105     // Start a login shell and execute the command 'path' with arguments 'args'
2106     // in the shell.  This ensures that user environment variables are set even
2107     // when MacVim was started from the Finder.
2109     int pid = -1;
2110     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
2112     // Determine which shell to use to execute the command.  The user
2113     // may decide which shell to use by setting a user default or the
2114     // $SHELL environment variable.
2115     NSString *shell = [ud stringForKey:MMLoginShellCommandKey];
2116     if (!shell || [shell length] == 0)
2117         shell = [[[NSProcessInfo processInfo] environment]
2118             objectForKey:@"SHELL"];
2119     if (!shell)
2120         shell = @"/bin/bash";
2122     // Bash needs the '-l' flag to launch a login shell.  The user may add
2123     // flags by setting a user default.
2124     NSString *shellArgument = [ud stringForKey:MMLoginShellArgumentKey];
2125     if (!shellArgument || [shellArgument length] == 0) {
2126         if ([[shell lastPathComponent] isEqual:@"bash"])
2127             shellArgument = @"-l";
2128         else
2129             shellArgument = nil;
2130     }
2132     // Build input string to pipe to the login shell.
2133     NSMutableString *input = [NSMutableString stringWithFormat:
2134             @"exec \"%@\"", path];
2135     if (args) {
2136         // Append all arguments, making sure they are properly quoted, even
2137         // when they contain single quotes.
2138         NSEnumerator *e = [args objectEnumerator];
2139         id obj;
2141         while ((obj = [e nextObject])) {
2142             NSMutableString *arg = [NSMutableString stringWithString:obj];
2143             [arg replaceOccurrencesOfString:@"'" withString:@"'\"'\"'"
2144                                     options:NSLiteralSearch
2145                                       range:NSMakeRange(0, [arg length])];
2146             [input appendFormat:@" '%@'", arg];
2147         }
2148     }
2150     // Build the argument vector used to start the login shell.
2151     NSString *shellArg0 = [NSString stringWithFormat:@"-%@",
2152              [shell lastPathComponent]];
2153     char *shellArgv[3] = { (char *)[shellArg0 UTF8String], NULL, NULL };
2154     if (shellArgument)
2155         shellArgv[1] = (char *)[shellArgument UTF8String];
2157     // Get the C string representation of the shell path before the fork since
2158     // we must not call Foundation functions after a fork.
2159     const char *shellPath = [shell fileSystemRepresentation];
2161     // Fork and execute the process.
2162     int ds[2];
2163     if (pipe(ds)) return -1;
2165     pid = fork();
2166     if (pid == -1) {
2167         return -1;
2168     } else if (pid == 0) {
2169         // Child process
2171         if (close(ds[1]) == -1) exit(255);
2172         if (dup2(ds[0], 0) == -1) exit(255);
2174         // Without the following call warning messages like this appear on the
2175         // console:
2176         //     com.apple.launchd[69] : Stray process with PGID equal to this
2177         //                             dead job: PID 1589 PPID 1 Vim
2178         setsid();
2180         execv(shellPath, shellArgv);
2182         // Never reached unless execv fails
2183         exit(255);
2184     } else {
2185         // Parent process
2186         if (close(ds[0]) == -1) return -1;
2188         // Send input to execute to the child process
2189         [input appendString:@"\n"];
2190         int bytes = [input lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
2192         if (write(ds[1], [input UTF8String], bytes) != bytes) return -1;
2193         if (close(ds[1]) == -1) return -1;
2195         ++numChildProcesses;
2196         ASLogDebug(@"new process pid=%d (count=%d)", pid, numChildProcesses);
2197     }
2199     return pid;
2202 - (void)reapChildProcesses:(id)sender
2204     // NOTE: numChildProcesses (currently) only counts the number of Vim
2205     // processes that have been started with executeInLoginShell::.  If other
2206     // processes are spawned this code may need to be adjusted (or
2207     // numChildProcesses needs to be incremented when such a process is
2208     // started).
2209     while (numChildProcesses > 0) {
2210         int status = 0;
2211         int pid = waitpid(-1, &status, WNOHANG);
2212         if (pid <= 0)
2213             break;
2215         ASLogDebug(@"Wait for pid=%d complete", pid);
2216         --numChildProcesses;
2217     }
2220 - (void)processInputQueues:(id)sender
2222     // NOTE: Because we use distributed objects it is quite possible for this
2223     // function to be re-entered.  This can cause all sorts of unexpected
2224     // problems so we guard against it here so that the rest of the code does
2225     // not need to worry about it.
2227     // The processing flag is > 0 if this function is already on the call
2228     // stack; < 0 if this function was also re-entered.
2229     if (processingFlag != 0) {
2230         ASLogDebug(@"BUSY!");
2231         processingFlag = -1;
2232         return;
2233     }
2235     // NOTE: Be _very_ careful that no exceptions can be raised between here
2236     // and the point at which 'processingFlag' is reset.  Otherwise the above
2237     // test could end up always failing and no input queues would ever be
2238     // processed!
2239     processingFlag = 1;
2241     // NOTE: New input may arrive while we're busy processing; we deal with
2242     // this by putting the current queue aside and creating a new input queue
2243     // for future input.
2244     NSDictionary *queues = inputQueues;
2245     inputQueues = [NSMutableDictionary new];
2247     // Pass each input queue on to the vim controller with matching
2248     // identifier (and note that it could be cached).
2249     NSEnumerator *e = [queues keyEnumerator];
2250     NSNumber *key;
2251     while ((key = [e nextObject])) {
2252         unsigned ukey = [key unsignedIntValue];
2253         int i = 0, count = [vimControllers count];
2254         for (i = 0; i < count; ++i) {
2255             MMVimController *vc = [vimControllers objectAtIndex:i];
2256             if (ukey == [vc identifier]) {
2257                 [vc processInputQueue:[queues objectForKey:key]]; // !exceptions
2258                 break;
2259             }
2260         }
2262         if (i < count) continue;
2264         count = [cachedVimControllers count];
2265         for (i = 0; i < count; ++i) {
2266             MMVimController *vc = [cachedVimControllers objectAtIndex:i];
2267             if (ukey == [vc identifier]) {
2268                 [vc processInputQueue:[queues objectForKey:key]]; // !exceptions
2269                 break;
2270             }
2271         }
2273         if (i == count) {
2274             ASLogWarn(@"No Vim controller for identifier=%d", ukey);
2275         }
2276     }
2278     [queues release];
2280     // If new input arrived while we were processing it would have been
2281     // blocked so we have to schedule it to be processed again.
2282     if (processingFlag < 0)
2283         [self performSelectorOnMainThread:@selector(processInputQueues:)
2284                                withObject:nil
2285                             waitUntilDone:NO
2286                                     modes:[NSArray arrayWithObjects:
2287                                            NSDefaultRunLoopMode,
2288                                            NSEventTrackingRunLoopMode, nil]];
2290     processingFlag = 0;
2293 - (void)addVimController:(MMVimController *)vc
2295     ASLogDebug(@"Add Vim controller pid=%d id=%d", [vc pid], [vc identifier]);
2297     int pid = [vc pid];
2298     NSNumber *pidKey = [NSNumber numberWithInt:pid];
2300     if (preloadPid == pid) {
2301         // This controller was preloaded, so add it to the cache and
2302         // schedule another vim process to be preloaded.
2303         preloadPid = -1;
2304         [vc setIsPreloading:YES];
2305         [cachedVimControllers addObject:vc];
2306         [self scheduleVimControllerPreloadAfterDelay:1];
2307     } else {
2308         [vimControllers addObject:vc];
2310         id args = [pidArguments objectForKey:pidKey];
2311         if (args && [NSNull null] != args)
2312             [vc passArguments:args];
2314         // HACK!  MacVim does not get activated if it is launched from the
2315         // terminal, so we forcibly activate here unless it is an untitled
2316         // window opening.  Untitled windows are treated differently, else
2317         // MacVim would steal the focus if another app was activated while the
2318         // untitled window was loading.
2319         if (!args || args != [NSNull null])
2320             [self activateWhenNextWindowOpens];
2322         if (args)
2323             [pidArguments removeObjectForKey:pidKey];
2324     }
2327 @end // MMAppController (Private)