Vim talks only to app controller
[MacVim.git] / src / MacVim / MMAppController.m
blob1d58140ca0d1c18bec1c1205622add12c306c1b0
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;
130 #ifdef MM_ENABLE_PLUGINS
131 - (void)removePlugInMenu;
132 - (void)addPlugInMenuToMenu:(NSMenu *)mainMenu;
133 #endif
134 @end
138 #if (MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_4)
139     static void
140 fsEventCallback(ConstFSEventStreamRef streamRef,
141                 void *clientCallBackInfo,
142                 size_t numEvents,
143                 void *eventPaths,
144                 const FSEventStreamEventFlags eventFlags[],
145                 const FSEventStreamEventId eventIds[])
147     [[MMAppController sharedInstance] handleFSEvent];
149 #endif
151 @implementation MMAppController
153 + (void)initialize
155     NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:
156         [NSNumber numberWithBool:NO],   MMNoWindowKey,
157         [NSNumber numberWithInt:64],    MMTabMinWidthKey,
158         [NSNumber numberWithInt:6*64],  MMTabMaxWidthKey,
159         [NSNumber numberWithInt:132],   MMTabOptimumWidthKey,
160         [NSNumber numberWithBool:YES],  MMShowAddTabButtonKey,
161         [NSNumber numberWithInt:2],     MMTextInsetLeftKey,
162         [NSNumber numberWithInt:1],     MMTextInsetRightKey,
163         [NSNumber numberWithInt:1],     MMTextInsetTopKey,
164         [NSNumber numberWithInt:1],     MMTextInsetBottomKey,
165         @"MMTypesetter",                MMTypesetterKey,
166         [NSNumber numberWithFloat:1],   MMCellWidthMultiplierKey,
167         [NSNumber numberWithFloat:-1],  MMBaselineOffsetKey,
168         [NSNumber numberWithBool:YES],  MMTranslateCtrlClickKey,
169         [NSNumber numberWithInt:0],     MMOpenInCurrentWindowKey,
170         [NSNumber numberWithBool:NO],   MMNoFontSubstitutionKey,
171         [NSNumber numberWithBool:YES],  MMLoginShellKey,
172         [NSNumber numberWithBool:NO],   MMAtsuiRendererKey,
173         [NSNumber numberWithInt:MMUntitledWindowAlways],
174                                         MMUntitledWindowKey,
175         [NSNumber numberWithBool:NO],   MMTexturedWindowKey,
176         [NSNumber numberWithBool:NO],   MMZoomBothKey,
177         @"",                            MMLoginShellCommandKey,
178         @"",                            MMLoginShellArgumentKey,
179         [NSNumber numberWithBool:YES],  MMDialogsTrackPwdKey,
180 #ifdef MM_ENABLE_PLUGINS
181         [NSNumber numberWithBool:YES],  MMShowLeftPlugInContainerKey,
182 #endif
183         [NSNumber numberWithInt:3],     MMOpenLayoutKey,
184         [NSNumber numberWithBool:NO],   MMVerticalSplitKey,
185         [NSNumber numberWithInt:0],     MMPreloadCacheSizeKey,
186         [NSNumber numberWithInt:0],     MMLastWindowClosedBehaviorKey,
187         [NSNumber numberWithBool:YES],  MMLoadDefaultFontKey,
188         nil];
190     [[NSUserDefaults standardUserDefaults] registerDefaults:dict];
192     NSArray *types = [NSArray arrayWithObject:NSStringPboardType];
193     [NSApp registerServicesMenuSendTypes:types returnTypes:types];
195     // NOTE: Set the current directory to user's home directory, otherwise it
196     // will default to the root directory.  (This matters since new Vim
197     // processes inherit MacVim's environment variables.)
198     [[NSFileManager defaultManager] changeCurrentDirectoryPath:
199             NSHomeDirectory()];
202 - (id)init
204     if (!(self = [super init])) return nil;
206     [self loadDefaultFont];
208     vimControllers = [NSMutableArray new];
209     cachedVimControllers = [NSMutableArray new];
210     preloadPid = -1;
211     pidArguments = [NSMutableDictionary new];
212     inputQueues = [NSMutableDictionary new];
214 #ifdef MM_ENABLE_PLUGINS
215     NSString *plugInTitle = NSLocalizedString(@"Plug-In",
216                                               @"Plug-In menu title");
217     plugInMenuItem = [[NSMenuItem alloc] initWithTitle:plugInTitle
218                                                 action:NULL
219                                          keyEquivalent:@""];
220     NSMenu *submenu = [[NSMenu alloc] initWithTitle:plugInTitle];
221     [plugInMenuItem setSubmenu:submenu];
222     [submenu release];
223 #endif
225     // NOTE: Do not use the default connection since the Logitech Control
226     // Center (LCC) input manager steals and this would cause MacVim to
227     // never open any windows.  (This is a bug in LCC but since they are
228     // unlikely to fix it, we graciously give them the default connection.)
229     connection = [[NSConnection alloc] initWithReceivePort:[NSPort port]
230                                                   sendPort:nil];
231     [connection setRootObject:self];
232     [connection setRequestTimeout:MMRequestTimeout];
233     [connection setReplyTimeout:MMReplyTimeout];
235     // NOTE!  If the name of the connection changes here it must also be
236     // updated in MMBackend.m.
237     NSString *name = [NSString stringWithFormat:@"%@-connection",
238              [[NSBundle mainBundle] bundlePath]];
239     //NSLog(@"Registering connection with name '%@'", name);
240     if (![connection registerName:name]) {
241         NSLog(@"FATAL ERROR: Failed to register connection with name '%@'",
242                 name);
243         [connection release];  connection = nil;
244     }
246     return self;
249 - (void)dealloc
251     //NSLog(@"MMAppController dealloc");
253     [connection release];  connection = nil;
254     [inputQueues release];  inputQueues = nil;
255     [pidArguments release];  pidArguments = nil;
256     [vimControllers release];  vimControllers = nil;
257     [cachedVimControllers release];  cachedVimControllers = nil;
258     [openSelectionString release];  openSelectionString = nil;
259     [recentFilesMenuItem release];  recentFilesMenuItem = nil;
260     [defaultMainMenu release];  defaultMainMenu = nil;
261 #ifdef MM_ENABLE_PLUGINS
262     [plugInMenuItem release];  plugInMenuItem = nil;
263 #endif
264     [appMenuItemTemplate release];  appMenuItemTemplate = nil;
266     [super dealloc];
269 - (void)applicationWillFinishLaunching:(NSNotification *)notification
271     // Remember the default menu so that it can be restored if the user closes
272     // all editor windows.
273     defaultMainMenu = [[NSApp mainMenu] retain];
275     // Store a copy of the default app menu so we can use this as a template
276     // for all other menus.  We make a copy here because the "Services" menu
277     // will not yet have been populated at this time.  If we don't we get
278     // problems trying to set key equivalents later on because they might clash
279     // with items on the "Services" menu.
280     appMenuItemTemplate = [defaultMainMenu itemAtIndex:0];
281     appMenuItemTemplate = [appMenuItemTemplate copy];
283     // Set up the "Open Recent" menu. See
284     //   http://lapcatsoftware.com/blog/2007/07/10/
285     //     working-without-a-nib-part-5-open-recent-menu/
286     // and
287     //   http://www.cocoabuilder.com/archive/message/cocoa/2007/8/15/187793
288     // for more information.
289     //
290     // The menu itself is created in MainMenu.nib but we still seem to have to
291     // hack around a bit to get it to work.  (This has to be done in
292     // applicationWillFinishLaunching at the latest, otherwise it doesn't
293     // work.)
294     NSMenu *fileMenu = [defaultMainMenu findFileMenu];
295     if (fileMenu) {
296         int idx = [fileMenu indexOfItemWithAction:@selector(fileOpen:)];
297         if (idx >= 0 && idx+1 < [fileMenu numberOfItems])
299         recentFilesMenuItem = [fileMenu itemWithTitle:@"Open Recent"];
300         [[recentFilesMenuItem submenu] performSelector:@selector(_setMenuName:)
301                                         withObject:@"NSRecentDocumentsMenu"];
303         // Note: The "Recent Files" menu must be moved around since there is no
304         // -[NSApp setRecentFilesMenu:] method.  We keep a reference to it to
305         // facilitate this move (see setMainMenu: below).
306         [recentFilesMenuItem retain];
307     }
309 #if MM_HANDLE_XCODE_MOD_EVENT
310     [[NSAppleEventManager sharedAppleEventManager]
311             setEventHandler:self
312                 andSelector:@selector(handleXcodeModEvent:replyEvent:)
313               forEventClass:'KAHL'
314                  andEventID:'MOD '];
315 #endif
317     // Register 'mvim://' URL handler
318     [[NSAppleEventManager sharedAppleEventManager]
319             setEventHandler:self
320                 andSelector:@selector(handleGetURLEvent:replyEvent:)
321               forEventClass:kInternetEventClass
322                  andEventID:kAEGetURL];
325 - (void)applicationDidFinishLaunching:(NSNotification *)notification
327     [NSApp setServicesProvider:self];
328 #ifdef MM_ENABLE_PLUGINS
329     [[MMPlugInManager sharedManager] loadAllPlugIns];
330 #endif
332     if ([self maxPreloadCacheSize] > 0) {
333         [self scheduleVimControllerPreloadAfterDelay:2];
334         [self startWatchingVimDir];
335     }
338 - (BOOL)applicationShouldOpenUntitledFile:(NSApplication *)sender
340     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
341     NSAppleEventManager *aem = [NSAppleEventManager sharedAppleEventManager];
342     NSAppleEventDescriptor *desc = [aem currentAppleEvent];
344     // The user default MMUntitledWindow can be set to control whether an
345     // untitled window should open on 'Open' and 'Reopen' events.
346     int untitledWindowFlag = [ud integerForKey:MMUntitledWindowKey];
348     BOOL isAppOpenEvent = [desc eventID] == kAEOpenApplication;
349     if (isAppOpenEvent && (untitledWindowFlag & MMUntitledWindowOnOpen) == 0)
350         return NO;
352     BOOL isAppReopenEvent = [desc eventID] == kAEReopenApplication;
353     if (isAppReopenEvent
354             && (untitledWindowFlag & MMUntitledWindowOnReopen) == 0)
355         return NO;
357     // When a process is started from the command line, the 'Open' event may
358     // contain a parameter to surpress the opening of an untitled window.
359     desc = [desc paramDescriptorForKeyword:keyAEPropData];
360     desc = [desc paramDescriptorForKeyword:keyMMUntitledWindow];
361     if (desc && ![desc booleanValue])
362         return NO;
364     // Never open an untitled window if there is at least one open window or if
365     // there are processes that are currently launching.
366     if ([vimControllers count] > 0 || [pidArguments count] > 0)
367         return NO;
369     // NOTE!  This way it possible to start the app with the command-line
370     // argument '-nowindow yes' and no window will be opened by default but
371     // this argument will only be heeded when the application is opening.
372     if (isAppOpenEvent && [ud boolForKey:MMNoWindowKey] == YES)
373         return NO;
375     return YES;
378 - (BOOL)applicationOpenUntitledFile:(NSApplication *)sender
380     [self newWindow:self];
381     return YES;
384 - (void)application:(NSApplication *)sender openFiles:(NSArray *)filenames
386     // Extract ODB/Xcode/Spotlight parameters from the current Apple event,
387     // sort the filenames, and then let openFiles:withArguments: do the heavy
388     // lifting.
390     if (!(filenames && [filenames count] > 0))
391         return;
393     // Sort filenames since the Finder doesn't take care in preserving the
394     // order in which files are selected anyway (and "sorted" is more
395     // predictable than "random").
396     if ([filenames count] > 1)
397         filenames = [filenames sortedArrayUsingSelector:
398                 @selector(localizedCompare:)];
400     // Extract ODB/Xcode/Spotlight parameters from the current Apple event
401     NSMutableDictionary *arguments = [self extractArgumentsFromOdocEvent:
402             [[NSAppleEventManager sharedAppleEventManager] currentAppleEvent]];
404     if ([self openFiles:filenames withArguments:arguments]) {
405         [NSApp replyToOpenOrPrint:NSApplicationDelegateReplySuccess];
406     } else {
407         // TODO: Notify user of failure?
408         [NSApp replyToOpenOrPrint:NSApplicationDelegateReplyFailure];
409     }
412 - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender
414     return (MMTerminateWhenLastWindowClosed ==
415             [[NSUserDefaults standardUserDefaults]
416                 integerForKey:MMLastWindowClosedBehaviorKey]);
419 - (NSApplicationTerminateReply)applicationShouldTerminate:
420     (NSApplication *)sender
422     // TODO: Follow Apple's guidelines for 'Graceful Application Termination'
423     // (in particular, allow user to review changes and save).
424     int reply = NSTerminateNow;
425     BOOL modifiedBuffers = NO;
427     // Go through windows, checking for modified buffers.  (Each Vim process
428     // tells MacVim when any buffer has been modified and MacVim sets the
429     // 'documentEdited' flag of the window correspondingly.)
430     NSEnumerator *e = [[NSApp windows] objectEnumerator];
431     id window;
432     while ((window = [e nextObject])) {
433         if ([window isDocumentEdited]) {
434             modifiedBuffers = YES;
435             break;
436         }
437     }
439     if (modifiedBuffers) {
440         NSAlert *alert = [[NSAlert alloc] init];
441         [alert setAlertStyle:NSWarningAlertStyle];
442         [alert addButtonWithTitle:NSLocalizedString(@"Quit",
443                 @"Dialog button")];
444         [alert addButtonWithTitle:NSLocalizedString(@"Cancel",
445                 @"Dialog button")];
446         [alert setMessageText:NSLocalizedString(@"Quit without saving?",
447                 @"Quit dialog with changed buffers, title")];
448         [alert setInformativeText:NSLocalizedString(
449                 @"There are modified buffers, "
450                 "if you quit now all changes will be lost.  Quit anyway?",
451                 @"Quit dialog with changed buffers, text")];
453         if ([alert runModal] != NSAlertFirstButtonReturn)
454             reply = NSTerminateCancel;
456         [alert release];
457     } else {
458         // No unmodified buffers, but give a warning if there are multiple
459         // windows and/or tabs open.
460         int numWindows = [vimControllers count];
461         int numTabs = 0;
463         // Count the number of open tabs
464         e = [vimControllers objectEnumerator];
465         id vc;
466         while ((vc = [e nextObject]))
467             numTabs += [[vc objectForVimStateKey:@"numTabs"] intValue];
469         if (numWindows > 1 || numTabs > 1) {
470             NSAlert *alert = [[NSAlert alloc] init];
471             [alert setAlertStyle:NSWarningAlertStyle];
472             [alert addButtonWithTitle:NSLocalizedString(@"Quit",
473                     @"Dialog button")];
474             [alert addButtonWithTitle:NSLocalizedString(@"Cancel",
475                     @"Dialog button")];
476             [alert setMessageText:NSLocalizedString(
477                     @"Are you sure you want to quit MacVim?",
478                     @"Quit dialog with no changed buffers, title")];
480             NSString *info = nil;
481             if (numWindows > 1) {
482                 if (numTabs > numWindows)
483                     info = [NSString stringWithFormat:NSLocalizedString(
484                             @"There are %d windows open in MacVim, with a "
485                             "total of %d tabs. Do you want to quit anyway?",
486                             @"Quit dialog with no changed buffers, text"),
487                          numWindows, numTabs];
488                 else
489                     info = [NSString stringWithFormat:NSLocalizedString(
490                             @"There are %d windows open in MacVim. "
491                             "Do you want to quit anyway?",
492                             @"Quit dialog with no changed buffers, text"),
493                         numWindows];
495             } else {
496                 info = [NSString stringWithFormat:NSLocalizedString(
497                         @"There are %d tabs open in MacVim. "
498                         "Do you want to quit anyway?",
499                         @"Quit dialog with no changed buffers, text"), 
500                      numTabs];
501             }
503             [alert setInformativeText:info];
505             if ([alert runModal] != NSAlertFirstButtonReturn)
506                 reply = NSTerminateCancel;
508             [alert release];
509         }
510     }
513     // Tell all Vim processes to terminate now (otherwise they'll leave swap
514     // files behind).
515     if (NSTerminateNow == reply) {
516         e = [vimControllers objectEnumerator];
517         id vc;
518         while ((vc = [e nextObject])) {
519             //NSLog(@"Terminate pid=%d", [vc pid]);
520             [vc sendMessage:TerminateNowMsgID data:nil];
521         }
523         e = [cachedVimControllers objectEnumerator];
524         while ((vc = [e nextObject])) {
525             //NSLog(@"Terminate pid=%d (cached)", [vc pid]);
526             [vc sendMessage:TerminateNowMsgID data:nil];
527         }
529         // If a Vim process is being preloaded as we quit we have to forcibly
530         // kill it since we have not established a connection yet.
531         if (preloadPid > 0) {
532             //NSLog(@"INCOMPLETE preloaded process: preloadPid=%d", preloadPid);
533             kill(preloadPid, SIGKILL);
534         }
536         // If a Vim process was loading as we quit we also have to kill it.
537         e = [[pidArguments allKeys] objectEnumerator];
538         NSNumber *pidKey;
539         while ((pidKey = [e nextObject])) {
540             //NSLog(@"INCOMPLETE process: pid=%d", [pidKey intValue]);
541             kill([pidKey intValue], SIGKILL);
542         }
544         // Sleep a little to allow all the Vim processes to exit.
545         usleep(10000);
546     }
548     return reply;
551 - (void)applicationWillTerminate:(NSNotification *)notification
553     [self stopWatchingVimDir];
555 #ifdef MM_ENABLE_PLUGINS
556     [[MMPlugInManager sharedManager] unloadAllPlugIns];
557 #endif
559 #if MM_HANDLE_XCODE_MOD_EVENT
560     [[NSAppleEventManager sharedAppleEventManager]
561             removeEventHandlerForEventClass:'KAHL'
562                                  andEventID:'MOD '];
563 #endif
565     // This will invalidate all connections (since they were spawned from this
566     // connection).
567     [connection invalidate];
569     // Deactivate the font we loaded from the app bundle.
570     // NOTE: This can take quite a while (~500 ms), so termination will be
571     // noticeably faster if loading of the default font is disabled.
572     if (fontContainerRef) {
573         ATSFontDeactivate(fontContainerRef, NULL, kATSOptionFlagsDefault);
574         fontContainerRef = 0;
575     }
577     [NSApp setDelegate:nil];
579     // Try to wait for all child processes to avoid leaving zombies behind (but
580     // don't wait around for too long).
581     NSDate *timeOutDate = [NSDate dateWithTimeIntervalSinceNow:2];
582     while ([timeOutDate timeIntervalSinceNow] > 0) {
583         [self reapChildProcesses:nil];
584         if (numChildProcesses <= 0)
585             break;
587         //NSLog(@"%d processes still left, sleep a bit...", numChildProcesses);
589         // Run in NSConnectionReplyMode while waiting instead of calling e.g.
590         // usleep().  Otherwise incoming messages may clog up the DO queues and
591         // the outgoing TerminateNowMsgID sent earlier never reaches the Vim
592         // process.
593         // This has at least one side-effect, namely we may receive the
594         // annoying "dropping incoming DO message".  (E.g. this may happen if
595         // you quickly hit Cmd-n several times in a row and then immediately
596         // press Cmd-q, Enter.)
597         while (CFRunLoopRunInMode((CFStringRef)NSConnectionReplyMode,
598                 0.05, true) == kCFRunLoopRunHandledSource)
599             ;   // do nothing
600     }
602     if (numChildProcesses > 0)
603         NSLog(@"%d ZOMBIES left behind", numChildProcesses);
606 + (MMAppController *)sharedInstance
608     // Note: The app controller is a singleton which is instantiated in
609     // MainMenu.nib where it is also connected as the delegate of NSApp.
610     id delegate = [NSApp delegate];
611     return [delegate isKindOfClass:self] ? (MMAppController*)delegate : nil;
614 - (NSMenu *)defaultMainMenu
616     return defaultMainMenu;
619 - (NSMenuItem *)appMenuItemTemplate
621     return appMenuItemTemplate;
624 - (void)removeVimController:(id)controller
626     int idx = [vimControllers indexOfObject:controller];
627     if (NSNotFound == idx)
628         return;
630     [controller cleanup];
632     [vimControllers removeObjectAtIndex:idx];
634     if (![vimControllers count]) {
635         // The last editor window just closed so restore the main menu back to
636         // its default state (which is defined in MainMenu.nib).
637         [self setMainMenu:defaultMainMenu];
639         BOOL hide = (MMHideWhenLastWindowClosed ==
640                     [[NSUserDefaults standardUserDefaults]
641                         integerForKey:MMLastWindowClosedBehaviorKey]);
642         if (hide)
643             [NSApp hide:self];
644     }
646     // There is a small delay before the Vim process actually exits so wait a
647     // little before trying to reap the child process.  If the process still
648     // hasn't exited after this wait it won't be reaped until the next time
649     // reapChildProcesses: is called (but this should be harmless).
650     [self performSelector:@selector(reapChildProcesses:)
651                withObject:nil
652                afterDelay:0.1];
655 - (void)windowControllerWillOpen:(MMWindowController *)windowController
657     NSPoint topLeft = NSZeroPoint;
658     NSWindow *topWin = [[[self topmostVimController] windowController] window];
659     NSWindow *win = [windowController window];
661     if (!win) return;
663     // If there is a window belonging to a Vim process, cascade from it,
664     // otherwise use the autosaved window position (if any).
665     if (topWin) {
666         NSRect frame = [topWin frame];
667         topLeft = NSMakePoint(frame.origin.x, NSMaxY(frame));
668     } else {
669         NSString *topLeftString = [[NSUserDefaults standardUserDefaults]
670             stringForKey:MMTopLeftPointKey];
671         if (topLeftString)
672             topLeft = NSPointFromString(topLeftString);
673     }
675     if (!NSEqualPoints(topLeft, NSZeroPoint)) {
676         NSPoint oldTopLeft = topLeft;
677         if (topWin)
678             topLeft = [win cascadeTopLeftFromPoint:topLeft];
680         [win setFrameTopLeftPoint:topLeft];
682         if ([win screen]) {
683             NSPoint screenOrigin = [[win screen] frame].origin;
684             if ([win frame].origin.y < screenOrigin.y) {
685                 // Try to avoid shifting the new window downwards if it means
686                 // that the bottom of the window will be off the screen.  E.g.
687                 // if the user has set windows to open maximized in the
688                 // vertical direction then the new window will cascade
689                 // horizontally only.
690                 topLeft.y = oldTopLeft.y;
691                 [win setFrameTopLeftPoint:topLeft];
692             }
694             if ([win frame].origin.y < screenOrigin.y) {
695                 // Move the window to the top of the screen if the bottom of
696                 // the window is still obscured.
697                 topLeft.y = NSMaxY([[win screen] frame]);
698                 [win setFrameTopLeftPoint:topLeft];
699             }
700         } else {
701             NSLog(@"[%s] WINDOW NOT ON SCREEN, don't constrain position", _cmd);
702         }
703     }
705     if (1 == [vimControllers count]) {
706         // The first window autosaves its position.  (The autosaving
707         // features of Cocoa are not used because we need more control over
708         // what is autosaved and when it is restored.)
709         [windowController setWindowAutosaveKey:MMTopLeftPointKey];
710     }
712     if (openSelectionString) {
713         // TODO: Pass this as a parameter instead!  Get rid of
714         // 'openSelectionString' etc.
715         //
716         // There is some text to paste into this window as a result of the
717         // services menu "Open selection ..." being used.
718         [[windowController vimController] dropString:openSelectionString];
719         [openSelectionString release];
720         openSelectionString = nil;
721     }
723     if (shouldActivateWhenNextWindowOpens) {
724         [NSApp activateIgnoringOtherApps:YES];
725         shouldActivateWhenNextWindowOpens = NO;
726     }
729 - (void)setMainMenu:(NSMenu *)mainMenu
731     if ([NSApp mainMenu] == mainMenu) return;
733     // If the new menu has a "Recent Files" dummy item, then swap the real item
734     // for the dummy.  We are forced to do this since Cocoa initializes the
735     // "Recent Files" menu and there is no way to simply point Cocoa to a new
736     // item each time the menus are swapped.
737     NSMenu *fileMenu = [mainMenu findFileMenu];
738     if (recentFilesMenuItem && fileMenu) {
739         int dummyIdx =
740                 [fileMenu indexOfItemWithAction:@selector(recentFilesDummy:)];
741         if (dummyIdx >= 0) {
742             NSMenuItem *dummyItem = [[fileMenu itemAtIndex:dummyIdx] retain];
743             [fileMenu removeItemAtIndex:dummyIdx];
745             NSMenu *recentFilesParentMenu = [recentFilesMenuItem menu];
746             int idx = [recentFilesParentMenu indexOfItem:recentFilesMenuItem];
747             if (idx >= 0) {
748                 [[recentFilesMenuItem retain] autorelease];
749                 [recentFilesParentMenu removeItemAtIndex:idx];
750                 [recentFilesParentMenu insertItem:dummyItem atIndex:idx];
751             }
753             [fileMenu insertItem:recentFilesMenuItem atIndex:dummyIdx];
754             [dummyItem release];
755         }
756     }
758     // Now set the new menu.  Notice that we keep one menu for each editor
759     // window since each editor can have its own set of menus.  When swapping
760     // menus we have to tell Cocoa where the new "MacVim", "Windows", and
761     // "Services" menu are.
762     [NSApp setMainMenu:mainMenu];
764     // Setting the "MacVim" (or "Application") menu ensures that it is typeset
765     // in boldface.  (The setAppleMenu: method used to be public but is now
766     // private so this will have to be considered a bit of a hack!)
767     NSMenu *appMenu = [mainMenu findApplicationMenu];
768     [NSApp performSelector:@selector(setAppleMenu:) withObject:appMenu];
770     NSMenu *servicesMenu = [mainMenu findServicesMenu];
771     [NSApp setServicesMenu:servicesMenu];
773     NSMenu *windowsMenu = [mainMenu findWindowsMenu];
774     if (windowsMenu) {
775         // Cocoa isn't clever enough to get rid of items it has added to the
776         // "Windows" menu so we have to do it ourselves otherwise there will be
777         // multiple menu items for each window in the "Windows" menu.
778         //   This code assumes that the only items Cocoa add are ones which
779         // send off the action makeKeyAndOrderFront:.  (Cocoa will not add
780         // another separator item if the last item on the "Windows" menu
781         // already is a separator, so we needen't worry about separators.)
782         int i, count = [windowsMenu numberOfItems];
783         for (i = count-1; i >= 0; --i) {
784             NSMenuItem *item = [windowsMenu itemAtIndex:i];
785             if ([item action] == @selector(makeKeyAndOrderFront:))
786                 [windowsMenu removeItem:item];
787         }
788     }
789     [NSApp setWindowsMenu:windowsMenu];
791 #ifdef MM_ENABLE_PLUGINS
792     // Move plugin menu from old to new main menu.
793     [self removePlugInMenu];
794     [self addPlugInMenuToMenu:mainMenu];
795 #endif
798 - (NSArray *)filterOpenFiles:(NSArray *)filenames
800     return [self filterOpenFiles:filenames openFilesDict:nil];
803 - (BOOL)openFiles:(NSArray *)filenames withArguments:(NSDictionary *)args
805     // Opening files works like this:
806     //  a) filter out any already open files
807     //  b) open any remaining files
808     //
809     // A file is opened in an untitled window if there is one (it may be
810     // currently launching, or it may already be visible), otherwise a new
811     // window is opened.
812     //
813     // Each launching Vim process has a dictionary of arguments that are passed
814     // to the process when in checks in (via connectBackend:pid:).  The
815     // arguments for each launching process can be looked up by its PID (in the
816     // pidArguments dictionary).
818     NSMutableDictionary *arguments = (args ? [[args mutableCopy] autorelease]
819                                            : [NSMutableDictionary dictionary]);
821     //
822     // a) Filter out any already open files
823     //
824     NSString *firstFile = [filenames objectAtIndex:0];
825     MMVimController *firstController = nil;
826     NSDictionary *openFilesDict = nil;
827     filenames = [self filterOpenFiles:filenames openFilesDict:&openFilesDict];
829     // Pass arguments to vim controllers that had files open.
830     id key;
831     NSEnumerator *e = [openFilesDict keyEnumerator];
833     // (Indicate that we do not wish to open any files at the moment.)
834     [arguments setObject:[NSNumber numberWithBool:YES] forKey:@"dontOpen"];
836     while ((key = [e nextObject])) {
837         NSArray *files = [openFilesDict objectForKey:key];
838         [arguments setObject:files forKey:@"filenames"];
840         MMVimController *vc = [key pointerValue];
841         [vc passArguments:arguments];
843         // If this controller holds the first file, then remember it for later.
844         if ([files containsObject:firstFile])
845             firstController = vc;
846     }
848     // The meaning of "layout" is defined by the WIN_* defines in main.c.
849     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
850     int layout = [ud integerForKey:MMOpenLayoutKey];
851     BOOL splitVert = [ud boolForKey:MMVerticalSplitKey];
852     BOOL openInCurrentWindow = [ud boolForKey:MMOpenInCurrentWindowKey];
854     if (splitVert && MMLayoutHorizontalSplit == layout)
855         layout = MMLayoutVerticalSplit;
856     if (layout < 0 || (layout > MMLayoutTabs && openInCurrentWindow))
857         layout = MMLayoutTabs;
859     if ([filenames count] == 0) {
860         // Raise the window containing the first file that was already open,
861         // and make sure that the tab containing that file is selected.  Only
862         // do this when there are no more files to open, otherwise sometimes
863         // the window with 'firstFile' will be raised, other times it might be
864         // the window that will open with the files in the 'filenames' array.
865         firstFile = [firstFile stringByEscapingSpecialFilenameCharacters];
867         NSString *bufCmd = @"tab sb";
868         switch (layout) {
869             case MMLayoutHorizontalSplit: bufCmd = @"sb"; break;
870             case MMLayoutVerticalSplit:   bufCmd = @"vert sb"; break;
871             case MMLayoutArglist:         bufCmd = @"b"; break;
872         }
874         NSString *input = [NSString stringWithFormat:@"<C-\\><C-N>"
875                 ":let oldswb=&swb|let &swb=\"useopen,usetab\"|"
876                 "%@ %@|let &swb=oldswb|unl oldswb|"
877                 "cal foreground()<CR>", bufCmd, firstFile];
879         [firstController addVimInput:input];
881         return YES;
882     }
884     // Add filenames to "Recent Files" menu, unless they are being edited
885     // remotely (using ODB).
886     if ([arguments objectForKey:@"remoteID"] == nil) {
887         [[NSDocumentController sharedDocumentController]
888                 noteNewRecentFilePaths:filenames];
889     }
891     //
892     // b) Open any remaining files
893     //
895     [arguments setObject:[NSNumber numberWithInt:layout] forKey:@"layout"];
896     [arguments setObject:filenames forKey:@"filenames"];
897     // (Indicate that files should be opened from now on.)
898     [arguments setObject:[NSNumber numberWithBool:NO] forKey:@"dontOpen"];
900     MMVimController *vc;
901     if (openInCurrentWindow && (vc = [self topmostVimController])) {
902         // Open files in an already open window.
903         [[[vc windowController] window] makeKeyAndOrderFront:self];
904         [vc passArguments:arguments];
905         return YES;
906     }
908     BOOL openOk = YES;
909     int numFiles = [filenames count];
910     if (MMLayoutWindows == layout && numFiles > 1) {
911         // Open one file at a time in a new window, but don't open too many at
912         // once (at most cap+1 windows will open).  If the user has increased
913         // the preload cache size we'll take that as a hint that more windows
914         // should be able to open at once.
915         int cap = [self maxPreloadCacheSize] - 1;
916         if (cap < 4) cap = 4;
917         if (cap > numFiles) cap = numFiles;
919         int i;
920         for (i = 0; i < cap; ++i) {
921             NSArray *a = [NSArray arrayWithObject:[filenames objectAtIndex:i]];
922             [arguments setObject:a forKey:@"filenames"];
924             // NOTE: We have to copy the args since we'll mutate them in the
925             // next loop and the below call may retain the arguments while
926             // waiting for a process to start.
927             NSDictionary *args = [[arguments copy] autorelease];
929             openOk = [self openVimControllerWithArguments:args];
930             if (!openOk) break;
931         }
933         // Open remaining files in tabs in a new window.
934         if (openOk && numFiles > cap) {
935             NSRange range = { i, numFiles-cap };
936             NSArray *a = [filenames subarrayWithRange:range];
937             [arguments setObject:a forKey:@"filenames"];
938             [arguments setObject:[NSNumber numberWithInt:MMLayoutTabs]
939                           forKey:@"layout"];
941             openOk = [self openVimControllerWithArguments:arguments];
942         }
943     } else {
944         // Open all files at once.
945         openOk = [self openVimControllerWithArguments:arguments];
946     }
948     return openOk;
951 #ifdef MM_ENABLE_PLUGINS
952 - (void)addItemToPlugInMenu:(NSMenuItem *)item
954     NSMenu *menu = [plugInMenuItem submenu];
955     [menu addItem:item];
956     if ([menu numberOfItems] == 1)
957         [self addPlugInMenuToMenu:[NSApp mainMenu]];
960 - (void)removeItemFromPlugInMenu:(NSMenuItem *)item
962     NSMenu *menu = [plugInMenuItem submenu];
963     [menu removeItem:item];
964     if ([menu numberOfItems] == 0)
965         [self removePlugInMenu];
967 #endif
969 - (IBAction)newWindow:(id)sender
971     // A cached controller requires no loading times and results in the new
972     // window popping up instantaneously.  If the cache is empty it may take
973     // 1-2 seconds to start a new Vim process.
974     MMVimController *vc = [self takeVimControllerFromCache];
975     if (vc) {
976         [[vc backendProxy] acknowledgeConnection];
977     } else {
978         [self launchVimProcessWithArguments:nil];
979     }
982 - (IBAction)newWindowAndActivate:(id)sender
984     [self activateWhenNextWindowOpens];
985     [self newWindow:sender];
988 - (IBAction)fileOpen:(id)sender
990     NSString *dir = nil;
991     BOOL trackPwd = [[NSUserDefaults standardUserDefaults]
992             boolForKey:MMDialogsTrackPwdKey];
993     if (trackPwd) {
994         MMVimController *vc = [self keyVimController];
995         if (vc) dir = [vc objectForVimStateKey:@"pwd"];
996     }
998     NSOpenPanel *panel = [NSOpenPanel openPanel];
999     [panel setAllowsMultipleSelection:YES];
1000     [panel setAccessoryView:openPanelAccessoryView()];
1002     int result = [panel runModalForDirectory:dir file:nil types:nil];
1003     if (NSOKButton == result)
1004         [self application:NSApp openFiles:[panel filenames]];
1007 - (IBAction)selectNextWindow:(id)sender
1009     unsigned i, count = [vimControllers count];
1010     if (!count) return;
1012     NSWindow *keyWindow = [NSApp keyWindow];
1013     for (i = 0; i < count; ++i) {
1014         MMVimController *vc = [vimControllers objectAtIndex:i];
1015         if ([[[vc windowController] window] isEqual:keyWindow])
1016             break;
1017     }
1019     if (i < count) {
1020         if (++i >= count)
1021             i = 0;
1022         MMVimController *vc = [vimControllers objectAtIndex:i];
1023         [[vc windowController] showWindow:self];
1024     }
1027 - (IBAction)selectPreviousWindow:(id)sender
1029     unsigned i, count = [vimControllers count];
1030     if (!count) return;
1032     NSWindow *keyWindow = [NSApp keyWindow];
1033     for (i = 0; i < count; ++i) {
1034         MMVimController *vc = [vimControllers objectAtIndex:i];
1035         if ([[[vc windowController] window] isEqual:keyWindow])
1036             break;
1037     }
1039     if (i < count) {
1040         if (i > 0) {
1041             --i;
1042         } else {
1043             i = count - 1;
1044         }
1045         MMVimController *vc = [vimControllers objectAtIndex:i];
1046         [[vc windowController] showWindow:self];
1047     }
1050 - (IBAction)orderFrontPreferencePanel:(id)sender
1052     [[MMPreferenceController sharedPrefsWindowController] showWindow:self];
1055 - (IBAction)openWebsite:(id)sender
1057     [[NSWorkspace sharedWorkspace] openURL:
1058             [NSURL URLWithString:MMWebsiteString]];
1061 - (IBAction)showVimHelp:(id)sender
1063     // Open a new window with the help window maximized.
1064     [self launchVimProcessWithArguments:[NSArray arrayWithObjects:
1065             @"-c", @":h gui_mac", @"-c", @":res", nil]];
1068 - (IBAction)zoomAll:(id)sender
1070     [NSApp makeWindowsPerform:@selector(performZoom:) inOrder:YES];
1073 - (IBAction)atsuiButtonClicked:(id)sender
1075     // This action is called when the user clicks the "use ATSUI renderer"
1076     // button in the advanced preferences pane.
1077     [self rebuildPreloadCache];
1080 - (IBAction)loginShellButtonClicked:(id)sender
1082     // This action is called when the user clicks the "use login shell" button
1083     // in the advanced preferences pane.
1084     [self rebuildPreloadCache];
1087 - (IBAction)quickstartButtonClicked:(id)sender
1089     if ([self maxPreloadCacheSize] > 0) {
1090         [self scheduleVimControllerPreloadAfterDelay:1.0];
1091         [self startWatchingVimDir];
1092     } else {
1093         [self cancelVimControllerPreloadRequests];
1094         [self clearPreloadCacheWithCount:-1];
1095         [self stopWatchingVimDir];
1096     }
1099 - (unsigned)connectBackend:(byref in id <MMBackendProtocol>)backend
1100                        pid:(int)pid
1102     //NSLog(@"Connect backend (pid=%d)", pid);
1103     NSNumber *pidKey = [NSNumber numberWithInt:pid];
1104     MMVimController *vc = nil;
1106     @try {
1107         [(NSDistantObject*)backend
1108                 setProtocolForProxy:@protocol(MMBackendProtocol)];
1110         vc = [[[MMVimController alloc] initWithBackend:backend pid:pid]
1111                 autorelease];
1113         if (preloadPid == pid) {
1114             // This backend was preloaded, so add it to the cache and schedule
1115             // another vim process to be preloaded.
1116             preloadPid = -1;
1117             [vc setIsPreloading:YES];
1118             [cachedVimControllers addObject:vc];
1119             [self scheduleVimControllerPreloadAfterDelay:1];
1121             return [vc identifier];
1122         }
1124         [vimControllers addObject:vc];
1126         id args = [pidArguments objectForKey:pidKey];
1127         if (args && [NSNull null] != args)
1128             [vc passArguments:args];
1130         // HACK!  MacVim does not get activated if it is launched from the
1131         // terminal, so we forcibly activate here unless it is an untitled
1132         // window opening.  Untitled windows are treated differently, else
1133         // MacVim would steal the focus if another app was activated while the
1134         // untitled window was loading.
1135         if (!args || args != [NSNull null])
1136             [self activateWhenNextWindowOpens];
1138         if (args)
1139             [pidArguments removeObjectForKey:pidKey];
1141         return [vc identifier];
1142     }
1144     @catch (NSException *e) {
1145         NSLog(@"Exception caught in %s: \"%@\"", _cmd, e);
1147         if (vc)
1148             [vimControllers removeObject:vc];
1150         [pidArguments removeObjectForKey:pidKey];
1151     }
1153     return 0;
1156 - (NSArray *)serverList
1158     NSMutableArray *array = [NSMutableArray array];
1160     unsigned i, count = [vimControllers count];
1161     for (i = 0; i < count; ++i) {
1162         MMVimController *controller = [vimControllers objectAtIndex:i];
1163         if ([controller serverName])
1164             [array addObject:[controller serverName]];
1165     }
1167     return array;
1170 - (oneway void)processInput:(in bycopy NSArray *)queue
1171               forIdentifier:(unsigned)identifier
1173     if (!queue)
1174         return;
1176     NSNumber *key = [NSNumber numberWithUnsignedInt:identifier];
1177     NSArray *q = [inputQueues objectForKey:key];
1178     if (q) {
1179         q = [q arrayByAddingObjectsFromArray:queue];
1180         [inputQueues setObject:q forKey:key];
1181     } else {
1182         [inputQueues setObject:queue forKey:key];
1183     }
1185     [self performSelector:@selector(processInputQueues:)
1186                withObject:nil
1187                afterDelay:0];
1190 - (MMVimController *)keyVimController
1192     NSWindow *keyWindow = [NSApp keyWindow];
1193     if (keyWindow) {
1194         unsigned i, count = [vimControllers count];
1195         for (i = 0; i < count; ++i) {
1196             MMVimController *vc = [vimControllers objectAtIndex:i];
1197             if ([[[vc windowController] window] isEqual:keyWindow])
1198                 return vc;
1199         }
1200     }
1202     return nil;
1205 @end // MMAppController
1210 @implementation MMAppController (MMServices)
1212 - (void)openSelection:(NSPasteboard *)pboard userData:(NSString *)userData
1213                 error:(NSString **)error
1215     if (![[pboard types] containsObject:NSStringPboardType]) {
1216         NSLog(@"WARNING: Pasteboard contains no object of type "
1217                 "NSStringPboardType");
1218         return;
1219     }
1221     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
1222     BOOL openInCurrentWindow = [ud boolForKey:MMOpenInCurrentWindowKey];
1223     MMVimController *vc;
1225     if (openInCurrentWindow && (vc = [self topmostVimController])) {
1226         [vc sendMessage:AddNewTabMsgID data:nil];
1227         [vc dropString:[pboard stringForType:NSStringPboardType]];
1228     } else {
1229         // Save the text, open a new window, and paste the text when the next
1230         // window opens.  (If this is called several times in a row, then all
1231         // but the last call may be ignored.)
1232         if (openSelectionString) [openSelectionString release];
1233         openSelectionString = [[pboard stringForType:NSStringPboardType] copy];
1235         [self newWindow:self];
1236     }
1239 - (void)openFile:(NSPasteboard *)pboard userData:(NSString *)userData
1240            error:(NSString **)error
1242     if (![[pboard types] containsObject:NSStringPboardType]) {
1243         NSLog(@"WARNING: Pasteboard contains no object of type "
1244                 "NSStringPboardType");
1245         return;
1246     }
1248     // TODO: Parse multiple filenames and create array with names.
1249     NSString *string = [pboard stringForType:NSStringPboardType];
1250     string = [string stringByTrimmingCharactersInSet:
1251             [NSCharacterSet whitespaceAndNewlineCharacterSet]];
1252     string = [string stringByStandardizingPath];
1254     NSArray *filenames = [self filterFilesAndNotify:
1255             [NSArray arrayWithObject:string]];
1256     if ([filenames count] == 0)
1257         return;
1259     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
1260     BOOL openInCurrentWindow = [ud boolForKey:MMOpenInCurrentWindowKey];
1261     MMVimController *vc;
1263     if (openInCurrentWindow && (vc = [self topmostVimController])) {
1264         [vc dropFiles:filenames forceOpen:YES];
1265     } else {
1266         [self openFiles:filenames withArguments:nil];
1267     }
1270 - (void)newFileHere:(NSPasteboard *)pboard userData:(NSString *)userData
1271               error:(NSString **)error
1273     if (![[pboard types] containsObject:NSStringPboardType]) {
1274         NSLog(@"WARNING: Pasteboard contains no object of type "
1275               "NSStringPboardType");
1276         return;
1277     }
1279     NSString *path = [pboard stringForType:NSStringPboardType];
1281     BOOL dirIndicator;
1282     if (![[NSFileManager defaultManager] fileExistsAtPath:path
1283                                               isDirectory:&dirIndicator]) {
1284         NSLog(@"Invalid path. Cannot open new document at: %@", path);
1285         return;
1286     }
1288     if (!dirIndicator)
1289         path = [path stringByDeletingLastPathComponent];
1291     path = [path stringByEscapingSpecialFilenameCharacters];
1293     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
1294     BOOL openInCurrentWindow = [ud boolForKey:MMOpenInCurrentWindowKey];
1295     MMVimController *vc;
1297     if (openInCurrentWindow && (vc = [self topmostVimController])) {
1298         NSString *input = [NSString stringWithFormat:@"<C-\\><C-N>"
1299                 ":tabe|cd %@<CR>", path];
1300         [vc addVimInput:input];
1301     } else {
1302         NSString *input = [NSString stringWithFormat:@":cd %@", path];
1303         [self launchVimProcessWithArguments:[NSArray arrayWithObjects:
1304                                              @"-c", input, nil]];
1305     }
1308 @end // MMAppController (MMServices)
1313 @implementation MMAppController (Private)
1315 - (MMVimController *)topmostVimController
1317     // Find the topmost visible window which has an associated vim controller.
1318     NSEnumerator *e = [[NSApp orderedWindows] objectEnumerator];
1319     id window;
1320     while ((window = [e nextObject]) && [window isVisible]) {
1321         unsigned i, count = [vimControllers count];
1322         for (i = 0; i < count; ++i) {
1323             MMVimController *vc = [vimControllers objectAtIndex:i];
1324             if ([[[vc windowController] window] isEqual:window])
1325                 return vc;
1326         }
1327     }
1329     return nil;
1332 - (int)launchVimProcessWithArguments:(NSArray *)args
1334     int pid = -1;
1335     NSString *path = [[NSBundle mainBundle] pathForAuxiliaryExecutable:@"Vim"];
1337     if (!path) {
1338         NSLog(@"ERROR: Vim executable could not be found inside app bundle!");
1339         return -1;
1340     }
1342     NSArray *taskArgs = [NSArray arrayWithObjects:@"-g", @"-f", nil];
1343     if (args)
1344         taskArgs = [taskArgs arrayByAddingObjectsFromArray:args];
1346     BOOL useLoginShell = [[NSUserDefaults standardUserDefaults]
1347             boolForKey:MMLoginShellKey];
1348     if (useLoginShell) {
1349         // Run process with a login shell, roughly:
1350         //   echo "exec Vim -g -f args" | ARGV0=-`basename $SHELL` $SHELL [-l]
1351         pid = [self executeInLoginShell:path arguments:taskArgs];
1352     } else {
1353         // Run process directly:
1354         //   Vim -g -f args
1355         NSTask *task = [NSTask launchedTaskWithLaunchPath:path
1356                                                 arguments:taskArgs];
1357         pid = task ? [task processIdentifier] : -1;
1358     }
1360     if (-1 != pid) {
1361         // The 'pidArguments' dictionary keeps arguments to be passed to the
1362         // process when it connects (this is in contrast to arguments which are
1363         // passed on the command line, like '-f' and '-g').
1364         // If this method is called with nil arguments we take this as a hint
1365         // that this is an "untitled window" being launched and add a null
1366         // object to the 'pidArguments' dictionary.  This way we can detect if
1367         // an untitled window is being launched by looking for null objects in
1368         // this dictionary.
1369         // If this method is called with non-nil arguments then it is assumed
1370         // that the caller takes care of adding items to 'pidArguments' as
1371         // necessary (only some arguments are passed on connect, e.g. files to
1372         // open).
1373         if (!args)
1374             [pidArguments setObject:[NSNull null]
1375                              forKey:[NSNumber numberWithInt:pid]];
1376     } else {
1377         NSLog(@"WARNING: %s%@ failed (useLoginShell=%d)", _cmd, args,
1378                 useLoginShell);
1379     }
1381     return pid;
1384 - (NSArray *)filterFilesAndNotify:(NSArray *)filenames
1386     // Go trough 'filenames' array and make sure each file exists.  Present
1387     // warning dialog if some file was missing.
1389     NSString *firstMissingFile = nil;
1390     NSMutableArray *files = [NSMutableArray array];
1391     unsigned i, count = [filenames count];
1393     for (i = 0; i < count; ++i) {
1394         NSString *name = [filenames objectAtIndex:i];
1395         if ([[NSFileManager defaultManager] fileExistsAtPath:name]) {
1396             [files addObject:name];
1397         } else if (!firstMissingFile) {
1398             firstMissingFile = name;
1399         }
1400     }
1402     if (firstMissingFile) {
1403         NSAlert *alert = [[NSAlert alloc] init];
1404         [alert addButtonWithTitle:NSLocalizedString(@"OK",
1405                 @"Dialog button")];
1407         NSString *text;
1408         if ([files count] >= count-1) {
1409             [alert setMessageText:NSLocalizedString(@"File not found",
1410                     @"File not found dialog, title")];
1411             text = [NSString stringWithFormat:NSLocalizedString(
1412                     @"Could not open file with name %@.",
1413                     @"File not found dialog, text"), firstMissingFile];
1414         } else {
1415             [alert setMessageText:NSLocalizedString(@"Multiple files not found",
1416                     @"File not found dialog, title")];
1417             text = [NSString stringWithFormat:NSLocalizedString(
1418                     @"Could not open file with name %@, and %d other files.",
1419                     @"File not found dialog, text"),
1420                 firstMissingFile, count-[files count]-1];
1421         }
1423         [alert setInformativeText:text];
1424         [alert setAlertStyle:NSWarningAlertStyle];
1426         [alert runModal];
1427         [alert release];
1429         [NSApp replyToOpenOrPrint:NSApplicationDelegateReplyFailure];
1430     }
1432     return files;
1435 - (NSArray *)filterOpenFiles:(NSArray *)filenames
1436                openFilesDict:(NSDictionary **)openFiles
1438     // Filter out any files in the 'filenames' array that are open and return
1439     // all files that are not already open.  On return, the 'openFiles'
1440     // parameter (if non-nil) will point to a dictionary of open files, indexed
1441     // by Vim controller.
1443     NSMutableDictionary *dict = [NSMutableDictionary dictionary];
1444     NSMutableArray *files = [filenames mutableCopy];
1446     // TODO: Escape special characters in 'files'?
1447     NSString *expr = [NSString stringWithFormat:
1448             @"map([\"%@\"],\"bufloaded(v:val)\")",
1449             [files componentsJoinedByString:@"\",\""]];
1451     unsigned i, count = [vimControllers count];
1452     for (i = 0; i < count && [files count] > 0; ++i) {
1453         MMVimController *vc = [vimControllers objectAtIndex:i];
1455         // Query Vim for which files in the 'files' array are open.
1456         NSString *eval = [vc evaluateVimExpression:expr];
1457         if (!eval) continue;
1459         NSIndexSet *idxSet = [NSIndexSet indexSetWithVimList:eval];
1460         if ([idxSet count] > 0) {
1461             [dict setObject:[files objectsAtIndexes:idxSet]
1462                      forKey:[NSValue valueWithPointer:vc]];
1464             // Remove all the files that were open in this Vim process and
1465             // create a new expression to evaluate.
1466             [files removeObjectsAtIndexes:idxSet];
1467             expr = [NSString stringWithFormat:
1468                     @"map([\"%@\"],\"bufloaded(v:val)\")",
1469                     [files componentsJoinedByString:@"\",\""]];
1470         }
1471     }
1473     if (openFiles != nil)
1474         *openFiles = dict;
1476     return files;
1479 #if MM_HANDLE_XCODE_MOD_EVENT
1480 - (void)handleXcodeModEvent:(NSAppleEventDescriptor *)event
1481                  replyEvent:(NSAppleEventDescriptor *)reply
1483 #if 0
1484     // Xcode sends this event to query MacVim which open files have been
1485     // modified.
1486     NSLog(@"reply:%@", reply);
1487     NSLog(@"event:%@", event);
1489     NSEnumerator *e = [vimControllers objectEnumerator];
1490     id vc;
1491     while ((vc = [e nextObject])) {
1492         DescType type = [reply descriptorType];
1493         unsigned len = [[type data] length];
1494         NSMutableData *data = [NSMutableData data];
1496         [data appendBytes:&type length:sizeof(DescType)];
1497         [data appendBytes:&len length:sizeof(unsigned)];
1498         [data appendBytes:[reply data] length:len];
1500         [vc sendMessage:XcodeModMsgID data:data];
1501     }
1502 #endif
1504 #endif
1506 - (void)handleGetURLEvent:(NSAppleEventDescriptor *)event
1507                replyEvent:(NSAppleEventDescriptor *)reply
1509     NSString *urlString = [[event paramDescriptorForKeyword:keyDirectObject]
1510         stringValue];
1511     NSURL *url = [NSURL URLWithString:urlString];
1513     // We try to be compatible with TextMate's URL scheme here, as documented
1514     // at http://blog.macromates.com/2007/the-textmate-url-scheme/ . Currently,
1515     // this means that:
1516     //
1517     // The format is: mvim://open?<arguments> where arguments can be:
1518     //
1519     // * url â€” the actual file to open (i.e. a file://… URL), if you leave
1520     //         out this argument, the frontmost document is implied.
1521     // * line â€” line number to go to (one based).
1522     // * column â€” column number to go to (one based).
1523     //
1524     // Example: mvim://open?url=file:///etc/profile&line=20
1526     if ([[url host] isEqualToString:@"open"]) {
1527         NSMutableDictionary *dict = [NSMutableDictionary dictionary];
1529         // Parse query ("url=file://...&line=14") into a dictionary
1530         NSArray *queries = [[url query] componentsSeparatedByString:@"&"];
1531         NSEnumerator *enumerator = [queries objectEnumerator];
1532         NSString *param;
1533         while( param = [enumerator nextObject] ) {
1534             NSArray *arr = [param componentsSeparatedByString:@"="];
1535             if ([arr count] == 2) {
1536                 [dict setValue:[[arr lastObject]
1537                             stringByReplacingPercentEscapesUsingEncoding:
1538                                 NSUTF8StringEncoding]
1539                         forKey:[[arr objectAtIndex:0]
1540                             stringByReplacingPercentEscapesUsingEncoding:
1541                                 NSUTF8StringEncoding]];
1542             }
1543         }
1545         // Actually open the file.
1546         NSString *file = [dict objectForKey:@"url"];
1547         if (file != nil) {
1548             NSURL *fileUrl= [NSURL URLWithString:file];
1549             // TextMate only opens files that already exist.
1550             if ([fileUrl isFileURL]
1551                     && [[NSFileManager defaultManager] fileExistsAtPath:
1552                            [fileUrl path]]) {
1553                 // Strip 'file://' path, else application:openFiles: might think
1554                 // the file is not yet open.
1555                 NSArray *filenames = [NSArray arrayWithObject:[fileUrl path]];
1557                 // Look for the line and column options.
1558                 NSDictionary *args = nil;
1559                 NSString *line = [dict objectForKey:@"line"];
1560                 if (line) {
1561                     NSString *column = [dict objectForKey:@"column"];
1562                     if (column)
1563                         args = [NSDictionary dictionaryWithObjectsAndKeys:
1564                                 line, @"cursorLine",
1565                                 column, @"cursorColumn",
1566                                 nil];
1567                     else
1568                         args = [NSDictionary dictionaryWithObject:line
1569                                 forKey:@"cursorLine"];
1570                 }
1572                 [self openFiles:filenames withArguments:args];
1573             }
1574         }
1575     } else {
1576         NSAlert *alert = [[NSAlert alloc] init];
1577         [alert addButtonWithTitle:NSLocalizedString(@"OK",
1578             @"Dialog button")];
1580         [alert setMessageText:NSLocalizedString(@"Unknown URL Scheme",
1581             @"Unknown URL Scheme dialog, title")];
1582         [alert setInformativeText:[NSString stringWithFormat:NSLocalizedString(
1583             @"This version of MacVim does not support \"%@\""
1584             @" in its URL scheme.",
1585             @"Unknown URL Scheme dialog, text"),
1586             [url host]]];
1588         [alert setAlertStyle:NSWarningAlertStyle];
1589         [alert runModal];
1590         [alert release];
1591     }
1595 - (int)findLaunchingProcessWithoutArguments
1597     NSArray *keys = [pidArguments allKeysForObject:[NSNull null]];
1598     if ([keys count] > 0) {
1599         //NSLog(@"found launching process without arguments");
1600         return [[keys objectAtIndex:0] intValue];
1601     }
1603     return -1;
1606 - (MMVimController *)findUnusedEditor
1608     NSEnumerator *e = [vimControllers objectEnumerator];
1609     id vc;
1610     while ((vc = [e nextObject])) {
1611         if ([[vc objectForVimStateKey:@"unusedEditor"] boolValue])
1612             return vc;
1613     }
1615     return nil;
1618 - (NSMutableDictionary *)extractArgumentsFromOdocEvent:
1619     (NSAppleEventDescriptor *)desc
1621     NSMutableDictionary *dict = [NSMutableDictionary dictionary];
1623     // 1. Extract ODB parameters (if any)
1624     NSAppleEventDescriptor *odbdesc = desc;
1625     if (![odbdesc paramDescriptorForKeyword:keyFileSender]) {
1626         // The ODB paramaters may hide inside the 'keyAEPropData' descriptor.
1627         odbdesc = [odbdesc paramDescriptorForKeyword:keyAEPropData];
1628         if (![odbdesc paramDescriptorForKeyword:keyFileSender])
1629             odbdesc = nil;
1630     }
1632     if (odbdesc) {
1633         NSAppleEventDescriptor *p =
1634                 [odbdesc paramDescriptorForKeyword:keyFileSender];
1635         if (p)
1636             [dict setObject:[NSNumber numberWithUnsignedInt:[p typeCodeValue]]
1637                      forKey:@"remoteID"];
1639         p = [odbdesc paramDescriptorForKeyword:keyFileCustomPath];
1640         if (p)
1641             [dict setObject:[p stringValue] forKey:@"remotePath"];
1643         p = [odbdesc paramDescriptorForKeyword:keyFileSenderToken];
1644         if (p) {
1645             [dict setObject:[NSNumber numberWithUnsignedLong:[p descriptorType]]
1646                      forKey:@"remoteTokenDescType"];
1647             [dict setObject:[p data] forKey:@"remoteTokenData"];
1648         }
1649     }
1651     // 2. Extract Xcode parameters (if any)
1652     NSAppleEventDescriptor *xcodedesc =
1653             [desc paramDescriptorForKeyword:keyAEPosition];
1654     if (xcodedesc) {
1655         NSRange range;
1656         MMSelectionRange *sr = (MMSelectionRange*)[[xcodedesc data] bytes];
1658         if (sr->lineNum < 0) {
1659             // Should select a range of lines.
1660             range.location = sr->startRange + 1;
1661             range.length = sr->endRange - sr->startRange + 1;
1662         } else {
1663             // Should only move cursor to a line.
1664             range.location = sr->lineNum + 1;
1665             range.length = 0;
1666         }
1668         [dict setObject:NSStringFromRange(range) forKey:@"selectionRange"];
1669     }
1671     // 3. Extract Spotlight search text (if any)
1672     NSAppleEventDescriptor *spotlightdesc = 
1673             [desc paramDescriptorForKeyword:keyAESearchText];
1674     if (spotlightdesc)
1675         [dict setObject:[spotlightdesc stringValue] forKey:@"searchText"];
1677     return dict;
1680 #ifdef MM_ENABLE_PLUGINS
1681 - (void)removePlugInMenu
1683     if ([plugInMenuItem menu])
1684         [[plugInMenuItem menu] removeItem:plugInMenuItem];
1687 - (void)addPlugInMenuToMenu:(NSMenu *)mainMenu
1689     NSMenu *windowsMenu = [mainMenu findWindowsMenu];
1691     if ([[plugInMenuItem submenu] numberOfItems] > 0) {
1692         int idx = windowsMenu ? [mainMenu indexOfItemWithSubmenu:windowsMenu]
1693                               : -1;
1694         if (idx > 0) {
1695             [mainMenu insertItem:plugInMenuItem atIndex:idx];
1696         } else {
1697             [mainMenu addItem:plugInMenuItem];
1698         }
1699     }
1701 #endif
1703 - (void)scheduleVimControllerPreloadAfterDelay:(NSTimeInterval)delay
1705     [self performSelector:@selector(preloadVimController:)
1706                withObject:nil
1707                afterDelay:delay];
1710 - (void)cancelVimControllerPreloadRequests
1712     [NSObject cancelPreviousPerformRequestsWithTarget:self
1713             selector:@selector(preloadVimController:)
1714               object:nil];
1717 - (void)preloadVimController:(id)sender
1719     // We only allow preloading of one Vim process at a time (to avoid hogging
1720     // CPU), so schedule another preload in a little while if necessary.
1721     if (-1 != preloadPid) {
1722         [self scheduleVimControllerPreloadAfterDelay:2];
1723         return;
1724     }
1726     if ([cachedVimControllers count] >= [self maxPreloadCacheSize])
1727         return;
1729     preloadPid = [self launchVimProcessWithArguments:
1730             [NSArray arrayWithObject:@"--mmwaitforack"]];
1733 - (int)maxPreloadCacheSize
1735     // The maximum number of Vim processes to keep in the cache can be
1736     // controlled via the user default "MMPreloadCacheSize".
1737     int maxCacheSize = [[NSUserDefaults standardUserDefaults]
1738             integerForKey:MMPreloadCacheSizeKey];
1739     if (maxCacheSize < 0) maxCacheSize = 0;
1740     else if (maxCacheSize > 10) maxCacheSize = 10;
1742     return maxCacheSize;
1745 - (MMVimController *)takeVimControllerFromCache
1747     // NOTE: After calling this message the backend corresponding to the
1748     // returned vim controller must be sent an acknowledgeConnection message,
1749     // else the vim process will be stuck.
1750     //
1751     // This method may return nil even though the cache might be non-empty; the
1752     // caller should handle this by starting a new Vim process.
1754     int i, count = [cachedVimControllers count];
1755     if (0 == count) return nil;
1757     // Locate the first Vim controller with up-to-date rc-files sourced.
1758     NSDate *rcDate = [self rcFilesModificationDate];
1759     for (i = 0; i < count; ++i) {
1760         MMVimController *vc = [cachedVimControllers objectAtIndex:i];
1761         NSDate *date = [vc creationDate];
1762         if ([date compare:rcDate] != NSOrderedAscending)
1763             break;
1764     }
1766     if (i > 0) {
1767         // Clear out cache entries whose vimrc/gvimrc files were sourced before
1768         // the latest modification date for those files.  This ensures that the
1769         // latest rc-files are always sourced for new windows.
1770         [self clearPreloadCacheWithCount:i];
1771     }
1773     if ([cachedVimControllers count] == 0) {
1774         [self scheduleVimControllerPreloadAfterDelay:2.0];
1775         return nil;
1776     }
1778     MMVimController *vc = [cachedVimControllers objectAtIndex:0];
1779     [vimControllers addObject:vc];
1780     [cachedVimControllers removeObjectAtIndex:0];
1781     [vc setIsPreloading:NO];
1783     // If the Vim process has finished loading then the window will displayed
1784     // now, otherwise it will be displayed when the OpenWindowMsgID message is
1785     // received.
1786     [[vc windowController] showWindow];
1788     // Since we've taken one controller from the cache we take the opportunity
1789     // to preload another.
1790     [self scheduleVimControllerPreloadAfterDelay:1];
1792     return vc;
1795 - (void)clearPreloadCacheWithCount:(int)count
1797     // Remove the 'count' first entries in the preload cache.  It is assumed
1798     // that objects are added/removed from the cache in a FIFO manner so that
1799     // this effectively clears the 'count' oldest entries.
1800     // If 'count' is negative, then the entire cache is cleared.
1802     if ([cachedVimControllers count] == 0 || count == 0)
1803         return;
1805     if (count < 0)
1806         count = [cachedVimControllers count];
1808     // Make sure the preloaded Vim processes get killed or they'll just hang
1809     // around being useless until MacVim is terminated.
1810     NSEnumerator *e = [cachedVimControllers objectEnumerator];
1811     MMVimController *vc;
1812     int n = count;
1813     while ((vc = [e nextObject]) && n-- > 0) {
1814         [[NSNotificationCenter defaultCenter] removeObserver:vc];
1815         [vc sendMessage:TerminateNowMsgID data:nil];
1817         // Since the preloaded processes were killed "prematurely" we have to
1818         // manually tell them to cleanup (it is not enough to simply release
1819         // them since deallocation and cleanup are separated).
1820         [vc cleanup];
1821     }
1823     n = count;
1824     while (n-- > 0 && [cachedVimControllers count] > 0)
1825         [cachedVimControllers removeObjectAtIndex:0];
1827     // There is a small delay before the Vim process actually exits so wait a
1828     // little before trying to reap the child process.  If the process still
1829     // hasn't exited after this wait it won't be reaped until the next time
1830     // reapChildProcesses: is called (but this should be harmless).
1831     [self performSelector:@selector(reapChildProcesses:)
1832                withObject:nil
1833                afterDelay:0.1];
1836 - (void)rebuildPreloadCache
1838     if ([self maxPreloadCacheSize] > 0) {
1839         [self clearPreloadCacheWithCount:-1];
1840         [self cancelVimControllerPreloadRequests];
1841         [self scheduleVimControllerPreloadAfterDelay:1.0];
1842     }
1845 - (NSDate *)rcFilesModificationDate
1847     // Check modification dates for ~/.vimrc and ~/.gvimrc and return the
1848     // latest modification date.  If ~/.vimrc does not exist, check ~/_vimrc
1849     // and similarly for gvimrc.
1850     // Returns distantPath if no rc files were found.
1852     NSDate *date = [NSDate distantPast];
1853     NSFileManager *fm = [NSFileManager defaultManager];
1855     NSString *path = [@"~/.vimrc" stringByExpandingTildeInPath];
1856     NSDictionary *attr = [fm fileAttributesAtPath:path traverseLink:YES];
1857     if (!attr) {
1858         path = [@"~/_vimrc" stringByExpandingTildeInPath];
1859         attr = [fm fileAttributesAtPath:path traverseLink:YES];
1860     }
1861     NSDate *modDate = [attr objectForKey:NSFileModificationDate];
1862     if (modDate)
1863         date = modDate;
1865     path = [@"~/.gvimrc" stringByExpandingTildeInPath];
1866     attr = [fm fileAttributesAtPath:path traverseLink:YES];
1867     if (!attr) {
1868         path = [@"~/_gvimrc" stringByExpandingTildeInPath];
1869         attr = [fm fileAttributesAtPath:path traverseLink:YES];
1870     }
1871     modDate = [attr objectForKey:NSFileModificationDate];
1872     if (modDate)
1873         date = [date laterDate:modDate];
1875     return date;
1878 - (BOOL)openVimControllerWithArguments:(NSDictionary *)arguments
1880     MMVimController *vc = [self findUnusedEditor];
1881     if (vc) {
1882         // Open files in an already open window.
1883         [[[vc windowController] window] makeKeyAndOrderFront:self];
1884         [vc passArguments:arguments];
1885     } else if ((vc = [self takeVimControllerFromCache])) {
1886         // Open files in a new window using a cached vim controller.  This
1887         // requires virtually no loading time so the new window will pop up
1888         // instantaneously.
1889         [vc passArguments:arguments];
1890         [[vc backendProxy] acknowledgeConnection];
1891     } else {
1892         // Open files in a launching Vim process or start a new process.  This
1893         // may take 1-2 seconds so there will be a visible delay before the
1894         // window appears on screen.
1895         int pid = [self findLaunchingProcessWithoutArguments];
1896         if (-1 == pid) {
1897             pid = [self launchVimProcessWithArguments:nil];
1898             if (-1 == pid)
1899                 return NO;
1900         }
1902         // TODO: If the Vim process fails to start, or if it changes PID,
1903         // then the memory allocated for these parameters will leak.
1904         // Ensure that this cannot happen or somehow detect it.
1906         if ([arguments count] > 0)
1907             [pidArguments setObject:arguments
1908                              forKey:[NSNumber numberWithInt:pid]];
1909     }
1911     return YES;
1914 - (void)activateWhenNextWindowOpens
1916     shouldActivateWhenNextWindowOpens = YES;
1919 - (void)startWatchingVimDir
1921     //NSLog(@"%s", _cmd);
1922 #if (MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_4)
1923     if (fsEventStream)
1924         return;
1925     if (NULL == FSEventStreamStart)
1926         return; // FSEvent functions are weakly linked
1928     NSString *path = [@"~/.vim" stringByExpandingTildeInPath];
1929     NSArray *pathsToWatch = [NSArray arrayWithObject:path];
1931     fsEventStream = FSEventStreamCreate(NULL, &fsEventCallback, NULL,
1932             (CFArrayRef)pathsToWatch, kFSEventStreamEventIdSinceNow,
1933             MMEventStreamLatency, kFSEventStreamCreateFlagNone);
1935     FSEventStreamScheduleWithRunLoop(fsEventStream,
1936             [[NSRunLoop currentRunLoop] getCFRunLoop],
1937             kCFRunLoopDefaultMode);
1939     FSEventStreamStart(fsEventStream);
1940     //NSLog(@"Started FS event stream");
1941 #endif
1944 - (void)stopWatchingVimDir
1946     //NSLog(@"%s", _cmd);
1947 #if (MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_4)
1948     if (NULL == FSEventStreamStop)
1949         return; // FSEvent functions are weakly linked
1951     if (fsEventStream) {
1952         FSEventStreamStop(fsEventStream);
1953         FSEventStreamInvalidate(fsEventStream);
1954         FSEventStreamRelease(fsEventStream);
1955         fsEventStream = NULL;
1956         //NSLog(@"Stopped FS event stream");
1957     }
1958 #endif
1962 - (void)handleFSEvent
1964     //NSLog(@"%s", _cmd);
1965     [self clearPreloadCacheWithCount:-1];
1967     // Several FS events may arrive in quick succession so make sure to cancel
1968     // any previous preload requests before making a new one.
1969     [self cancelVimControllerPreloadRequests];
1970     [self scheduleVimControllerPreloadAfterDelay:0.5];
1973 - (void)loadDefaultFont
1975     // It is possible to set a user default to avoid loading the default font
1976     // (this cuts down on startup time).
1977     if (![[NSUserDefaults standardUserDefaults] boolForKey:MMLoadDefaultFontKey]
1978             || fontContainerRef)
1979         return;
1981     // Load all fonts in the Resouces folder of the app bundle.
1982     NSString *fontsFolder = [[NSBundle mainBundle] resourcePath];
1983     if (fontsFolder) {
1984         NSURL *fontsURL = [NSURL fileURLWithPath:fontsFolder];
1985         if (fontsURL) {
1986             FSRef fsRef;
1987             CFURLGetFSRef((CFURLRef)fontsURL, &fsRef);
1989 #if (MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_4)
1990             // This is the font activation API for OS X 10.5.  Only compile
1991             // this code if we're building on OS X 10.5 or later.
1992             if (NULL != ATSFontActivateFromFileReference) { // Weakly linked
1993                 ATSFontActivateFromFileReference(&fsRef, kATSFontContextLocal,
1994                                                  kATSFontFormatUnspecified,
1995                                                  NULL, kATSOptionFlagsDefault,
1996                                                  &fontContainerRef);
1997             }
1998 #endif
1999 #if (MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_4)
2000             // The following font activation API was deprecated in OS X 10.5.
2001             // Don't compile this code unless we're targeting OS X 10.4.
2002             FSSpec fsSpec;
2003             if (fontContainerRef == 0 &&
2004                     FSGetCatalogInfo(&fsRef, kFSCatInfoNone, NULL, NULL,
2005                                      &fsSpec, NULL) == noErr) {
2006                 ATSFontActivateFromFileSpecification(&fsSpec,
2007                         kATSFontContextLocal, kATSFontFormatUnspecified, NULL,
2008                         kATSOptionFlagsDefault, &fontContainerRef);
2009             }
2010 #endif
2011         }
2012     }
2014     if (!fontContainerRef)
2015         NSLog(@"WARNING: Failed to activate the default font (the app bundle "
2016                 "may be incomplete)");
2019 - (int)executeInLoginShell:(NSString *)path arguments:(NSArray *)args
2021     // Start a login shell and execute the command 'path' with arguments 'args'
2022     // in the shell.  This ensures that user environment variables are set even
2023     // when MacVim was started from the Finder.
2025     int pid = -1;
2026     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
2028     // Determine which shell to use to execute the command.  The user
2029     // may decide which shell to use by setting a user default or the
2030     // $SHELL environment variable.
2031     NSString *shell = [ud stringForKey:MMLoginShellCommandKey];
2032     if (!shell || [shell length] == 0)
2033         shell = [[[NSProcessInfo processInfo] environment]
2034             objectForKey:@"SHELL"];
2035     if (!shell)
2036         shell = @"/bin/bash";
2038     //NSLog(@"shell = %@", shell);
2040     // Bash needs the '-l' flag to launch a login shell.  The user may add
2041     // flags by setting a user default.
2042     NSString *shellArgument = [ud stringForKey:MMLoginShellArgumentKey];
2043     if (!shellArgument || [shellArgument length] == 0) {
2044         if ([[shell lastPathComponent] isEqual:@"bash"])
2045             shellArgument = @"-l";
2046         else
2047             shellArgument = nil;
2048     }
2050     //NSLog(@"shellArgument = %@", shellArgument);
2052     // Build input string to pipe to the login shell.
2053     NSMutableString *input = [NSMutableString stringWithFormat:
2054             @"exec \"%@\"", path];
2055     if (args) {
2056         // Append all arguments, making sure they are properly quoted, even
2057         // when they contain single quotes.
2058         NSEnumerator *e = [args objectEnumerator];
2059         id obj;
2061         while ((obj = [e nextObject])) {
2062             NSMutableString *arg = [NSMutableString stringWithString:obj];
2063             [arg replaceOccurrencesOfString:@"'" withString:@"'\"'\"'"
2064                                     options:NSLiteralSearch
2065                                       range:NSMakeRange(0, [arg length])];
2066             [input appendFormat:@" '%@'", arg];
2067         }
2068     }
2070     // Build the argument vector used to start the login shell.
2071     NSString *shellArg0 = [NSString stringWithFormat:@"-%@",
2072              [shell lastPathComponent]];
2073     char *shellArgv[3] = { (char *)[shellArg0 UTF8String], NULL, NULL };
2074     if (shellArgument)
2075         shellArgv[1] = (char *)[shellArgument UTF8String];
2077     // Get the C string representation of the shell path before the fork since
2078     // we must not call Foundation functions after a fork.
2079     const char *shellPath = [shell fileSystemRepresentation];
2081     // Fork and execute the process.
2082     int ds[2];
2083     if (pipe(ds)) return -1;
2085     pid = fork();
2086     if (pid == -1) {
2087         return -1;
2088     } else if (pid == 0) {
2089         // Child process
2091         if (close(ds[1]) == -1) exit(255);
2092         if (dup2(ds[0], 0) == -1) exit(255);
2094         // Without the following call warning messages like this appear on the
2095         // console:
2096         //     com.apple.launchd[69] : Stray process with PGID equal to this
2097         //                             dead job: PID 1589 PPID 1 Vim
2098         setsid();
2100         execv(shellPath, shellArgv);
2102         // Never reached unless execv fails
2103         exit(255);
2104     } else {
2105         // Parent process
2106         if (close(ds[0]) == -1) return -1;
2108         // Send input to execute to the child process
2109         [input appendString:@"\n"];
2110         int bytes = [input lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
2112         if (write(ds[1], [input UTF8String], bytes) != bytes) return -1;
2113         if (close(ds[1]) == -1) return -1;
2115         ++numChildProcesses;
2116         //NSLog(@"new process pid=%d (count=%d)", pid, numChildProcesses);
2117     }
2119     return pid;
2122 - (void)reapChildProcesses:(id)sender
2124     // NOTE: numChildProcesses (currently) only counts the number of Vim
2125     // processes that have been started with executeInLoginShell::.  If other
2126     // processes are spawned this code may need to be adjusted (or
2127     // numChildProcesses needs to be incremented when such a process is
2128     // started).
2129     while (numChildProcesses > 0) {
2130         int status = 0;
2131         int pid = waitpid(-1, &status, WNOHANG);
2132         if (pid <= 0)
2133             break;
2135         //NSLog(@"WAIT for pid=%d complete", pid);
2136         --numChildProcesses;
2137     }
2140 - (void)processInputQueues:(id)sender
2142     MMVimController *vc;
2143     NSEnumerator *e = [vimControllers objectEnumerator];
2144     while ((vc = [e nextObject])) {
2145         NSNumber *key = [NSNumber numberWithUnsignedInt:[vc identifier]];
2146         NSArray *q = [inputQueues objectForKey:key];
2147         if (q) {
2148             // Remove queue from the dictionary before processing it because
2149             // more input may arrive during processing of the queue (we have to
2150             // retain/release the queue otherwise it will be deallocated when
2151             // we remove it from the dictionary).
2152             [q retain];
2153             [inputQueues removeObjectForKey:key];
2155             [vc processInputQueue:q];
2156             [q release];
2157         }
2158     }
2161 @end // MMAppController (Private)