Add Vimball (vba) as supported filetype
[MacVim.git] / src / MacVim / MMAppController.m
blob53b727525dec99cdb02e10b4c04abd202d6909bc
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
86 @interface MMAppController (MMServices)
87 - (void)openSelection:(NSPasteboard *)pboard userData:(NSString *)userData
88                 error:(NSString **)error;
89 - (void)openFile:(NSPasteboard *)pboard userData:(NSString *)userData
90            error:(NSString **)error;
91 - (void)newFileHere:(NSPasteboard *)pboard userData:(NSString *)userData
92               error:(NSString **)error;
93 @end
96 @interface MMAppController (Private)
97 - (MMVimController *)topmostVimController;
98 - (int)launchVimProcessWithArguments:(NSArray *)args;
99 - (NSArray *)filterFilesAndNotify:(NSArray *)files;
100 - (NSArray *)filterOpenFiles:(NSArray *)filenames
101                openFilesDict:(NSDictionary **)openFiles;
102 #if MM_HANDLE_XCODE_MOD_EVENT
103 - (void)handleXcodeModEvent:(NSAppleEventDescriptor *)event
104                  replyEvent:(NSAppleEventDescriptor *)reply;
105 #endif
106 - (void)handleGetURLEvent:(NSAppleEventDescriptor *)event
107                replyEvent:(NSAppleEventDescriptor *)reply;
108 - (int)findLaunchingProcessWithoutArguments;
109 - (MMVimController *)findUnusedEditor;
110 - (NSMutableDictionary *)extractArgumentsFromOdocEvent:
111     (NSAppleEventDescriptor *)desc;
112 - (void)scheduleVimControllerPreloadAfterDelay:(NSTimeInterval)delay;
113 - (void)cancelVimControllerPreloadRequests;
114 - (void)preloadVimController:(id)sender;
115 - (int)maxPreloadCacheSize;
116 - (MMVimController *)takeVimControllerFromCache;
117 - (void)clearPreloadCacheWithCount:(int)count;
118 - (void)rebuildPreloadCache;
119 - (NSDate *)rcFilesModificationDate;
120 - (BOOL)openVimControllerWithArguments:(NSDictionary *)arguments;
121 - (void)activateWhenNextWindowOpens;
122 - (void)startWatchingVimDir;
123 - (void)stopWatchingVimDir;
124 - (void)handleFSEvent;
125 - (void)loadDefaultFont;
126 - (int)executeInLoginShell:(NSString *)path arguments:(NSArray *)args;
127 - (void)reapChildProcesses:(id)sender;
128 - (void)processInputQueues:(id)sender;
129 - (void)addVimController:(MMVimController *)vc;
131 #ifdef MM_ENABLE_PLUGINS
132 - (void)removePlugInMenu;
133 - (void)addPlugInMenuToMenu:(NSMenu *)mainMenu;
134 #endif
135 @end
139 #if (MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_4)
140     static void
141 fsEventCallback(ConstFSEventStreamRef streamRef,
142                 void *clientCallBackInfo,
143                 size_t numEvents,
144                 void *eventPaths,
145                 const FSEventStreamEventFlags eventFlags[],
146                 const FSEventStreamEventId eventIds[])
148     [[MMAppController sharedInstance] handleFSEvent];
150 #endif
152 @implementation MMAppController
154 + (void)initialize
156     static BOOL initDone = NO;
157     if (initDone) return;
158     initDone = YES;
160     ASLInit();
162     NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:
163         [NSNumber numberWithBool:NO],   MMNoWindowKey,
164         [NSNumber numberWithInt:64],    MMTabMinWidthKey,
165         [NSNumber numberWithInt:6*64],  MMTabMaxWidthKey,
166         [NSNumber numberWithInt:132],   MMTabOptimumWidthKey,
167         [NSNumber numberWithBool:YES],  MMShowAddTabButtonKey,
168         [NSNumber numberWithInt:2],     MMTextInsetLeftKey,
169         [NSNumber numberWithInt:1],     MMTextInsetRightKey,
170         [NSNumber numberWithInt:1],     MMTextInsetTopKey,
171         [NSNumber numberWithInt:1],     MMTextInsetBottomKey,
172         @"MMTypesetter",                MMTypesetterKey,
173         [NSNumber numberWithFloat:1],   MMCellWidthMultiplierKey,
174         [NSNumber numberWithFloat:-1],  MMBaselineOffsetKey,
175         [NSNumber numberWithBool:YES],  MMTranslateCtrlClickKey,
176         [NSNumber numberWithInt:0],     MMOpenInCurrentWindowKey,
177         [NSNumber numberWithBool:NO],   MMNoFontSubstitutionKey,
178         [NSNumber numberWithBool:YES],  MMLoginShellKey,
179         [NSNumber numberWithBool:NO],   MMAtsuiRendererKey,
180         [NSNumber numberWithInt:MMUntitledWindowAlways],
181                                         MMUntitledWindowKey,
182         [NSNumber numberWithBool:NO],   MMTexturedWindowKey,
183         [NSNumber numberWithBool:NO],   MMZoomBothKey,
184         @"",                            MMLoginShellCommandKey,
185         @"",                            MMLoginShellArgumentKey,
186         [NSNumber numberWithBool:YES],  MMDialogsTrackPwdKey,
187 #ifdef MM_ENABLE_PLUGINS
188         [NSNumber numberWithBool:YES],  MMShowLeftPlugInContainerKey,
189 #endif
190         [NSNumber numberWithInt:3],     MMOpenLayoutKey,
191         [NSNumber numberWithBool:NO],   MMVerticalSplitKey,
192         [NSNumber numberWithInt:0],     MMPreloadCacheSizeKey,
193         [NSNumber numberWithInt:0],     MMLastWindowClosedBehaviorKey,
194         [NSNumber numberWithBool:YES],  MMLoadDefaultFontKey,
195         nil];
197     [[NSUserDefaults standardUserDefaults] registerDefaults:dict];
199     NSArray *types = [NSArray arrayWithObject:NSStringPboardType];
200     [NSApp registerServicesMenuSendTypes:types returnTypes:types];
202     // NOTE: Set the current directory to user's home directory, otherwise it
203     // will default to the root directory.  (This matters since new Vim
204     // processes inherit MacVim's environment variables.)
205     [[NSFileManager defaultManager] changeCurrentDirectoryPath:
206             NSHomeDirectory()];
209 - (id)init
211     if (!(self = [super init])) return nil;
213     [self loadDefaultFont];
215     vimControllers = [NSMutableArray new];
216     cachedVimControllers = [NSMutableArray new];
217     preloadPid = -1;
218     pidArguments = [NSMutableDictionary new];
219     inputQueues = [NSMutableDictionary new];
221 #ifdef MM_ENABLE_PLUGINS
222     NSString *plugInTitle = NSLocalizedString(@"Plug-In",
223                                               @"Plug-In menu title");
224     plugInMenuItem = [[NSMenuItem alloc] initWithTitle:plugInTitle
225                                                 action:NULL
226                                          keyEquivalent:@""];
227     NSMenu *submenu = [[NSMenu alloc] initWithTitle:plugInTitle];
228     [plugInMenuItem setSubmenu:submenu];
229     [submenu release];
230 #endif
232     // NOTE: Do not use the default connection since the Logitech Control
233     // Center (LCC) input manager steals and this would cause MacVim to
234     // never open any windows.  (This is a bug in LCC but since they are
235     // unlikely to fix it, we graciously give them the default connection.)
236     connection = [[NSConnection alloc] initWithReceivePort:[NSPort port]
237                                                   sendPort:nil];
238     [connection setRootObject:self];
239     [connection setRequestTimeout:MMRequestTimeout];
240     [connection setReplyTimeout:MMReplyTimeout];
242     // NOTE!  If the name of the connection changes here it must also be
243     // updated in MMBackend.m.
244     NSString *name = [NSString stringWithFormat:@"%@-connection",
245              [[NSBundle mainBundle] bundlePath]];
246     if (![connection registerName:name]) {
247         ASLogCrit(@"Failed to register connection with name '%@'", name);
248         [connection release];  connection = nil;
249     }
251     return self;
254 - (void)dealloc
256     ASLogDebug(@"");
258     [connection release];  connection = nil;
259     [inputQueues release];  inputQueues = nil;
260     [pidArguments release];  pidArguments = nil;
261     [vimControllers release];  vimControllers = nil;
262     [cachedVimControllers release];  cachedVimControllers = nil;
263     [openSelectionString release];  openSelectionString = nil;
264     [recentFilesMenuItem release];  recentFilesMenuItem = nil;
265     [defaultMainMenu release];  defaultMainMenu = nil;
266 #ifdef MM_ENABLE_PLUGINS
267     [plugInMenuItem release];  plugInMenuItem = nil;
268 #endif
269     [appMenuItemTemplate release];  appMenuItemTemplate = nil;
271     [super dealloc];
274 - (void)applicationWillFinishLaunching:(NSNotification *)notification
276     // Remember the default menu so that it can be restored if the user closes
277     // all editor windows.
278     defaultMainMenu = [[NSApp mainMenu] retain];
280     // Store a copy of the default app menu so we can use this as a template
281     // for all other menus.  We make a copy here because the "Services" menu
282     // will not yet have been populated at this time.  If we don't we get
283     // problems trying to set key equivalents later on because they might clash
284     // with items on the "Services" menu.
285     appMenuItemTemplate = [defaultMainMenu itemAtIndex:0];
286     appMenuItemTemplate = [appMenuItemTemplate copy];
288     // Set up the "Open Recent" menu. See
289     //   http://lapcatsoftware.com/blog/2007/07/10/
290     //     working-without-a-nib-part-5-open-recent-menu/
291     // and
292     //   http://www.cocoabuilder.com/archive/message/cocoa/2007/8/15/187793
293     // for more information.
294     //
295     // The menu itself is created in MainMenu.nib but we still seem to have to
296     // hack around a bit to get it to work.  (This has to be done in
297     // applicationWillFinishLaunching at the latest, otherwise it doesn't
298     // work.)
299     NSMenu *fileMenu = [defaultMainMenu findFileMenu];
300     if (fileMenu) {
301         int idx = [fileMenu indexOfItemWithAction:@selector(fileOpen:)];
302         if (idx >= 0 && idx+1 < [fileMenu numberOfItems])
304         recentFilesMenuItem = [fileMenu itemWithTitle:@"Open Recent"];
305         [[recentFilesMenuItem submenu] performSelector:@selector(_setMenuName:)
306                                         withObject:@"NSRecentDocumentsMenu"];
308         // Note: The "Recent Files" menu must be moved around since there is no
309         // -[NSApp setRecentFilesMenu:] method.  We keep a reference to it to
310         // facilitate this move (see setMainMenu: below).
311         [recentFilesMenuItem retain];
312     }
314 #if MM_HANDLE_XCODE_MOD_EVENT
315     [[NSAppleEventManager sharedAppleEventManager]
316             setEventHandler:self
317                 andSelector:@selector(handleXcodeModEvent:replyEvent:)
318               forEventClass:'KAHL'
319                  andEventID:'MOD '];
320 #endif
322     // Register 'mvim://' URL handler
323     [[NSAppleEventManager sharedAppleEventManager]
324             setEventHandler:self
325                 andSelector:@selector(handleGetURLEvent:replyEvent:)
326               forEventClass:kInternetEventClass
327                  andEventID:kAEGetURL];
330 - (void)applicationDidFinishLaunching:(NSNotification *)notification
332     [NSApp setServicesProvider:self];
333 #ifdef MM_ENABLE_PLUGINS
334     [[MMPlugInManager sharedManager] loadAllPlugIns];
335 #endif
337     if ([self maxPreloadCacheSize] > 0) {
338         [self scheduleVimControllerPreloadAfterDelay:2];
339         [self startWatchingVimDir];
340     }
342     ASLogInfo(@"MacVim finished launching");
345 - (BOOL)applicationShouldOpenUntitledFile:(NSApplication *)sender
347     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
348     NSAppleEventManager *aem = [NSAppleEventManager sharedAppleEventManager];
349     NSAppleEventDescriptor *desc = [aem currentAppleEvent];
351     // The user default MMUntitledWindow can be set to control whether an
352     // untitled window should open on 'Open' and 'Reopen' events.
353     int untitledWindowFlag = [ud integerForKey:MMUntitledWindowKey];
355     BOOL isAppOpenEvent = [desc eventID] == kAEOpenApplication;
356     if (isAppOpenEvent && (untitledWindowFlag & MMUntitledWindowOnOpen) == 0)
357         return NO;
359     BOOL isAppReopenEvent = [desc eventID] == kAEReopenApplication;
360     if (isAppReopenEvent
361             && (untitledWindowFlag & MMUntitledWindowOnReopen) == 0)
362         return NO;
364     // When a process is started from the command line, the 'Open' event may
365     // contain a parameter to surpress the opening of an untitled window.
366     desc = [desc paramDescriptorForKeyword:keyAEPropData];
367     desc = [desc paramDescriptorForKeyword:keyMMUntitledWindow];
368     if (desc && ![desc booleanValue])
369         return NO;
371     // Never open an untitled window if there is at least one open window or if
372     // there are processes that are currently launching.
373     if ([vimControllers count] > 0 || [pidArguments count] > 0)
374         return NO;
376     // NOTE!  This way it possible to start the app with the command-line
377     // argument '-nowindow yes' and no window will be opened by default but
378     // this argument will only be heeded when the application is opening.
379     if (isAppOpenEvent && [ud boolForKey:MMNoWindowKey] == YES)
380         return NO;
382     return YES;
385 - (BOOL)applicationOpenUntitledFile:(NSApplication *)sender
387     ASLogDebug(@"Opening untitled window...");
388     [self newWindow:self];
389     return YES;
392 - (void)application:(NSApplication *)sender openFiles:(NSArray *)filenames
394     ASLogInfo(@"Opening files %@", filenames);
396     // Extract ODB/Xcode/Spotlight parameters from the current Apple event,
397     // sort the filenames, and then let openFiles:withArguments: do the heavy
398     // lifting.
400     if (!(filenames && [filenames count] > 0))
401         return;
403     // Sort filenames since the Finder doesn't take care in preserving the
404     // order in which files are selected anyway (and "sorted" is more
405     // predictable than "random").
406     if ([filenames count] > 1)
407         filenames = [filenames sortedArrayUsingSelector:
408                 @selector(localizedCompare:)];
410     // Extract ODB/Xcode/Spotlight parameters from the current Apple event
411     NSMutableDictionary *arguments = [self extractArgumentsFromOdocEvent:
412             [[NSAppleEventManager sharedAppleEventManager] currentAppleEvent]];
414     if ([self openFiles:filenames withArguments:arguments]) {
415         [NSApp replyToOpenOrPrint:NSApplicationDelegateReplySuccess];
416     } else {
417         // TODO: Notify user of failure?
418         [NSApp replyToOpenOrPrint:NSApplicationDelegateReplyFailure];
419     }
422 - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender
424     return (MMTerminateWhenLastWindowClosed ==
425             [[NSUserDefaults standardUserDefaults]
426                 integerForKey:MMLastWindowClosedBehaviorKey]);
429 - (NSApplicationTerminateReply)applicationShouldTerminate:
430     (NSApplication *)sender
432     // TODO: Follow Apple's guidelines for 'Graceful Application Termination'
433     // (in particular, allow user to review changes and save).
434     int reply = NSTerminateNow;
435     BOOL modifiedBuffers = NO;
437     // Go through windows, checking for modified buffers.  (Each Vim process
438     // tells MacVim when any buffer has been modified and MacVim sets the
439     // 'documentEdited' flag of the window correspondingly.)
440     NSEnumerator *e = [[NSApp windows] objectEnumerator];
441     id window;
442     while ((window = [e nextObject])) {
443         if ([window isDocumentEdited]) {
444             modifiedBuffers = YES;
445             break;
446         }
447     }
449     if (modifiedBuffers) {
450         NSAlert *alert = [[NSAlert alloc] init];
451         [alert setAlertStyle:NSWarningAlertStyle];
452         [alert addButtonWithTitle:NSLocalizedString(@"Quit",
453                 @"Dialog button")];
454         [alert addButtonWithTitle:NSLocalizedString(@"Cancel",
455                 @"Dialog button")];
456         [alert setMessageText:NSLocalizedString(@"Quit without saving?",
457                 @"Quit dialog with changed buffers, title")];
458         [alert setInformativeText:NSLocalizedString(
459                 @"There are modified buffers, "
460                 "if you quit now all changes will be lost.  Quit anyway?",
461                 @"Quit dialog with changed buffers, text")];
463         if ([alert runModal] != NSAlertFirstButtonReturn)
464             reply = NSTerminateCancel;
466         [alert release];
467     } else {
468         // No unmodified buffers, but give a warning if there are multiple
469         // windows and/or tabs open.
470         int numWindows = [vimControllers count];
471         int numTabs = 0;
473         // Count the number of open tabs
474         e = [vimControllers objectEnumerator];
475         id vc;
476         while ((vc = [e nextObject]))
477             numTabs += [[vc objectForVimStateKey:@"numTabs"] intValue];
479         if (numWindows > 1 || numTabs > 1) {
480             NSAlert *alert = [[NSAlert alloc] init];
481             [alert setAlertStyle:NSWarningAlertStyle];
482             [alert addButtonWithTitle:NSLocalizedString(@"Quit",
483                     @"Dialog button")];
484             [alert addButtonWithTitle:NSLocalizedString(@"Cancel",
485                     @"Dialog button")];
486             [alert setMessageText:NSLocalizedString(
487                     @"Are you sure you want to quit MacVim?",
488                     @"Quit dialog with no changed buffers, title")];
490             NSString *info = nil;
491             if (numWindows > 1) {
492                 if (numTabs > numWindows)
493                     info = [NSString stringWithFormat:NSLocalizedString(
494                             @"There are %d windows open in MacVim, with a "
495                             "total of %d tabs. Do you want to quit anyway?",
496                             @"Quit dialog with no changed buffers, text"),
497                          numWindows, numTabs];
498                 else
499                     info = [NSString stringWithFormat:NSLocalizedString(
500                             @"There are %d windows open in MacVim. "
501                             "Do you want to quit anyway?",
502                             @"Quit dialog with no changed buffers, text"),
503                         numWindows];
505             } else {
506                 info = [NSString stringWithFormat:NSLocalizedString(
507                         @"There are %d tabs open in MacVim. "
508                         "Do you want to quit anyway?",
509                         @"Quit dialog with no changed buffers, text"), 
510                      numTabs];
511             }
513             [alert setInformativeText:info];
515             if ([alert runModal] != NSAlertFirstButtonReturn)
516                 reply = NSTerminateCancel;
518             [alert release];
519         }
520     }
523     // Tell all Vim processes to terminate now (otherwise they'll leave swap
524     // files behind).
525     if (NSTerminateNow == reply) {
526         e = [vimControllers objectEnumerator];
527         id vc;
528         while ((vc = [e nextObject])) {
529             ASLogDebug(@"Terminate pid=%d", [vc pid]);
530             [vc sendMessage:TerminateNowMsgID data:nil];
531         }
533         e = [cachedVimControllers objectEnumerator];
534         while ((vc = [e nextObject])) {
535             ASLogDebug(@"Terminate pid=%d (cached)", [vc pid]);
536             [vc sendMessage:TerminateNowMsgID data:nil];
537         }
539         // If a Vim process is being preloaded as we quit we have to forcibly
540         // kill it since we have not established a connection yet.
541         if (preloadPid > 0) {
542             ASLogDebug(@"Kill incomplete preloaded process pid=%d", preloadPid);
543             kill(preloadPid, SIGKILL);
544         }
546         // If a Vim process was loading as we quit we also have to kill it.
547         e = [[pidArguments allKeys] objectEnumerator];
548         NSNumber *pidKey;
549         while ((pidKey = [e nextObject])) {
550             ASLogDebug(@"Kill incomplete process pid=%d", [pidKey intValue]);
551             kill([pidKey intValue], SIGKILL);
552         }
554         // Sleep a little to allow all the Vim processes to exit.
555         usleep(10000);
556     }
558     return reply;
561 - (void)applicationWillTerminate:(NSNotification *)notification
563     ASLogInfo(@"Terminating MacVim...");
565     [self stopWatchingVimDir];
567 #ifdef MM_ENABLE_PLUGINS
568     [[MMPlugInManager sharedManager] unloadAllPlugIns];
569 #endif
571 #if MM_HANDLE_XCODE_MOD_EVENT
572     [[NSAppleEventManager sharedAppleEventManager]
573             removeEventHandlerForEventClass:'KAHL'
574                                  andEventID:'MOD '];
575 #endif
577     // This will invalidate all connections (since they were spawned from this
578     // connection).
579     [connection invalidate];
581     // Deactivate the font we loaded from the app bundle.
582     // NOTE: This can take quite a while (~500 ms), so termination will be
583     // noticeably faster if loading of the default font is disabled.
584     if (fontContainerRef) {
585         ATSFontDeactivate(fontContainerRef, NULL, kATSOptionFlagsDefault);
586         fontContainerRef = 0;
587     }
589     [NSApp setDelegate:nil];
591     // Try to wait for all child processes to avoid leaving zombies behind (but
592     // don't wait around for too long).
593     NSDate *timeOutDate = [NSDate dateWithTimeIntervalSinceNow:2];
594     while ([timeOutDate timeIntervalSinceNow] > 0) {
595         [self reapChildProcesses:nil];
596         if (numChildProcesses <= 0)
597             break;
599         ASLogDebug(@"%d processes still left, hold on...", numChildProcesses);
601         // Run in NSConnectionReplyMode while waiting instead of calling e.g.
602         // usleep().  Otherwise incoming messages may clog up the DO queues and
603         // the outgoing TerminateNowMsgID sent earlier never reaches the Vim
604         // process.
605         // This has at least one side-effect, namely we may receive the
606         // annoying "dropping incoming DO message".  (E.g. this may happen if
607         // you quickly hit Cmd-n several times in a row and then immediately
608         // press Cmd-q, Enter.)
609         while (CFRunLoopRunInMode((CFStringRef)NSConnectionReplyMode,
610                 0.05, true) == kCFRunLoopRunHandledSource)
611             ;   // do nothing
612     }
614     if (numChildProcesses > 0) {
615         ASLogNotice(@"%d zombies left behind", numChildProcesses);
616     }
619 + (MMAppController *)sharedInstance
621     // Note: The app controller is a singleton which is instantiated in
622     // MainMenu.nib where it is also connected as the delegate of NSApp.
623     id delegate = [NSApp delegate];
624     return [delegate isKindOfClass:self] ? (MMAppController*)delegate : nil;
627 - (NSMenu *)defaultMainMenu
629     return defaultMainMenu;
632 - (NSMenuItem *)appMenuItemTemplate
634     return appMenuItemTemplate;
637 - (void)removeVimController:(id)controller
639     ASLogDebug(@"Remove Vim controller pid=%d id=%d (processingFlag=%d)",
640                [controller pid], [controller identifier], processingFlag);
642     int idx = [vimControllers indexOfObject:controller];
643     if (NSNotFound == idx) {
644         ASLogDebug(@"Controller not found, probably due to duplicate removal");
645         return;
646     }
648     [controller cleanup];
650     [vimControllers removeObjectAtIndex:idx];
652     if (![vimControllers count]) {
653         // The last editor window just closed so restore the main menu back to
654         // its default state (which is defined in MainMenu.nib).
655         [self setMainMenu:defaultMainMenu];
657         BOOL hide = (MMHideWhenLastWindowClosed ==
658                     [[NSUserDefaults standardUserDefaults]
659                         integerForKey:MMLastWindowClosedBehaviorKey]);
660         if (hide)
661             [NSApp hide:self];
662     }
664     // There is a small delay before the Vim process actually exits so wait a
665     // little before trying to reap the child process.  If the process still
666     // hasn't exited after this wait it won't be reaped until the next time
667     // reapChildProcesses: is called (but this should be harmless).
668     [self performSelector:@selector(reapChildProcesses:)
669                withObject:nil
670                afterDelay:0.1];
673 - (void)windowControllerWillOpen:(MMWindowController *)windowController
675     NSPoint topLeft = NSZeroPoint;
676     NSWindow *topWin = [[[self topmostVimController] windowController] window];
677     NSWindow *win = [windowController window];
679     if (!win) return;
681     // If there is a window belonging to a Vim process, cascade from it,
682     // otherwise use the autosaved window position (if any).
683     if (topWin) {
684         NSRect frame = [topWin frame];
685         topLeft = NSMakePoint(frame.origin.x, NSMaxY(frame));
686     } else {
687         NSString *topLeftString = [[NSUserDefaults standardUserDefaults]
688             stringForKey:MMTopLeftPointKey];
689         if (topLeftString)
690             topLeft = NSPointFromString(topLeftString);
691     }
693     if (!NSEqualPoints(topLeft, NSZeroPoint)) {
694         NSPoint oldTopLeft = topLeft;
695         if (topWin)
696             topLeft = [win cascadeTopLeftFromPoint:topLeft];
698         [win setFrameTopLeftPoint:topLeft];
700         if ([win screen]) {
701             NSPoint screenOrigin = [[win screen] frame].origin;
702             if ([win frame].origin.y < screenOrigin.y) {
703                 // Try to avoid shifting the new window downwards if it means
704                 // that the bottom of the window will be off the screen.  E.g.
705                 // if the user has set windows to open maximized in the
706                 // vertical direction then the new window will cascade
707                 // horizontally only.
708                 topLeft.y = oldTopLeft.y;
709                 [win setFrameTopLeftPoint:topLeft];
710             }
712             if ([win frame].origin.y < screenOrigin.y) {
713                 // Move the window to the top of the screen if the bottom of
714                 // the window is still obscured.
715                 topLeft.y = NSMaxY([[win screen] frame]);
716                 [win setFrameTopLeftPoint:topLeft];
717             }
718         } else {
719             ASLogNotice(@"Window not on screen, don't constrain position");
720         }
721     }
723     if (1 == [vimControllers count]) {
724         // The first window autosaves its position.  (The autosaving
725         // features of Cocoa are not used because we need more control over
726         // what is autosaved and when it is restored.)
727         [windowController setWindowAutosaveKey:MMTopLeftPointKey];
728     }
730     if (openSelectionString) {
731         // TODO: Pass this as a parameter instead!  Get rid of
732         // 'openSelectionString' etc.
733         //
734         // There is some text to paste into this window as a result of the
735         // services menu "Open selection ..." being used.
736         [[windowController vimController] dropString:openSelectionString];
737         [openSelectionString release];
738         openSelectionString = nil;
739     }
741     if (shouldActivateWhenNextWindowOpens) {
742         [NSApp activateIgnoringOtherApps:YES];
743         shouldActivateWhenNextWindowOpens = NO;
744     }
747 - (void)setMainMenu:(NSMenu *)mainMenu
749     if ([NSApp mainMenu] == mainMenu) return;
751     // If the new menu has a "Recent Files" dummy item, then swap the real item
752     // for the dummy.  We are forced to do this since Cocoa initializes the
753     // "Recent Files" menu and there is no way to simply point Cocoa to a new
754     // item each time the menus are swapped.
755     NSMenu *fileMenu = [mainMenu findFileMenu];
756     if (recentFilesMenuItem && fileMenu) {
757         int dummyIdx =
758                 [fileMenu indexOfItemWithAction:@selector(recentFilesDummy:)];
759         if (dummyIdx >= 0) {
760             NSMenuItem *dummyItem = [[fileMenu itemAtIndex:dummyIdx] retain];
761             [fileMenu removeItemAtIndex:dummyIdx];
763             NSMenu *recentFilesParentMenu = [recentFilesMenuItem menu];
764             int idx = [recentFilesParentMenu indexOfItem:recentFilesMenuItem];
765             if (idx >= 0) {
766                 [[recentFilesMenuItem retain] autorelease];
767                 [recentFilesParentMenu removeItemAtIndex:idx];
768                 [recentFilesParentMenu insertItem:dummyItem atIndex:idx];
769             }
771             [fileMenu insertItem:recentFilesMenuItem atIndex:dummyIdx];
772             [dummyItem release];
773         }
774     }
776     // Now set the new menu.  Notice that we keep one menu for each editor
777     // window since each editor can have its own set of menus.  When swapping
778     // menus we have to tell Cocoa where the new "MacVim", "Windows", and
779     // "Services" menu are.
780     [NSApp setMainMenu:mainMenu];
782     // Setting the "MacVim" (or "Application") menu ensures that it is typeset
783     // in boldface.  (The setAppleMenu: method used to be public but is now
784     // private so this will have to be considered a bit of a hack!)
785     NSMenu *appMenu = [mainMenu findApplicationMenu];
786     [NSApp performSelector:@selector(setAppleMenu:) withObject:appMenu];
788     NSMenu *servicesMenu = [mainMenu findServicesMenu];
789     [NSApp setServicesMenu:servicesMenu];
791     NSMenu *windowsMenu = [mainMenu findWindowsMenu];
792     if (windowsMenu) {
793         // Cocoa isn't clever enough to get rid of items it has added to the
794         // "Windows" menu so we have to do it ourselves otherwise there will be
795         // multiple menu items for each window in the "Windows" menu.
796         //   This code assumes that the only items Cocoa add are ones which
797         // send off the action makeKeyAndOrderFront:.  (Cocoa will not add
798         // another separator item if the last item on the "Windows" menu
799         // already is a separator, so we needen't worry about separators.)
800         int i, count = [windowsMenu numberOfItems];
801         for (i = count-1; i >= 0; --i) {
802             NSMenuItem *item = [windowsMenu itemAtIndex:i];
803             if ([item action] == @selector(makeKeyAndOrderFront:))
804                 [windowsMenu removeItem:item];
805         }
806     }
807     [NSApp setWindowsMenu:windowsMenu];
809 #ifdef MM_ENABLE_PLUGINS
810     // Move plugin menu from old to new main menu.
811     [self removePlugInMenu];
812     [self addPlugInMenuToMenu:mainMenu];
813 #endif
816 - (NSArray *)filterOpenFiles:(NSArray *)filenames
818     return [self filterOpenFiles:filenames openFilesDict:nil];
821 - (BOOL)openFiles:(NSArray *)filenames withArguments:(NSDictionary *)args
823     // Opening files works like this:
824     //  a) filter out any already open files
825     //  b) open any remaining files
826     //
827     // A file is opened in an untitled window if there is one (it may be
828     // currently launching, or it may already be visible), otherwise a new
829     // window is opened.
830     //
831     // Each launching Vim process has a dictionary of arguments that are passed
832     // to the process when in checks in (via connectBackend:pid:).  The
833     // arguments for each launching process can be looked up by its PID (in the
834     // pidArguments dictionary).
836     NSMutableDictionary *arguments = (args ? [[args mutableCopy] autorelease]
837                                            : [NSMutableDictionary dictionary]);
839     filenames = normalizeFilenames(filenames);
841     //
842     // a) Filter out any already open files
843     //
844     NSString *firstFile = [filenames objectAtIndex:0];
845     MMVimController *firstController = nil;
846     NSDictionary *openFilesDict = nil;
847     filenames = [self filterOpenFiles:filenames openFilesDict:&openFilesDict];
849     // Pass arguments to vim controllers that had files open.
850     id key;
851     NSEnumerator *e = [openFilesDict keyEnumerator];
853     // (Indicate that we do not wish to open any files at the moment.)
854     [arguments setObject:[NSNumber numberWithBool:YES] forKey:@"dontOpen"];
856     while ((key = [e nextObject])) {
857         NSArray *files = [openFilesDict objectForKey:key];
858         [arguments setObject:files forKey:@"filenames"];
860         MMVimController *vc = [key pointerValue];
861         [vc passArguments:arguments];
863         // If this controller holds the first file, then remember it for later.
864         if ([files containsObject:firstFile])
865             firstController = vc;
866     }
868     // The meaning of "layout" is defined by the WIN_* defines in main.c.
869     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
870     int layout = [ud integerForKey:MMOpenLayoutKey];
871     BOOL splitVert = [ud boolForKey:MMVerticalSplitKey];
872     BOOL openInCurrentWindow = [ud boolForKey:MMOpenInCurrentWindowKey];
874     if (splitVert && MMLayoutHorizontalSplit == layout)
875         layout = MMLayoutVerticalSplit;
876     if (layout < 0 || (layout > MMLayoutTabs && openInCurrentWindow))
877         layout = MMLayoutTabs;
879     if ([filenames count] == 0) {
880         // Raise the window containing the first file that was already open,
881         // and make sure that the tab containing that file is selected.  Only
882         // do this when there are no more files to open, otherwise sometimes
883         // the window with 'firstFile' will be raised, other times it might be
884         // the window that will open with the files in the 'filenames' array.
885         firstFile = [firstFile stringByEscapingSpecialFilenameCharacters];
887         NSString *bufCmd = @"tab sb";
888         switch (layout) {
889             case MMLayoutHorizontalSplit: bufCmd = @"sb"; break;
890             case MMLayoutVerticalSplit:   bufCmd = @"vert sb"; break;
891             case MMLayoutArglist:         bufCmd = @"b"; break;
892         }
894         NSString *input = [NSString stringWithFormat:@"<C-\\><C-N>"
895                 ":let oldswb=&swb|let &swb=\"useopen,usetab\"|"
896                 "%@ %@|let &swb=oldswb|unl oldswb|"
897                 "cal foreground()<CR>", bufCmd, firstFile];
899         [firstController addVimInput:input];
901         return YES;
902     }
904     // Add filenames to "Recent Files" menu, unless they are being edited
905     // remotely (using ODB).
906     if ([arguments objectForKey:@"remoteID"] == nil) {
907         [[NSDocumentController sharedDocumentController]
908                 noteNewRecentFilePaths:filenames];
909     }
911     //
912     // b) Open any remaining files
913     //
915     [arguments setObject:[NSNumber numberWithInt:layout] forKey:@"layout"];
916     [arguments setObject:filenames forKey:@"filenames"];
917     // (Indicate that files should be opened from now on.)
918     [arguments setObject:[NSNumber numberWithBool:NO] forKey:@"dontOpen"];
920     MMVimController *vc;
921     if (openInCurrentWindow && (vc = [self topmostVimController])) {
922         // Open files in an already open window.
923         [[[vc windowController] window] makeKeyAndOrderFront:self];
924         [vc passArguments:arguments];
925         return YES;
926     }
928     BOOL openOk = YES;
929     int numFiles = [filenames count];
930     if (MMLayoutWindows == layout && numFiles > 1) {
931         // Open one file at a time in a new window, but don't open too many at
932         // once (at most cap+1 windows will open).  If the user has increased
933         // the preload cache size we'll take that as a hint that more windows
934         // should be able to open at once.
935         int cap = [self maxPreloadCacheSize] - 1;
936         if (cap < 4) cap = 4;
937         if (cap > numFiles) cap = numFiles;
939         int i;
940         for (i = 0; i < cap; ++i) {
941             NSArray *a = [NSArray arrayWithObject:[filenames objectAtIndex:i]];
942             [arguments setObject:a forKey:@"filenames"];
944             // NOTE: We have to copy the args since we'll mutate them in the
945             // next loop and the below call may retain the arguments while
946             // waiting for a process to start.
947             NSDictionary *args = [[arguments copy] autorelease];
949             openOk = [self openVimControllerWithArguments:args];
950             if (!openOk) break;
951         }
953         // Open remaining files in tabs in a new window.
954         if (openOk && numFiles > cap) {
955             NSRange range = { i, numFiles-cap };
956             NSArray *a = [filenames subarrayWithRange:range];
957             [arguments setObject:a forKey:@"filenames"];
958             [arguments setObject:[NSNumber numberWithInt:MMLayoutTabs]
959                           forKey:@"layout"];
961             openOk = [self openVimControllerWithArguments:arguments];
962         }
963     } else {
964         // Open all files at once.
965         openOk = [self openVimControllerWithArguments:arguments];
966     }
968     return openOk;
971 #ifdef MM_ENABLE_PLUGINS
972 - (void)addItemToPlugInMenu:(NSMenuItem *)item
974     NSMenu *menu = [plugInMenuItem submenu];
975     [menu addItem:item];
976     if ([menu numberOfItems] == 1)
977         [self addPlugInMenuToMenu:[NSApp mainMenu]];
980 - (void)removeItemFromPlugInMenu:(NSMenuItem *)item
982     NSMenu *menu = [plugInMenuItem submenu];
983     [menu removeItem:item];
984     if ([menu numberOfItems] == 0)
985         [self removePlugInMenu];
987 #endif
989 - (IBAction)newWindow:(id)sender
991     ASLogDebug(@"Open new window");
993     // A cached controller requires no loading times and results in the new
994     // window popping up instantaneously.  If the cache is empty it may take
995     // 1-2 seconds to start a new Vim process.
996     MMVimController *vc = [self takeVimControllerFromCache];
997     if (vc) {
998         [[vc backendProxy] acknowledgeConnection];
999     } else {
1000         [self launchVimProcessWithArguments:nil];
1001     }
1004 - (IBAction)newWindowAndActivate:(id)sender
1006     [self activateWhenNextWindowOpens];
1007     [self newWindow:sender];
1010 - (IBAction)fileOpen:(id)sender
1012     ASLogDebug(@"Show file open panel");
1014     NSString *dir = nil;
1015     BOOL trackPwd = [[NSUserDefaults standardUserDefaults]
1016             boolForKey:MMDialogsTrackPwdKey];
1017     if (trackPwd) {
1018         MMVimController *vc = [self keyVimController];
1019         if (vc) dir = [vc objectForVimStateKey:@"pwd"];
1020     }
1022     NSOpenPanel *panel = [NSOpenPanel openPanel];
1023     [panel setAllowsMultipleSelection:YES];
1024     [panel setAccessoryView:showHiddenFilesView()];
1026     int result = [panel runModalForDirectory:dir file:nil types:nil];
1027     if (NSOKButton == result)
1028         [self application:NSApp openFiles:[panel filenames]];
1031 - (IBAction)selectNextWindow:(id)sender
1033     ASLogDebug(@"Select next window");
1035     unsigned i, count = [vimControllers count];
1036     if (!count) return;
1038     NSWindow *keyWindow = [NSApp keyWindow];
1039     for (i = 0; i < count; ++i) {
1040         MMVimController *vc = [vimControllers objectAtIndex:i];
1041         if ([[[vc windowController] window] isEqual:keyWindow])
1042             break;
1043     }
1045     if (i < count) {
1046         if (++i >= count)
1047             i = 0;
1048         MMVimController *vc = [vimControllers objectAtIndex:i];
1049         [[vc windowController] showWindow:self];
1050     }
1053 - (IBAction)selectPreviousWindow:(id)sender
1055     ASLogDebug(@"Select previous window");
1057     unsigned i, count = [vimControllers count];
1058     if (!count) return;
1060     NSWindow *keyWindow = [NSApp keyWindow];
1061     for (i = 0; i < count; ++i) {
1062         MMVimController *vc = [vimControllers objectAtIndex:i];
1063         if ([[[vc windowController] window] isEqual:keyWindow])
1064             break;
1065     }
1067     if (i < count) {
1068         if (i > 0) {
1069             --i;
1070         } else {
1071             i = count - 1;
1072         }
1073         MMVimController *vc = [vimControllers objectAtIndex:i];
1074         [[vc windowController] showWindow:self];
1075     }
1078 - (IBAction)orderFrontPreferencePanel:(id)sender
1080     ASLogDebug(@"Show preferences panel");
1081     [[MMPreferenceController sharedPrefsWindowController] showWindow:self];
1084 - (IBAction)openWebsite:(id)sender
1086     ASLogDebug(@"Open MacVim website");
1087     [[NSWorkspace sharedWorkspace] openURL:
1088             [NSURL URLWithString:MMWebsiteString]];
1091 - (IBAction)showVimHelp:(id)sender
1093     ASLogDebug(@"Open window with Vim help");
1094     // Open a new window with the help window maximized.
1095     [self launchVimProcessWithArguments:[NSArray arrayWithObjects:
1096             @"-c", @":h gui_mac", @"-c", @":res", nil]];
1099 - (IBAction)zoomAll:(id)sender
1101     ASLogDebug(@"Zoom all windows");
1102     [NSApp makeWindowsPerform:@selector(performZoom:) inOrder:YES];
1105 - (IBAction)atsuiButtonClicked:(id)sender
1107     ASLogDebug(@"Toggle ATSUI renderer");
1108     // This action is called when the user clicks the "use ATSUI renderer"
1109     // button in the advanced preferences pane.
1110     [self rebuildPreloadCache];
1113 - (IBAction)loginShellButtonClicked:(id)sender
1115     ASLogDebug(@"Toggle login shell option");
1116     // This action is called when the user clicks the "use login shell" button
1117     // in the advanced preferences pane.
1118     [self rebuildPreloadCache];
1121 - (IBAction)quickstartButtonClicked:(id)sender
1123     ASLogDebug(@"Toggle Quickstart option");
1124     if ([self maxPreloadCacheSize] > 0) {
1125         [self scheduleVimControllerPreloadAfterDelay:1.0];
1126         [self startWatchingVimDir];
1127     } else {
1128         [self cancelVimControllerPreloadRequests];
1129         [self clearPreloadCacheWithCount:-1];
1130         [self stopWatchingVimDir];
1131     }
1134 - (MMVimController *)keyVimController
1136     NSWindow *keyWindow = [NSApp keyWindow];
1137     if (keyWindow) {
1138         unsigned i, count = [vimControllers count];
1139         for (i = 0; i < count; ++i) {
1140             MMVimController *vc = [vimControllers objectAtIndex:i];
1141             if ([[[vc windowController] window] isEqual:keyWindow])
1142                 return vc;
1143         }
1144     }
1146     return nil;
1149 - (unsigned)connectBackend:(byref in id <MMBackendProtocol>)proxy pid:(int)pid
1151     ASLogDebug(@"pid=%d", pid);
1153     [(NSDistantObject*)proxy setProtocolForProxy:@protocol(MMBackendProtocol)];
1155     // NOTE: Allocate the vim controller now but don't add it to the list of
1156     // controllers since this is a distributed object call and as such can
1157     // arrive at unpredictable times (e.g. while iterating the list of vim
1158     // controllers).
1159     // (What if input arrives before the vim controller is added to the list of
1160     // controllers?  This should not be a problem since the input isn't
1161     // processed immediately (see processInput:forIdentifier:).)
1162     MMVimController *vc = [[MMVimController alloc] initWithBackend:proxy
1163                                                                pid:pid];
1164     [self performSelector:@selector(addVimController:)
1165                withObject:vc
1166                afterDelay:0];
1168     [vc release];
1170     return [vc identifier];
1173 - (oneway void)processInput:(in bycopy NSArray *)queue
1174               forIdentifier:(unsigned)identifier
1176     // NOTE: Input is not handled immediately since this is a distributed
1177     // object call and as such can arrive at unpredictable times.  Instead,
1178     // queue the input and process it when the run loop is updated.
1180     if (!(queue && identifier)) {
1181         ASLogWarn(@"Bad input for identifier=%d", identifier);
1182         return;
1183     }
1185     ASLogDebug(@"QUEUE for identifier=%d: <<< %@>>>", identifier,
1186                debugStringForMessageQueue(queue));
1188     NSNumber *key = [NSNumber numberWithUnsignedInt:identifier];
1189     NSArray *q = [inputQueues objectForKey:key];
1190     if (q) {
1191         q = [q arrayByAddingObjectsFromArray:queue];
1192         [inputQueues setObject:q forKey:key];
1193     } else {
1194         [inputQueues setObject:queue forKey:key];
1195     }
1197     // NOTE: We must use "event tracking mode" as well as "default mode",
1198     // otherwise the input queue will not be processed e.g. during live
1199     // resizing.
1200     [self performSelector:@selector(processInputQueues:)
1201                withObject:nil
1202                afterDelay:0
1203                   inModes:[NSArray arrayWithObjects:NSDefaultRunLoopMode,
1204                                             NSEventTrackingRunLoopMode, nil]];
1207 - (NSArray *)serverList
1209     NSMutableArray *array = [NSMutableArray array];
1211     unsigned i, count = [vimControllers count];
1212     for (i = 0; i < count; ++i) {
1213         MMVimController *controller = [vimControllers objectAtIndex:i];
1214         if ([controller serverName])
1215             [array addObject:[controller serverName]];
1216     }
1218     return array;
1221 @end // MMAppController
1226 @implementation MMAppController (MMServices)
1228 - (void)openSelection:(NSPasteboard *)pboard userData:(NSString *)userData
1229                 error:(NSString **)error
1231     if (![[pboard types] containsObject:NSStringPboardType]) {
1232         ASLogNotice(@"Pasteboard contains no NSStringPboardType");
1233         return;
1234     }
1236     ASLogInfo(@"Open new window containing current selection");
1238     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
1239     BOOL openInCurrentWindow = [ud boolForKey:MMOpenInCurrentWindowKey];
1240     MMVimController *vc;
1242     if (openInCurrentWindow && (vc = [self topmostVimController])) {
1243         [vc sendMessage:AddNewTabMsgID data:nil];
1244         [vc dropString:[pboard stringForType:NSStringPboardType]];
1245     } else {
1246         // Save the text, open a new window, and paste the text when the next
1247         // window opens.  (If this is called several times in a row, then all
1248         // but the last call may be ignored.)
1249         if (openSelectionString) [openSelectionString release];
1250         openSelectionString = [[pboard stringForType:NSStringPboardType] copy];
1252         [self newWindow:self];
1253     }
1256 - (void)openFile:(NSPasteboard *)pboard userData:(NSString *)userData
1257            error:(NSString **)error
1259     if (![[pboard types] containsObject:NSStringPboardType]) {
1260         ASLogNotice(@"Pasteboard contains no NSStringPboardType");
1261         return;
1262     }
1264     // TODO: Parse multiple filenames and create array with names.
1265     NSString *string = [pboard stringForType:NSStringPboardType];
1266     string = [string stringByTrimmingCharactersInSet:
1267             [NSCharacterSet whitespaceAndNewlineCharacterSet]];
1268     string = [string stringByStandardizingPath];
1270     ASLogInfo(@"Open new window with selected file: %@", string);
1272     NSArray *filenames = [self filterFilesAndNotify:
1273             [NSArray arrayWithObject:string]];
1274     if ([filenames count] == 0)
1275         return;
1277     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
1278     BOOL openInCurrentWindow = [ud boolForKey:MMOpenInCurrentWindowKey];
1279     MMVimController *vc;
1281     if (openInCurrentWindow && (vc = [self topmostVimController])) {
1282         [vc dropFiles:filenames forceOpen:YES];
1283     } else {
1284         [self openFiles:filenames withArguments:nil];
1285     }
1288 - (void)newFileHere:(NSPasteboard *)pboard userData:(NSString *)userData
1289               error:(NSString **)error
1291     if (![[pboard types] containsObject:NSStringPboardType]) {
1292         ASLogNotice(@"Pasteboard contains no NSStringPboardType");
1293         return;
1294     }
1296     NSString *path = [pboard stringForType:NSStringPboardType];
1298     BOOL dirIndicator;
1299     if (![[NSFileManager defaultManager] fileExistsAtPath:path
1300                                               isDirectory:&dirIndicator]) {
1301         ASLogNotice(@"Invalid path. Cannot open new document at: %@", path);
1302         return;
1303     }
1305     ASLogInfo(@"Open new file at path=%@", path);
1307     if (!dirIndicator)
1308         path = [path stringByDeletingLastPathComponent];
1310     path = [path stringByEscapingSpecialFilenameCharacters];
1312     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
1313     BOOL openInCurrentWindow = [ud boolForKey:MMOpenInCurrentWindowKey];
1314     MMVimController *vc;
1316     if (openInCurrentWindow && (vc = [self topmostVimController])) {
1317         NSString *input = [NSString stringWithFormat:@"<C-\\><C-N>"
1318                 ":tabe|cd %@<CR>", path];
1319         [vc addVimInput:input];
1320     } else {
1321         NSString *input = [NSString stringWithFormat:@":cd %@", path];
1322         [self launchVimProcessWithArguments:[NSArray arrayWithObjects:
1323                                              @"-c", input, nil]];
1324     }
1327 @end // MMAppController (MMServices)
1332 @implementation MMAppController (Private)
1334 - (MMVimController *)topmostVimController
1336     // Find the topmost visible window which has an associated vim controller.
1337     NSEnumerator *e = [[NSApp orderedWindows] objectEnumerator];
1338     id window;
1339     while ((window = [e nextObject]) && [window isVisible]) {
1340         unsigned i, count = [vimControllers count];
1341         for (i = 0; i < count; ++i) {
1342             MMVimController *vc = [vimControllers objectAtIndex:i];
1343             if ([[[vc windowController] window] isEqual:window])
1344                 return vc;
1345         }
1346     }
1348     return nil;
1351 - (int)launchVimProcessWithArguments:(NSArray *)args
1353     int pid = -1;
1354     NSString *path = [[NSBundle mainBundle] pathForAuxiliaryExecutable:@"Vim"];
1356     if (!path) {
1357         ASLogCrit(@"Vim executable could not be found inside app bundle!");
1358         return -1;
1359     }
1361     NSArray *taskArgs = [NSArray arrayWithObjects:@"-g", @"-f", nil];
1362     if (args)
1363         taskArgs = [taskArgs arrayByAddingObjectsFromArray:args];
1365     BOOL useLoginShell = [[NSUserDefaults standardUserDefaults]
1366             boolForKey:MMLoginShellKey];
1367     if (useLoginShell) {
1368         // Run process with a login shell, roughly:
1369         //   echo "exec Vim -g -f args" | ARGV0=-`basename $SHELL` $SHELL [-l]
1370         pid = [self executeInLoginShell:path arguments:taskArgs];
1371     } else {
1372         // Run process directly:
1373         //   Vim -g -f args
1374         NSTask *task = [NSTask launchedTaskWithLaunchPath:path
1375                                                 arguments:taskArgs];
1376         pid = task ? [task processIdentifier] : -1;
1377     }
1379     if (-1 != pid) {
1380         // The 'pidArguments' dictionary keeps arguments to be passed to the
1381         // process when it connects (this is in contrast to arguments which are
1382         // passed on the command line, like '-f' and '-g').
1383         // If this method is called with nil arguments we take this as a hint
1384         // that this is an "untitled window" being launched and add a null
1385         // object to the 'pidArguments' dictionary.  This way we can detect if
1386         // an untitled window is being launched by looking for null objects in
1387         // this dictionary.
1388         // If this method is called with non-nil arguments then it is assumed
1389         // that the caller takes care of adding items to 'pidArguments' as
1390         // necessary (only some arguments are passed on connect, e.g. files to
1391         // open).
1392         if (!args)
1393             [pidArguments setObject:[NSNull null]
1394                              forKey:[NSNumber numberWithInt:pid]];
1395     } else {
1396         ASLogWarn(@"Failed to launch Vim process: args=%@, useLoginShell=%d",
1397                   args, useLoginShell);
1398     }
1400     return pid;
1403 - (NSArray *)filterFilesAndNotify:(NSArray *)filenames
1405     // Go trough 'filenames' array and make sure each file exists.  Present
1406     // warning dialog if some file was missing.
1408     NSString *firstMissingFile = nil;
1409     NSMutableArray *files = [NSMutableArray array];
1410     unsigned i, count = [filenames count];
1412     for (i = 0; i < count; ++i) {
1413         NSString *name = [filenames objectAtIndex:i];
1414         if ([[NSFileManager defaultManager] fileExistsAtPath:name]) {
1415             [files addObject:name];
1416         } else if (!firstMissingFile) {
1417             firstMissingFile = name;
1418         }
1419     }
1421     if (firstMissingFile) {
1422         NSAlert *alert = [[NSAlert alloc] init];
1423         [alert addButtonWithTitle:NSLocalizedString(@"OK",
1424                 @"Dialog button")];
1426         NSString *text;
1427         if ([files count] >= count-1) {
1428             [alert setMessageText:NSLocalizedString(@"File not found",
1429                     @"File not found dialog, title")];
1430             text = [NSString stringWithFormat:NSLocalizedString(
1431                     @"Could not open file with name %@.",
1432                     @"File not found dialog, text"), firstMissingFile];
1433         } else {
1434             [alert setMessageText:NSLocalizedString(@"Multiple files not found",
1435                     @"File not found dialog, title")];
1436             text = [NSString stringWithFormat:NSLocalizedString(
1437                     @"Could not open file with name %@, and %d other files.",
1438                     @"File not found dialog, text"),
1439                 firstMissingFile, count-[files count]-1];
1440         }
1442         [alert setInformativeText:text];
1443         [alert setAlertStyle:NSWarningAlertStyle];
1445         [alert runModal];
1446         [alert release];
1448         [NSApp replyToOpenOrPrint:NSApplicationDelegateReplyFailure];
1449     }
1451     return files;
1454 - (NSArray *)filterOpenFiles:(NSArray *)filenames
1455                openFilesDict:(NSDictionary **)openFiles
1457     // Filter out any files in the 'filenames' array that are open and return
1458     // all files that are not already open.  On return, the 'openFiles'
1459     // parameter (if non-nil) will point to a dictionary of open files, indexed
1460     // by Vim controller.
1462     NSMutableDictionary *dict = [NSMutableDictionary dictionary];
1463     NSMutableArray *files = [filenames mutableCopy];
1465     // TODO: Escape special characters in 'files'?
1466     NSString *expr = [NSString stringWithFormat:
1467             @"map([\"%@\"],\"bufloaded(v:val)\")",
1468             [files componentsJoinedByString:@"\",\""]];
1470     unsigned i, count = [vimControllers count];
1471     for (i = 0; i < count && [files count] > 0; ++i) {
1472         MMVimController *vc = [vimControllers objectAtIndex:i];
1474         // Query Vim for which files in the 'files' array are open.
1475         NSString *eval = [vc evaluateVimExpression:expr];
1476         if (!eval) continue;
1478         NSIndexSet *idxSet = [NSIndexSet indexSetWithVimList:eval];
1479         if ([idxSet count] > 0) {
1480             [dict setObject:[files objectsAtIndexes:idxSet]
1481                      forKey:[NSValue valueWithPointer:vc]];
1483             // Remove all the files that were open in this Vim process and
1484             // create a new expression to evaluate.
1485             [files removeObjectsAtIndexes:idxSet];
1486             expr = [NSString stringWithFormat:
1487                     @"map([\"%@\"],\"bufloaded(v:val)\")",
1488                     [files componentsJoinedByString:@"\",\""]];
1489         }
1490     }
1492     if (openFiles != nil)
1493         *openFiles = dict;
1495     return files;
1498 #if MM_HANDLE_XCODE_MOD_EVENT
1499 - (void)handleXcodeModEvent:(NSAppleEventDescriptor *)event
1500                  replyEvent:(NSAppleEventDescriptor *)reply
1502 #if 0
1503     // Xcode sends this event to query MacVim which open files have been
1504     // modified.
1505     ASLogDebug(@"reply:%@", reply);
1506     ASLogDebug(@"event:%@", event);
1508     NSEnumerator *e = [vimControllers objectEnumerator];
1509     id vc;
1510     while ((vc = [e nextObject])) {
1511         DescType type = [reply descriptorType];
1512         unsigned len = [[type data] length];
1513         NSMutableData *data = [NSMutableData data];
1515         [data appendBytes:&type length:sizeof(DescType)];
1516         [data appendBytes:&len length:sizeof(unsigned)];
1517         [data appendBytes:[reply data] length:len];
1519         [vc sendMessage:XcodeModMsgID data:data];
1520     }
1521 #endif
1523 #endif
1525 - (void)handleGetURLEvent:(NSAppleEventDescriptor *)event
1526                replyEvent:(NSAppleEventDescriptor *)reply
1528     NSString *urlString = [[event paramDescriptorForKeyword:keyDirectObject]
1529         stringValue];
1530     NSURL *url = [NSURL URLWithString:urlString];
1532     // We try to be compatible with TextMate's URL scheme here, as documented
1533     // at http://blog.macromates.com/2007/the-textmate-url-scheme/ . Currently,
1534     // this means that:
1535     //
1536     // The format is: mvim://open?<arguments> where arguments can be:
1537     //
1538     // * url â€” the actual file to open (i.e. a file://… URL), if you leave
1539     //         out this argument, the frontmost document is implied.
1540     // * line â€” line number to go to (one based).
1541     // * column â€” column number to go to (one based).
1542     //
1543     // Example: mvim://open?url=file:///etc/profile&line=20
1545     if ([[url host] isEqualToString:@"open"]) {
1546         NSMutableDictionary *dict = [NSMutableDictionary dictionary];
1548         // Parse query ("url=file://...&line=14") into a dictionary
1549         NSArray *queries = [[url query] componentsSeparatedByString:@"&"];
1550         NSEnumerator *enumerator = [queries objectEnumerator];
1551         NSString *param;
1552         while( param = [enumerator nextObject] ) {
1553             NSArray *arr = [param componentsSeparatedByString:@"="];
1554             if ([arr count] == 2) {
1555                 [dict setValue:[[arr lastObject]
1556                             stringByReplacingPercentEscapesUsingEncoding:
1557                                 NSUTF8StringEncoding]
1558                         forKey:[[arr objectAtIndex:0]
1559                             stringByReplacingPercentEscapesUsingEncoding:
1560                                 NSUTF8StringEncoding]];
1561             }
1562         }
1564         // Actually open the file.
1565         NSString *file = [dict objectForKey:@"url"];
1566         if (file != nil) {
1567             NSURL *fileUrl= [NSURL URLWithString:file];
1568             // TextMate only opens files that already exist.
1569             if ([fileUrl isFileURL]
1570                     && [[NSFileManager defaultManager] fileExistsAtPath:
1571                            [fileUrl path]]) {
1572                 // Strip 'file://' path, else application:openFiles: might think
1573                 // the file is not yet open.
1574                 NSArray *filenames = [NSArray arrayWithObject:[fileUrl path]];
1576                 // Look for the line and column options.
1577                 NSDictionary *args = nil;
1578                 NSString *line = [dict objectForKey:@"line"];
1579                 if (line) {
1580                     NSString *column = [dict objectForKey:@"column"];
1581                     if (column)
1582                         args = [NSDictionary dictionaryWithObjectsAndKeys:
1583                                 line, @"cursorLine",
1584                                 column, @"cursorColumn",
1585                                 nil];
1586                     else
1587                         args = [NSDictionary dictionaryWithObject:line
1588                                 forKey:@"cursorLine"];
1589                 }
1591                 [self openFiles:filenames withArguments:args];
1592             }
1593         }
1594     } else {
1595         NSAlert *alert = [[NSAlert alloc] init];
1596         [alert addButtonWithTitle:NSLocalizedString(@"OK",
1597             @"Dialog button")];
1599         [alert setMessageText:NSLocalizedString(@"Unknown URL Scheme",
1600             @"Unknown URL Scheme dialog, title")];
1601         [alert setInformativeText:[NSString stringWithFormat:NSLocalizedString(
1602             @"This version of MacVim does not support \"%@\""
1603             @" in its URL scheme.",
1604             @"Unknown URL Scheme dialog, text"),
1605             [url host]]];
1607         [alert setAlertStyle:NSWarningAlertStyle];
1608         [alert runModal];
1609         [alert release];
1610     }
1614 - (int)findLaunchingProcessWithoutArguments
1616     NSArray *keys = [pidArguments allKeysForObject:[NSNull null]];
1617     if ([keys count] > 0)
1618         return [[keys objectAtIndex:0] intValue];
1620     return -1;
1623 - (MMVimController *)findUnusedEditor
1625     NSEnumerator *e = [vimControllers objectEnumerator];
1626     id vc;
1627     while ((vc = [e nextObject])) {
1628         if ([[vc objectForVimStateKey:@"unusedEditor"] boolValue])
1629             return vc;
1630     }
1632     return nil;
1635 - (NSMutableDictionary *)extractArgumentsFromOdocEvent:
1636     (NSAppleEventDescriptor *)desc
1638     NSMutableDictionary *dict = [NSMutableDictionary dictionary];
1640     // 1. Extract ODB parameters (if any)
1641     NSAppleEventDescriptor *odbdesc = desc;
1642     if (![odbdesc paramDescriptorForKeyword:keyFileSender]) {
1643         // The ODB paramaters may hide inside the 'keyAEPropData' descriptor.
1644         odbdesc = [odbdesc paramDescriptorForKeyword:keyAEPropData];
1645         if (![odbdesc paramDescriptorForKeyword:keyFileSender])
1646             odbdesc = nil;
1647     }
1649     if (odbdesc) {
1650         NSAppleEventDescriptor *p =
1651                 [odbdesc paramDescriptorForKeyword:keyFileSender];
1652         if (p)
1653             [dict setObject:[NSNumber numberWithUnsignedInt:[p typeCodeValue]]
1654                      forKey:@"remoteID"];
1656         p = [odbdesc paramDescriptorForKeyword:keyFileCustomPath];
1657         if (p)
1658             [dict setObject:[p stringValue] forKey:@"remotePath"];
1660         p = [odbdesc paramDescriptorForKeyword:keyFileSenderToken];
1661         if (p) {
1662             [dict setObject:[NSNumber numberWithUnsignedLong:[p descriptorType]]
1663                      forKey:@"remoteTokenDescType"];
1664             [dict setObject:[p data] forKey:@"remoteTokenData"];
1665         }
1666     }
1668     // 2. Extract Xcode parameters (if any)
1669     NSAppleEventDescriptor *xcodedesc =
1670             [desc paramDescriptorForKeyword:keyAEPosition];
1671     if (xcodedesc) {
1672         NSRange range;
1673         MMSelectionRange *sr = (MMSelectionRange*)[[xcodedesc data] bytes];
1675         if (sr->lineNum < 0) {
1676             // Should select a range of lines.
1677             range.location = sr->startRange + 1;
1678             range.length = sr->endRange - sr->startRange + 1;
1679         } else {
1680             // Should only move cursor to a line.
1681             range.location = sr->lineNum + 1;
1682             range.length = 0;
1683         }
1685         [dict setObject:NSStringFromRange(range) forKey:@"selectionRange"];
1686     }
1688     // 3. Extract Spotlight search text (if any)
1689     NSAppleEventDescriptor *spotlightdesc = 
1690             [desc paramDescriptorForKeyword:keyAESearchText];
1691     if (spotlightdesc)
1692         [dict setObject:[spotlightdesc stringValue] forKey:@"searchText"];
1694     return dict;
1697 #ifdef MM_ENABLE_PLUGINS
1698 - (void)removePlugInMenu
1700     if ([plugInMenuItem menu])
1701         [[plugInMenuItem menu] removeItem:plugInMenuItem];
1704 - (void)addPlugInMenuToMenu:(NSMenu *)mainMenu
1706     NSMenu *windowsMenu = [mainMenu findWindowsMenu];
1708     if ([[plugInMenuItem submenu] numberOfItems] > 0) {
1709         int idx = windowsMenu ? [mainMenu indexOfItemWithSubmenu:windowsMenu]
1710                               : -1;
1711         if (idx > 0) {
1712             [mainMenu insertItem:plugInMenuItem atIndex:idx];
1713         } else {
1714             [mainMenu addItem:plugInMenuItem];
1715         }
1716     }
1718 #endif
1720 - (void)scheduleVimControllerPreloadAfterDelay:(NSTimeInterval)delay
1722     [self performSelector:@selector(preloadVimController:)
1723                withObject:nil
1724                afterDelay:delay];
1727 - (void)cancelVimControllerPreloadRequests
1729     [NSObject cancelPreviousPerformRequestsWithTarget:self
1730             selector:@selector(preloadVimController:)
1731               object:nil];
1734 - (void)preloadVimController:(id)sender
1736     // We only allow preloading of one Vim process at a time (to avoid hogging
1737     // CPU), so schedule another preload in a little while if necessary.
1738     if (-1 != preloadPid) {
1739         [self scheduleVimControllerPreloadAfterDelay:2];
1740         return;
1741     }
1743     if ([cachedVimControllers count] >= [self maxPreloadCacheSize])
1744         return;
1746     preloadPid = [self launchVimProcessWithArguments:
1747             [NSArray arrayWithObject:@"--mmwaitforack"]];
1750 - (int)maxPreloadCacheSize
1752     // The maximum number of Vim processes to keep in the cache can be
1753     // controlled via the user default "MMPreloadCacheSize".
1754     int maxCacheSize = [[NSUserDefaults standardUserDefaults]
1755             integerForKey:MMPreloadCacheSizeKey];
1756     if (maxCacheSize < 0) maxCacheSize = 0;
1757     else if (maxCacheSize > 10) maxCacheSize = 10;
1759     return maxCacheSize;
1762 - (MMVimController *)takeVimControllerFromCache
1764     // NOTE: After calling this message the backend corresponding to the
1765     // returned vim controller must be sent an acknowledgeConnection message,
1766     // else the vim process will be stuck.
1767     //
1768     // This method may return nil even though the cache might be non-empty; the
1769     // caller should handle this by starting a new Vim process.
1771     int i, count = [cachedVimControllers count];
1772     if (0 == count) return nil;
1774     // Locate the first Vim controller with up-to-date rc-files sourced.
1775     NSDate *rcDate = [self rcFilesModificationDate];
1776     for (i = 0; i < count; ++i) {
1777         MMVimController *vc = [cachedVimControllers objectAtIndex:i];
1778         NSDate *date = [vc creationDate];
1779         if ([date compare:rcDate] != NSOrderedAscending)
1780             break;
1781     }
1783     if (i > 0) {
1784         // Clear out cache entries whose vimrc/gvimrc files were sourced before
1785         // the latest modification date for those files.  This ensures that the
1786         // latest rc-files are always sourced for new windows.
1787         [self clearPreloadCacheWithCount:i];
1788     }
1790     if ([cachedVimControllers count] == 0) {
1791         [self scheduleVimControllerPreloadAfterDelay:2.0];
1792         return nil;
1793     }
1795     MMVimController *vc = [cachedVimControllers objectAtIndex:0];
1796     [vimControllers addObject:vc];
1797     [cachedVimControllers removeObjectAtIndex:0];
1798     [vc setIsPreloading:NO];
1800     // If the Vim process has finished loading then the window will displayed
1801     // now, otherwise it will be displayed when the OpenWindowMsgID message is
1802     // received.
1803     [[vc windowController] showWindow];
1805     // Since we've taken one controller from the cache we take the opportunity
1806     // to preload another.
1807     [self scheduleVimControllerPreloadAfterDelay:1];
1809     return vc;
1812 - (void)clearPreloadCacheWithCount:(int)count
1814     // Remove the 'count' first entries in the preload cache.  It is assumed
1815     // that objects are added/removed from the cache in a FIFO manner so that
1816     // this effectively clears the 'count' oldest entries.
1817     // If 'count' is negative, then the entire cache is cleared.
1819     if ([cachedVimControllers count] == 0 || count == 0)
1820         return;
1822     if (count < 0)
1823         count = [cachedVimControllers count];
1825     // Make sure the preloaded Vim processes get killed or they'll just hang
1826     // around being useless until MacVim is terminated.
1827     NSEnumerator *e = [cachedVimControllers objectEnumerator];
1828     MMVimController *vc;
1829     int n = count;
1830     while ((vc = [e nextObject]) && n-- > 0) {
1831         [[NSNotificationCenter defaultCenter] removeObserver:vc];
1832         [vc sendMessage:TerminateNowMsgID data:nil];
1834         // Since the preloaded processes were killed "prematurely" we have to
1835         // manually tell them to cleanup (it is not enough to simply release
1836         // them since deallocation and cleanup are separated).
1837         [vc cleanup];
1838     }
1840     n = count;
1841     while (n-- > 0 && [cachedVimControllers count] > 0)
1842         [cachedVimControllers removeObjectAtIndex:0];
1844     // There is a small delay before the Vim process actually exits so wait a
1845     // little before trying to reap the child process.  If the process still
1846     // hasn't exited after this wait it won't be reaped until the next time
1847     // reapChildProcesses: is called (but this should be harmless).
1848     [self performSelector:@selector(reapChildProcesses:)
1849                withObject:nil
1850                afterDelay:0.1];
1853 - (void)rebuildPreloadCache
1855     if ([self maxPreloadCacheSize] > 0) {
1856         [self clearPreloadCacheWithCount:-1];
1857         [self cancelVimControllerPreloadRequests];
1858         [self scheduleVimControllerPreloadAfterDelay:1.0];
1859     }
1862 - (NSDate *)rcFilesModificationDate
1864     // Check modification dates for ~/.vimrc and ~/.gvimrc and return the
1865     // latest modification date.  If ~/.vimrc does not exist, check ~/_vimrc
1866     // and similarly for gvimrc.
1867     // Returns distantPath if no rc files were found.
1869     NSDate *date = [NSDate distantPast];
1870     NSFileManager *fm = [NSFileManager defaultManager];
1872     NSString *path = [@"~/.vimrc" stringByExpandingTildeInPath];
1873     NSDictionary *attr = [fm fileAttributesAtPath:path traverseLink:YES];
1874     if (!attr) {
1875         path = [@"~/_vimrc" stringByExpandingTildeInPath];
1876         attr = [fm fileAttributesAtPath:path traverseLink:YES];
1877     }
1878     NSDate *modDate = [attr objectForKey:NSFileModificationDate];
1879     if (modDate)
1880         date = modDate;
1882     path = [@"~/.gvimrc" stringByExpandingTildeInPath];
1883     attr = [fm fileAttributesAtPath:path traverseLink:YES];
1884     if (!attr) {
1885         path = [@"~/_gvimrc" stringByExpandingTildeInPath];
1886         attr = [fm fileAttributesAtPath:path traverseLink:YES];
1887     }
1888     modDate = [attr objectForKey:NSFileModificationDate];
1889     if (modDate)
1890         date = [date laterDate:modDate];
1892     return date;
1895 - (BOOL)openVimControllerWithArguments:(NSDictionary *)arguments
1897     MMVimController *vc = [self findUnusedEditor];
1898     if (vc) {
1899         // Open files in an already open window.
1900         [[[vc windowController] window] makeKeyAndOrderFront:self];
1901         [vc passArguments:arguments];
1902     } else if ((vc = [self takeVimControllerFromCache])) {
1903         // Open files in a new window using a cached vim controller.  This
1904         // requires virtually no loading time so the new window will pop up
1905         // instantaneously.
1906         [vc passArguments:arguments];
1907         [[vc backendProxy] acknowledgeConnection];
1908     } else {
1909         // Open files in a launching Vim process or start a new process.  This
1910         // may take 1-2 seconds so there will be a visible delay before the
1911         // window appears on screen.
1912         int pid = [self findLaunchingProcessWithoutArguments];
1913         if (-1 == pid) {
1914             pid = [self launchVimProcessWithArguments:nil];
1915             if (-1 == pid)
1916                 return NO;
1917         }
1919         // TODO: If the Vim process fails to start, or if it changes PID,
1920         // then the memory allocated for these parameters will leak.
1921         // Ensure that this cannot happen or somehow detect it.
1923         if ([arguments count] > 0)
1924             [pidArguments setObject:arguments
1925                              forKey:[NSNumber numberWithInt:pid]];
1926     }
1928     return YES;
1931 - (void)activateWhenNextWindowOpens
1933     ASLogDebug(@"Activate MacVim when next window opens");
1934     shouldActivateWhenNextWindowOpens = YES;
1937 - (void)startWatchingVimDir
1939 #if (MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_4)
1940     if (fsEventStream)
1941         return;
1942     if (NULL == FSEventStreamStart)
1943         return; // FSEvent functions are weakly linked
1945     NSString *path = [@"~/.vim" stringByExpandingTildeInPath];
1946     NSArray *pathsToWatch = [NSArray arrayWithObject:path];
1948     fsEventStream = FSEventStreamCreate(NULL, &fsEventCallback, NULL,
1949             (CFArrayRef)pathsToWatch, kFSEventStreamEventIdSinceNow,
1950             MMEventStreamLatency, kFSEventStreamCreateFlagNone);
1952     FSEventStreamScheduleWithRunLoop(fsEventStream,
1953             [[NSRunLoop currentRunLoop] getCFRunLoop],
1954             kCFRunLoopDefaultMode);
1956     FSEventStreamStart(fsEventStream);
1957     ASLogDebug(@"Started FS event stream");
1958 #endif
1961 - (void)stopWatchingVimDir
1963 #if (MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_4)
1964     if (NULL == FSEventStreamStop)
1965         return; // FSEvent functions are weakly linked
1967     if (fsEventStream) {
1968         FSEventStreamStop(fsEventStream);
1969         FSEventStreamInvalidate(fsEventStream);
1970         FSEventStreamRelease(fsEventStream);
1971         fsEventStream = NULL;
1972         ASLogDebug(@"Stopped FS event stream");
1973     }
1974 #endif
1978 - (void)handleFSEvent
1980     [self clearPreloadCacheWithCount:-1];
1982     // Several FS events may arrive in quick succession so make sure to cancel
1983     // any previous preload requests before making a new one.
1984     [self cancelVimControllerPreloadRequests];
1985     [self scheduleVimControllerPreloadAfterDelay:0.5];
1988 - (void)loadDefaultFont
1990     // It is possible to set a user default to avoid loading the default font
1991     // (this cuts down on startup time).
1992     if (![[NSUserDefaults standardUserDefaults] boolForKey:MMLoadDefaultFontKey]
1993             || fontContainerRef) {
1994         ASLogInfo(@"Skip loading of the default font...");
1995         return;
1996     }
1998     ASLogInfo(@"Loading the default font...");
2000     // Load all fonts in the Resouces folder of the app bundle.
2001     NSString *fontsFolder = [[NSBundle mainBundle] resourcePath];
2002     if (fontsFolder) {
2003         NSURL *fontsURL = [NSURL fileURLWithPath:fontsFolder];
2004         if (fontsURL) {
2005             FSRef fsRef;
2006             CFURLGetFSRef((CFURLRef)fontsURL, &fsRef);
2008 #if (MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_4)
2009             // This is the font activation API for OS X 10.5.  Only compile
2010             // this code if we're building on OS X 10.5 or later.
2011             if (NULL != ATSFontActivateFromFileReference) { // Weakly linked
2012                 ATSFontActivateFromFileReference(&fsRef, kATSFontContextLocal,
2013                                                  kATSFontFormatUnspecified,
2014                                                  NULL, kATSOptionFlagsDefault,
2015                                                  &fontContainerRef);
2016             }
2017 #endif
2018 #if (MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_4)
2019             // The following font activation API was deprecated in OS X 10.5.
2020             // Don't compile this code unless we're targeting OS X 10.4.
2021             FSSpec fsSpec;
2022             if (fontContainerRef == 0 &&
2023                     FSGetCatalogInfo(&fsRef, kFSCatInfoNone, NULL, NULL,
2024                                      &fsSpec, NULL) == noErr) {
2025                 ATSFontActivateFromFileSpecification(&fsSpec,
2026                         kATSFontContextLocal, kATSFontFormatUnspecified, NULL,
2027                         kATSOptionFlagsDefault, &fontContainerRef);
2028             }
2029 #endif
2030         }
2031     }
2033     if (!fontContainerRef) {
2034         ASLogNotice(@"Failed to activate the default font (the app bundle "
2035                     "may be incomplete)");
2036     }
2039 - (int)executeInLoginShell:(NSString *)path arguments:(NSArray *)args
2041     // Start a login shell and execute the command 'path' with arguments 'args'
2042     // in the shell.  This ensures that user environment variables are set even
2043     // when MacVim was started from the Finder.
2045     int pid = -1;
2046     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
2048     // Determine which shell to use to execute the command.  The user
2049     // may decide which shell to use by setting a user default or the
2050     // $SHELL environment variable.
2051     NSString *shell = [ud stringForKey:MMLoginShellCommandKey];
2052     if (!shell || [shell length] == 0)
2053         shell = [[[NSProcessInfo processInfo] environment]
2054             objectForKey:@"SHELL"];
2055     if (!shell)
2056         shell = @"/bin/bash";
2058     // Bash needs the '-l' flag to launch a login shell.  The user may add
2059     // flags by setting a user default.
2060     NSString *shellArgument = [ud stringForKey:MMLoginShellArgumentKey];
2061     if (!shellArgument || [shellArgument length] == 0) {
2062         if ([[shell lastPathComponent] isEqual:@"bash"])
2063             shellArgument = @"-l";
2064         else
2065             shellArgument = nil;
2066     }
2068     // Build input string to pipe to the login shell.
2069     NSMutableString *input = [NSMutableString stringWithFormat:
2070             @"exec \"%@\"", path];
2071     if (args) {
2072         // Append all arguments, making sure they are properly quoted, even
2073         // when they contain single quotes.
2074         NSEnumerator *e = [args objectEnumerator];
2075         id obj;
2077         while ((obj = [e nextObject])) {
2078             NSMutableString *arg = [NSMutableString stringWithString:obj];
2079             [arg replaceOccurrencesOfString:@"'" withString:@"'\"'\"'"
2080                                     options:NSLiteralSearch
2081                                       range:NSMakeRange(0, [arg length])];
2082             [input appendFormat:@" '%@'", arg];
2083         }
2084     }
2086     // Build the argument vector used to start the login shell.
2087     NSString *shellArg0 = [NSString stringWithFormat:@"-%@",
2088              [shell lastPathComponent]];
2089     char *shellArgv[3] = { (char *)[shellArg0 UTF8String], NULL, NULL };
2090     if (shellArgument)
2091         shellArgv[1] = (char *)[shellArgument UTF8String];
2093     // Get the C string representation of the shell path before the fork since
2094     // we must not call Foundation functions after a fork.
2095     const char *shellPath = [shell fileSystemRepresentation];
2097     // Fork and execute the process.
2098     int ds[2];
2099     if (pipe(ds)) return -1;
2101     pid = fork();
2102     if (pid == -1) {
2103         return -1;
2104     } else if (pid == 0) {
2105         // Child process
2107         if (close(ds[1]) == -1) exit(255);
2108         if (dup2(ds[0], 0) == -1) exit(255);
2110         // Without the following call warning messages like this appear on the
2111         // console:
2112         //     com.apple.launchd[69] : Stray process with PGID equal to this
2113         //                             dead job: PID 1589 PPID 1 Vim
2114         setsid();
2116         execv(shellPath, shellArgv);
2118         // Never reached unless execv fails
2119         exit(255);
2120     } else {
2121         // Parent process
2122         if (close(ds[0]) == -1) return -1;
2124         // Send input to execute to the child process
2125         [input appendString:@"\n"];
2126         int bytes = [input lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
2128         if (write(ds[1], [input UTF8String], bytes) != bytes) return -1;
2129         if (close(ds[1]) == -1) return -1;
2131         ++numChildProcesses;
2132         ASLogDebug(@"new process pid=%d (count=%d)", pid, numChildProcesses);
2133     }
2135     return pid;
2138 - (void)reapChildProcesses:(id)sender
2140     // NOTE: numChildProcesses (currently) only counts the number of Vim
2141     // processes that have been started with executeInLoginShell::.  If other
2142     // processes are spawned this code may need to be adjusted (or
2143     // numChildProcesses needs to be incremented when such a process is
2144     // started).
2145     while (numChildProcesses > 0) {
2146         int status = 0;
2147         int pid = waitpid(-1, &status, WNOHANG);
2148         if (pid <= 0)
2149             break;
2151         ASLogDebug(@"Wait for pid=%d complete", pid);
2152         --numChildProcesses;
2153     }
2156 - (void)processInputQueues:(id)sender
2158     // NOTE: Because we use distributed objects it is quite possible for this
2159     // function to be re-entered.  This can cause all sorts of unexpected
2160     // problems so we guard against it here so that the rest of the code does
2161     // not need to worry about it.
2163     // The processing flag is > 0 if this function is already on the call
2164     // stack; < 0 if this function was also re-entered.
2165     if (processingFlag != 0) {
2166         ASLogDebug(@"BUSY!");
2167         processingFlag = -1;
2168         return;
2169     }
2171     // NOTE: Be _very_ careful that no exceptions can be raised between here
2172     // and the point at which 'processingFlag' is reset.  Otherwise the above
2173     // test could end up always failing and no input queues would ever be
2174     // processed!
2175     processingFlag = 1;
2177     // NOTE: New input may arrive while we're busy processing; we deal with
2178     // this by putting the current queue aside and creating a new input queue
2179     // for future input.
2180     NSDictionary *queues = inputQueues;
2181     inputQueues = [NSMutableDictionary new];
2183     // Pass each input queue on to the vim controller with matching
2184     // identifier (and note that it could be cached).
2185     NSEnumerator *e = [queues keyEnumerator];
2186     NSNumber *key;
2187     while ((key = [e nextObject])) {
2188         unsigned ukey = [key unsignedIntValue];
2189         int i = 0, count = [vimControllers count];
2190         for (i = 0; i < count; ++i) {
2191             MMVimController *vc = [vimControllers objectAtIndex:i];
2192             if (ukey == [vc identifier]) {
2193                 [vc processInputQueue:[queues objectForKey:key]]; // !exceptions
2194                 break;
2195             }
2196         }
2198         if (i < count) continue;
2200         count = [cachedVimControllers count];
2201         for (i = 0; i < count; ++i) {
2202             MMVimController *vc = [cachedVimControllers objectAtIndex:i];
2203             if (ukey == [vc identifier]) {
2204                 [vc processInputQueue:[queues objectForKey:key]]; // !exceptions
2205                 break;
2206             }
2207         }
2209         if (i == count) {
2210             ASLogWarn(@"No Vim controller for identifier=%d", ukey);
2211         }
2212     }
2214     [queues release];
2216     // If new input arrived while we were processing it would have been
2217     // blocked so we have to schedule it to be processed again.
2218     if (processingFlag < 0)
2219         [self performSelector:@selector(processInputQueues:)
2220                    withObject:nil
2221                    afterDelay:0
2222                       inModes:[NSArray arrayWithObjects:NSDefaultRunLoopMode,
2223                                             NSEventTrackingRunLoopMode, nil]];
2225     processingFlag = 0;
2228 - (void)addVimController:(MMVimController *)vc
2230     ASLogDebug(@"Add Vim controller pid=%d id=%d", [vc pid], [vc identifier]);
2232     int pid = [vc pid];
2233     NSNumber *pidKey = [NSNumber numberWithInt:pid];
2235     if (preloadPid == pid) {
2236         // This controller was preloaded, so add it to the cache and
2237         // schedule another vim process to be preloaded.
2238         preloadPid = -1;
2239         [vc setIsPreloading:YES];
2240         [cachedVimControllers addObject:vc];
2241         [self scheduleVimControllerPreloadAfterDelay:1];
2242     } else {
2243         [vimControllers addObject:vc];
2245         id args = [pidArguments objectForKey:pidKey];
2246         if (args && [NSNull null] != args)
2247             [vc passArguments:args];
2249         // HACK!  MacVim does not get activated if it is launched from the
2250         // terminal, so we forcibly activate here unless it is an untitled
2251         // window opening.  Untitled windows are treated differently, else
2252         // MacVim would steal the focus if another app was activated while the
2253         // untitled window was loading.
2254         if (!args || args != [NSNull null])
2255             [self activateWhenNextWindowOpens];
2257         if (args)
2258             [pidArguments removeObjectForKey:pidKey];
2259     }
2262 @end // MMAppController (Private)