Fix compilation problems on Tiger
[MacVim.git] / src / MacVim / MMAppController.m
blobf2e6b92c131532221ffb506f0a20d046942fde44
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     NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:
157         [NSNumber numberWithBool:NO],   MMNoWindowKey,
158         [NSNumber numberWithInt:64],    MMTabMinWidthKey,
159         [NSNumber numberWithInt:6*64],  MMTabMaxWidthKey,
160         [NSNumber numberWithInt:132],   MMTabOptimumWidthKey,
161         [NSNumber numberWithBool:YES],  MMShowAddTabButtonKey,
162         [NSNumber numberWithInt:2],     MMTextInsetLeftKey,
163         [NSNumber numberWithInt:1],     MMTextInsetRightKey,
164         [NSNumber numberWithInt:1],     MMTextInsetTopKey,
165         [NSNumber numberWithInt:1],     MMTextInsetBottomKey,
166         @"MMTypesetter",                MMTypesetterKey,
167         [NSNumber numberWithFloat:1],   MMCellWidthMultiplierKey,
168         [NSNumber numberWithFloat:-1],  MMBaselineOffsetKey,
169         [NSNumber numberWithBool:YES],  MMTranslateCtrlClickKey,
170         [NSNumber numberWithInt:0],     MMOpenInCurrentWindowKey,
171         [NSNumber numberWithBool:NO],   MMNoFontSubstitutionKey,
172         [NSNumber numberWithBool:YES],  MMLoginShellKey,
173         [NSNumber numberWithBool:NO],   MMAtsuiRendererKey,
174         [NSNumber numberWithInt:MMUntitledWindowAlways],
175                                         MMUntitledWindowKey,
176         [NSNumber numberWithBool:NO],   MMTexturedWindowKey,
177         [NSNumber numberWithBool:NO],   MMZoomBothKey,
178         @"",                            MMLoginShellCommandKey,
179         @"",                            MMLoginShellArgumentKey,
180         [NSNumber numberWithBool:YES],  MMDialogsTrackPwdKey,
181 #ifdef MM_ENABLE_PLUGINS
182         [NSNumber numberWithBool:YES],  MMShowLeftPlugInContainerKey,
183 #endif
184         [NSNumber numberWithInt:3],     MMOpenLayoutKey,
185         [NSNumber numberWithBool:NO],   MMVerticalSplitKey,
186         [NSNumber numberWithInt:0],     MMPreloadCacheSizeKey,
187         [NSNumber numberWithInt:0],     MMLastWindowClosedBehaviorKey,
188         [NSNumber numberWithBool:YES],  MMLoadDefaultFontKey,
189         nil];
191     [[NSUserDefaults standardUserDefaults] registerDefaults:dict];
193     NSArray *types = [NSArray arrayWithObject:NSStringPboardType];
194     [NSApp registerServicesMenuSendTypes:types returnTypes:types];
196     // NOTE: Set the current directory to user's home directory, otherwise it
197     // will default to the root directory.  (This matters since new Vim
198     // processes inherit MacVim's environment variables.)
199     [[NSFileManager defaultManager] changeCurrentDirectoryPath:
200             NSHomeDirectory()];
203 - (id)init
205     if (!(self = [super init])) return nil;
207     [self loadDefaultFont];
209     vimControllers = [NSMutableArray new];
210     cachedVimControllers = [NSMutableArray new];
211     preloadPid = -1;
212     pidArguments = [NSMutableDictionary new];
213     inputQueues = [NSMutableDictionary new];
215 #ifdef MM_ENABLE_PLUGINS
216     NSString *plugInTitle = NSLocalizedString(@"Plug-In",
217                                               @"Plug-In menu title");
218     plugInMenuItem = [[NSMenuItem alloc] initWithTitle:plugInTitle
219                                                 action:NULL
220                                          keyEquivalent:@""];
221     NSMenu *submenu = [[NSMenu alloc] initWithTitle:plugInTitle];
222     [plugInMenuItem setSubmenu:submenu];
223     [submenu release];
224 #endif
226     // NOTE: Do not use the default connection since the Logitech Control
227     // Center (LCC) input manager steals and this would cause MacVim to
228     // never open any windows.  (This is a bug in LCC but since they are
229     // unlikely to fix it, we graciously give them the default connection.)
230     connection = [[NSConnection alloc] initWithReceivePort:[NSPort port]
231                                                   sendPort:nil];
232     [connection setRootObject:self];
233     [connection setRequestTimeout:MMRequestTimeout];
234     [connection setReplyTimeout:MMReplyTimeout];
236     // NOTE!  If the name of the connection changes here it must also be
237     // updated in MMBackend.m.
238     NSString *name = [NSString stringWithFormat:@"%@-connection",
239              [[NSBundle mainBundle] bundlePath]];
240     //NSLog(@"Registering connection with name '%@'", name);
241     if (![connection registerName:name]) {
242         NSLog(@"FATAL ERROR: Failed to register connection with name '%@'",
243                 name);
244         [connection release];  connection = nil;
245     }
247     return self;
250 - (void)dealloc
252     //NSLog(@"MMAppController dealloc");
254     [connection release];  connection = nil;
255     [inputQueues release];  inputQueues = nil;
256     [pidArguments release];  pidArguments = nil;
257     [vimControllers release];  vimControllers = nil;
258     [cachedVimControllers release];  cachedVimControllers = nil;
259     [openSelectionString release];  openSelectionString = nil;
260     [recentFilesMenuItem release];  recentFilesMenuItem = nil;
261     [defaultMainMenu release];  defaultMainMenu = nil;
262 #ifdef MM_ENABLE_PLUGINS
263     [plugInMenuItem release];  plugInMenuItem = nil;
264 #endif
265     [appMenuItemTemplate release];  appMenuItemTemplate = nil;
267     [super dealloc];
270 - (void)applicationWillFinishLaunching:(NSNotification *)notification
272     // Remember the default menu so that it can be restored if the user closes
273     // all editor windows.
274     defaultMainMenu = [[NSApp mainMenu] retain];
276     // Store a copy of the default app menu so we can use this as a template
277     // for all other menus.  We make a copy here because the "Services" menu
278     // will not yet have been populated at this time.  If we don't we get
279     // problems trying to set key equivalents later on because they might clash
280     // with items on the "Services" menu.
281     appMenuItemTemplate = [defaultMainMenu itemAtIndex:0];
282     appMenuItemTemplate = [appMenuItemTemplate copy];
284     // Set up the "Open Recent" menu. See
285     //   http://lapcatsoftware.com/blog/2007/07/10/
286     //     working-without-a-nib-part-5-open-recent-menu/
287     // and
288     //   http://www.cocoabuilder.com/archive/message/cocoa/2007/8/15/187793
289     // for more information.
290     //
291     // The menu itself is created in MainMenu.nib but we still seem to have to
292     // hack around a bit to get it to work.  (This has to be done in
293     // applicationWillFinishLaunching at the latest, otherwise it doesn't
294     // work.)
295     NSMenu *fileMenu = [defaultMainMenu findFileMenu];
296     if (fileMenu) {
297         int idx = [fileMenu indexOfItemWithAction:@selector(fileOpen:)];
298         if (idx >= 0 && idx+1 < [fileMenu numberOfItems])
300         recentFilesMenuItem = [fileMenu itemWithTitle:@"Open Recent"];
301         [[recentFilesMenuItem submenu] performSelector:@selector(_setMenuName:)
302                                         withObject:@"NSRecentDocumentsMenu"];
304         // Note: The "Recent Files" menu must be moved around since there is no
305         // -[NSApp setRecentFilesMenu:] method.  We keep a reference to it to
306         // facilitate this move (see setMainMenu: below).
307         [recentFilesMenuItem retain];
308     }
310 #if MM_HANDLE_XCODE_MOD_EVENT
311     [[NSAppleEventManager sharedAppleEventManager]
312             setEventHandler:self
313                 andSelector:@selector(handleXcodeModEvent:replyEvent:)
314               forEventClass:'KAHL'
315                  andEventID:'MOD '];
316 #endif
318     // Register 'mvim://' URL handler
319     [[NSAppleEventManager sharedAppleEventManager]
320             setEventHandler:self
321                 andSelector:@selector(handleGetURLEvent:replyEvent:)
322               forEventClass:kInternetEventClass
323                  andEventID:kAEGetURL];
326 - (void)applicationDidFinishLaunching:(NSNotification *)notification
328     [NSApp setServicesProvider:self];
329 #ifdef MM_ENABLE_PLUGINS
330     [[MMPlugInManager sharedManager] loadAllPlugIns];
331 #endif
333     if ([self maxPreloadCacheSize] > 0) {
334         [self scheduleVimControllerPreloadAfterDelay:2];
335         [self startWatchingVimDir];
336     }
339 - (BOOL)applicationShouldOpenUntitledFile:(NSApplication *)sender
341     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
342     NSAppleEventManager *aem = [NSAppleEventManager sharedAppleEventManager];
343     NSAppleEventDescriptor *desc = [aem currentAppleEvent];
345     // The user default MMUntitledWindow can be set to control whether an
346     // untitled window should open on 'Open' and 'Reopen' events.
347     int untitledWindowFlag = [ud integerForKey:MMUntitledWindowKey];
349     BOOL isAppOpenEvent = [desc eventID] == kAEOpenApplication;
350     if (isAppOpenEvent && (untitledWindowFlag & MMUntitledWindowOnOpen) == 0)
351         return NO;
353     BOOL isAppReopenEvent = [desc eventID] == kAEReopenApplication;
354     if (isAppReopenEvent
355             && (untitledWindowFlag & MMUntitledWindowOnReopen) == 0)
356         return NO;
358     // When a process is started from the command line, the 'Open' event may
359     // contain a parameter to surpress the opening of an untitled window.
360     desc = [desc paramDescriptorForKeyword:keyAEPropData];
361     desc = [desc paramDescriptorForKeyword:keyMMUntitledWindow];
362     if (desc && ![desc booleanValue])
363         return NO;
365     // Never open an untitled window if there is at least one open window or if
366     // there are processes that are currently launching.
367     if ([vimControllers count] > 0 || [pidArguments count] > 0)
368         return NO;
370     // NOTE!  This way it possible to start the app with the command-line
371     // argument '-nowindow yes' and no window will be opened by default but
372     // this argument will only be heeded when the application is opening.
373     if (isAppOpenEvent && [ud boolForKey:MMNoWindowKey] == YES)
374         return NO;
376     return YES;
379 - (BOOL)applicationOpenUntitledFile:(NSApplication *)sender
381     [self newWindow:self];
382     return YES;
385 - (void)application:(NSApplication *)sender openFiles:(NSArray *)filenames
387     // Extract ODB/Xcode/Spotlight parameters from the current Apple event,
388     // sort the filenames, and then let openFiles:withArguments: do the heavy
389     // lifting.
391     if (!(filenames && [filenames count] > 0))
392         return;
394     // Sort filenames since the Finder doesn't take care in preserving the
395     // order in which files are selected anyway (and "sorted" is more
396     // predictable than "random").
397     if ([filenames count] > 1)
398         filenames = [filenames sortedArrayUsingSelector:
399                 @selector(localizedCompare:)];
401     // Extract ODB/Xcode/Spotlight parameters from the current Apple event
402     NSMutableDictionary *arguments = [self extractArgumentsFromOdocEvent:
403             [[NSAppleEventManager sharedAppleEventManager] currentAppleEvent]];
405     if ([self openFiles:filenames withArguments:arguments]) {
406         [NSApp replyToOpenOrPrint:NSApplicationDelegateReplySuccess];
407     } else {
408         // TODO: Notify user of failure?
409         [NSApp replyToOpenOrPrint:NSApplicationDelegateReplyFailure];
410     }
413 - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender
415     return (MMTerminateWhenLastWindowClosed ==
416             [[NSUserDefaults standardUserDefaults]
417                 integerForKey:MMLastWindowClosedBehaviorKey]);
420 - (NSApplicationTerminateReply)applicationShouldTerminate:
421     (NSApplication *)sender
423     // TODO: Follow Apple's guidelines for 'Graceful Application Termination'
424     // (in particular, allow user to review changes and save).
425     int reply = NSTerminateNow;
426     BOOL modifiedBuffers = NO;
428     // Go through windows, checking for modified buffers.  (Each Vim process
429     // tells MacVim when any buffer has been modified and MacVim sets the
430     // 'documentEdited' flag of the window correspondingly.)
431     NSEnumerator *e = [[NSApp windows] objectEnumerator];
432     id window;
433     while ((window = [e nextObject])) {
434         if ([window isDocumentEdited]) {
435             modifiedBuffers = YES;
436             break;
437         }
438     }
440     if (modifiedBuffers) {
441         NSAlert *alert = [[NSAlert alloc] init];
442         [alert setAlertStyle:NSWarningAlertStyle];
443         [alert addButtonWithTitle:NSLocalizedString(@"Quit",
444                 @"Dialog button")];
445         [alert addButtonWithTitle:NSLocalizedString(@"Cancel",
446                 @"Dialog button")];
447         [alert setMessageText:NSLocalizedString(@"Quit without saving?",
448                 @"Quit dialog with changed buffers, title")];
449         [alert setInformativeText:NSLocalizedString(
450                 @"There are modified buffers, "
451                 "if you quit now all changes will be lost.  Quit anyway?",
452                 @"Quit dialog with changed buffers, text")];
454         if ([alert runModal] != NSAlertFirstButtonReturn)
455             reply = NSTerminateCancel;
457         [alert release];
458     } else {
459         // No unmodified buffers, but give a warning if there are multiple
460         // windows and/or tabs open.
461         int numWindows = [vimControllers count];
462         int numTabs = 0;
464         // Count the number of open tabs
465         e = [vimControllers objectEnumerator];
466         id vc;
467         while ((vc = [e nextObject]))
468             numTabs += [[vc objectForVimStateKey:@"numTabs"] intValue];
470         if (numWindows > 1 || numTabs > 1) {
471             NSAlert *alert = [[NSAlert alloc] init];
472             [alert setAlertStyle:NSWarningAlertStyle];
473             [alert addButtonWithTitle:NSLocalizedString(@"Quit",
474                     @"Dialog button")];
475             [alert addButtonWithTitle:NSLocalizedString(@"Cancel",
476                     @"Dialog button")];
477             [alert setMessageText:NSLocalizedString(
478                     @"Are you sure you want to quit MacVim?",
479                     @"Quit dialog with no changed buffers, title")];
481             NSString *info = nil;
482             if (numWindows > 1) {
483                 if (numTabs > numWindows)
484                     info = [NSString stringWithFormat:NSLocalizedString(
485                             @"There are %d windows open in MacVim, with a "
486                             "total of %d tabs. Do you want to quit anyway?",
487                             @"Quit dialog with no changed buffers, text"),
488                          numWindows, numTabs];
489                 else
490                     info = [NSString stringWithFormat:NSLocalizedString(
491                             @"There are %d windows open in MacVim. "
492                             "Do you want to quit anyway?",
493                             @"Quit dialog with no changed buffers, text"),
494                         numWindows];
496             } else {
497                 info = [NSString stringWithFormat:NSLocalizedString(
498                         @"There are %d tabs open in MacVim. "
499                         "Do you want to quit anyway?",
500                         @"Quit dialog with no changed buffers, text"), 
501                      numTabs];
502             }
504             [alert setInformativeText:info];
506             if ([alert runModal] != NSAlertFirstButtonReturn)
507                 reply = NSTerminateCancel;
509             [alert release];
510         }
511     }
514     // Tell all Vim processes to terminate now (otherwise they'll leave swap
515     // files behind).
516     if (NSTerminateNow == reply) {
517         e = [vimControllers objectEnumerator];
518         id vc;
519         while ((vc = [e nextObject])) {
520             //NSLog(@"Terminate pid=%d", [vc pid]);
521             [vc sendMessage:TerminateNowMsgID data:nil];
522         }
524         e = [cachedVimControllers objectEnumerator];
525         while ((vc = [e nextObject])) {
526             //NSLog(@"Terminate pid=%d (cached)", [vc pid]);
527             [vc sendMessage:TerminateNowMsgID data:nil];
528         }
530         // If a Vim process is being preloaded as we quit we have to forcibly
531         // kill it since we have not established a connection yet.
532         if (preloadPid > 0) {
533             //NSLog(@"INCOMPLETE preloaded process: preloadPid=%d", preloadPid);
534             kill(preloadPid, SIGKILL);
535         }
537         // If a Vim process was loading as we quit we also have to kill it.
538         e = [[pidArguments allKeys] objectEnumerator];
539         NSNumber *pidKey;
540         while ((pidKey = [e nextObject])) {
541             //NSLog(@"INCOMPLETE process: pid=%d", [pidKey intValue]);
542             kill([pidKey intValue], SIGKILL);
543         }
545         // Sleep a little to allow all the Vim processes to exit.
546         usleep(10000);
547     }
549     return reply;
552 - (void)applicationWillTerminate:(NSNotification *)notification
554     [self stopWatchingVimDir];
556 #ifdef MM_ENABLE_PLUGINS
557     [[MMPlugInManager sharedManager] unloadAllPlugIns];
558 #endif
560 #if MM_HANDLE_XCODE_MOD_EVENT
561     [[NSAppleEventManager sharedAppleEventManager]
562             removeEventHandlerForEventClass:'KAHL'
563                                  andEventID:'MOD '];
564 #endif
566     // This will invalidate all connections (since they were spawned from this
567     // connection).
568     [connection invalidate];
570     // Deactivate the font we loaded from the app bundle.
571     // NOTE: This can take quite a while (~500 ms), so termination will be
572     // noticeably faster if loading of the default font is disabled.
573     if (fontContainerRef) {
574         ATSFontDeactivate(fontContainerRef, NULL, kATSOptionFlagsDefault);
575         fontContainerRef = 0;
576     }
578     [NSApp setDelegate:nil];
580     // Try to wait for all child processes to avoid leaving zombies behind (but
581     // don't wait around for too long).
582     NSDate *timeOutDate = [NSDate dateWithTimeIntervalSinceNow:2];
583     while ([timeOutDate timeIntervalSinceNow] > 0) {
584         [self reapChildProcesses:nil];
585         if (numChildProcesses <= 0)
586             break;
588         //NSLog(@"%d processes still left, sleep a bit...", numChildProcesses);
590         // Run in NSConnectionReplyMode while waiting instead of calling e.g.
591         // usleep().  Otherwise incoming messages may clog up the DO queues and
592         // the outgoing TerminateNowMsgID sent earlier never reaches the Vim
593         // process.
594         // This has at least one side-effect, namely we may receive the
595         // annoying "dropping incoming DO message".  (E.g. this may happen if
596         // you quickly hit Cmd-n several times in a row and then immediately
597         // press Cmd-q, Enter.)
598         while (CFRunLoopRunInMode((CFStringRef)NSConnectionReplyMode,
599                 0.05, true) == kCFRunLoopRunHandledSource)
600             ;   // do nothing
601     }
603     if (numChildProcesses > 0)
604         NSLog(@"%d ZOMBIES left behind", numChildProcesses);
607 + (MMAppController *)sharedInstance
609     // Note: The app controller is a singleton which is instantiated in
610     // MainMenu.nib where it is also connected as the delegate of NSApp.
611     id delegate = [NSApp delegate];
612     return [delegate isKindOfClass:self] ? (MMAppController*)delegate : nil;
615 - (NSMenu *)defaultMainMenu
617     return defaultMainMenu;
620 - (NSMenuItem *)appMenuItemTemplate
622     return appMenuItemTemplate;
625 - (void)removeVimController:(id)controller
627     int idx = [vimControllers indexOfObject:controller];
628     if (NSNotFound == idx)
629         return;
631     [controller cleanup];
633     [vimControllers removeObjectAtIndex:idx];
635     if (![vimControllers count]) {
636         // The last editor window just closed so restore the main menu back to
637         // its default state (which is defined in MainMenu.nib).
638         [self setMainMenu:defaultMainMenu];
640         BOOL hide = (MMHideWhenLastWindowClosed ==
641                     [[NSUserDefaults standardUserDefaults]
642                         integerForKey:MMLastWindowClosedBehaviorKey]);
643         if (hide)
644             [NSApp hide:self];
645     }
647     // There is a small delay before the Vim process actually exits so wait a
648     // little before trying to reap the child process.  If the process still
649     // hasn't exited after this wait it won't be reaped until the next time
650     // reapChildProcesses: is called (but this should be harmless).
651     [self performSelector:@selector(reapChildProcesses:)
652                withObject:nil
653                afterDelay:0.1];
656 - (void)windowControllerWillOpen:(MMWindowController *)windowController
658     NSPoint topLeft = NSZeroPoint;
659     NSWindow *topWin = [[[self topmostVimController] windowController] window];
660     NSWindow *win = [windowController window];
662     if (!win) return;
664     // If there is a window belonging to a Vim process, cascade from it,
665     // otherwise use the autosaved window position (if any).
666     if (topWin) {
667         NSRect frame = [topWin frame];
668         topLeft = NSMakePoint(frame.origin.x, NSMaxY(frame));
669     } else {
670         NSString *topLeftString = [[NSUserDefaults standardUserDefaults]
671             stringForKey:MMTopLeftPointKey];
672         if (topLeftString)
673             topLeft = NSPointFromString(topLeftString);
674     }
676     if (!NSEqualPoints(topLeft, NSZeroPoint)) {
677         NSPoint oldTopLeft = topLeft;
678         if (topWin)
679             topLeft = [win cascadeTopLeftFromPoint:topLeft];
681         [win setFrameTopLeftPoint:topLeft];
683         if ([win screen]) {
684             NSPoint screenOrigin = [[win screen] frame].origin;
685             if ([win frame].origin.y < screenOrigin.y) {
686                 // Try to avoid shifting the new window downwards if it means
687                 // that the bottom of the window will be off the screen.  E.g.
688                 // if the user has set windows to open maximized in the
689                 // vertical direction then the new window will cascade
690                 // horizontally only.
691                 topLeft.y = oldTopLeft.y;
692                 [win setFrameTopLeftPoint:topLeft];
693             }
695             if ([win frame].origin.y < screenOrigin.y) {
696                 // Move the window to the top of the screen if the bottom of
697                 // the window is still obscured.
698                 topLeft.y = NSMaxY([[win screen] frame]);
699                 [win setFrameTopLeftPoint:topLeft];
700             }
701         } else {
702             NSLog(@"[%s] WINDOW NOT ON SCREEN, don't constrain position", _cmd);
703         }
704     }
706     if (1 == [vimControllers count]) {
707         // The first window autosaves its position.  (The autosaving
708         // features of Cocoa are not used because we need more control over
709         // what is autosaved and when it is restored.)
710         [windowController setWindowAutosaveKey:MMTopLeftPointKey];
711     }
713     if (openSelectionString) {
714         // TODO: Pass this as a parameter instead!  Get rid of
715         // 'openSelectionString' etc.
716         //
717         // There is some text to paste into this window as a result of the
718         // services menu "Open selection ..." being used.
719         [[windowController vimController] dropString:openSelectionString];
720         [openSelectionString release];
721         openSelectionString = nil;
722     }
724     if (shouldActivateWhenNextWindowOpens) {
725         [NSApp activateIgnoringOtherApps:YES];
726         shouldActivateWhenNextWindowOpens = NO;
727     }
730 - (void)setMainMenu:(NSMenu *)mainMenu
732     if ([NSApp mainMenu] == mainMenu) return;
734     // If the new menu has a "Recent Files" dummy item, then swap the real item
735     // for the dummy.  We are forced to do this since Cocoa initializes the
736     // "Recent Files" menu and there is no way to simply point Cocoa to a new
737     // item each time the menus are swapped.
738     NSMenu *fileMenu = [mainMenu findFileMenu];
739     if (recentFilesMenuItem && fileMenu) {
740         int dummyIdx =
741                 [fileMenu indexOfItemWithAction:@selector(recentFilesDummy:)];
742         if (dummyIdx >= 0) {
743             NSMenuItem *dummyItem = [[fileMenu itemAtIndex:dummyIdx] retain];
744             [fileMenu removeItemAtIndex:dummyIdx];
746             NSMenu *recentFilesParentMenu = [recentFilesMenuItem menu];
747             int idx = [recentFilesParentMenu indexOfItem:recentFilesMenuItem];
748             if (idx >= 0) {
749                 [[recentFilesMenuItem retain] autorelease];
750                 [recentFilesParentMenu removeItemAtIndex:idx];
751                 [recentFilesParentMenu insertItem:dummyItem atIndex:idx];
752             }
754             [fileMenu insertItem:recentFilesMenuItem atIndex:dummyIdx];
755             [dummyItem release];
756         }
757     }
759     // Now set the new menu.  Notice that we keep one menu for each editor
760     // window since each editor can have its own set of menus.  When swapping
761     // menus we have to tell Cocoa where the new "MacVim", "Windows", and
762     // "Services" menu are.
763     [NSApp setMainMenu:mainMenu];
765     // Setting the "MacVim" (or "Application") menu ensures that it is typeset
766     // in boldface.  (The setAppleMenu: method used to be public but is now
767     // private so this will have to be considered a bit of a hack!)
768     NSMenu *appMenu = [mainMenu findApplicationMenu];
769     [NSApp performSelector:@selector(setAppleMenu:) withObject:appMenu];
771     NSMenu *servicesMenu = [mainMenu findServicesMenu];
772     [NSApp setServicesMenu:servicesMenu];
774     NSMenu *windowsMenu = [mainMenu findWindowsMenu];
775     if (windowsMenu) {
776         // Cocoa isn't clever enough to get rid of items it has added to the
777         // "Windows" menu so we have to do it ourselves otherwise there will be
778         // multiple menu items for each window in the "Windows" menu.
779         //   This code assumes that the only items Cocoa add are ones which
780         // send off the action makeKeyAndOrderFront:.  (Cocoa will not add
781         // another separator item if the last item on the "Windows" menu
782         // already is a separator, so we needen't worry about separators.)
783         int i, count = [windowsMenu numberOfItems];
784         for (i = count-1; i >= 0; --i) {
785             NSMenuItem *item = [windowsMenu itemAtIndex:i];
786             if ([item action] == @selector(makeKeyAndOrderFront:))
787                 [windowsMenu removeItem:item];
788         }
789     }
790     [NSApp setWindowsMenu:windowsMenu];
792 #ifdef MM_ENABLE_PLUGINS
793     // Move plugin menu from old to new main menu.
794     [self removePlugInMenu];
795     [self addPlugInMenuToMenu:mainMenu];
796 #endif
799 - (NSArray *)filterOpenFiles:(NSArray *)filenames
801     return [self filterOpenFiles:filenames openFilesDict:nil];
804 - (BOOL)openFiles:(NSArray *)filenames withArguments:(NSDictionary *)args
806     // Opening files works like this:
807     //  a) filter out any already open files
808     //  b) open any remaining files
809     //
810     // A file is opened in an untitled window if there is one (it may be
811     // currently launching, or it may already be visible), otherwise a new
812     // window is opened.
813     //
814     // Each launching Vim process has a dictionary of arguments that are passed
815     // to the process when in checks in (via connectBackend:pid:).  The
816     // arguments for each launching process can be looked up by its PID (in the
817     // pidArguments dictionary).
819     NSMutableDictionary *arguments = (args ? [[args mutableCopy] autorelease]
820                                            : [NSMutableDictionary dictionary]);
822     //
823     // a) Filter out any already open files
824     //
825     NSString *firstFile = [filenames objectAtIndex:0];
826     MMVimController *firstController = nil;
827     NSDictionary *openFilesDict = nil;
828     filenames = [self filterOpenFiles:filenames openFilesDict:&openFilesDict];
830     // Pass arguments to vim controllers that had files open.
831     id key;
832     NSEnumerator *e = [openFilesDict keyEnumerator];
834     // (Indicate that we do not wish to open any files at the moment.)
835     [arguments setObject:[NSNumber numberWithBool:YES] forKey:@"dontOpen"];
837     while ((key = [e nextObject])) {
838         NSArray *files = [openFilesDict objectForKey:key];
839         [arguments setObject:files forKey:@"filenames"];
841         MMVimController *vc = [key pointerValue];
842         [vc passArguments:arguments];
844         // If this controller holds the first file, then remember it for later.
845         if ([files containsObject:firstFile])
846             firstController = vc;
847     }
849     // The meaning of "layout" is defined by the WIN_* defines in main.c.
850     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
851     int layout = [ud integerForKey:MMOpenLayoutKey];
852     BOOL splitVert = [ud boolForKey:MMVerticalSplitKey];
853     BOOL openInCurrentWindow = [ud boolForKey:MMOpenInCurrentWindowKey];
855     if (splitVert && MMLayoutHorizontalSplit == layout)
856         layout = MMLayoutVerticalSplit;
857     if (layout < 0 || (layout > MMLayoutTabs && openInCurrentWindow))
858         layout = MMLayoutTabs;
860     if ([filenames count] == 0) {
861         // Raise the window containing the first file that was already open,
862         // and make sure that the tab containing that file is selected.  Only
863         // do this when there are no more files to open, otherwise sometimes
864         // the window with 'firstFile' will be raised, other times it might be
865         // the window that will open with the files in the 'filenames' array.
866         firstFile = [firstFile stringByEscapingSpecialFilenameCharacters];
868         NSString *bufCmd = @"tab sb";
869         switch (layout) {
870             case MMLayoutHorizontalSplit: bufCmd = @"sb"; break;
871             case MMLayoutVerticalSplit:   bufCmd = @"vert sb"; break;
872             case MMLayoutArglist:         bufCmd = @"b"; break;
873         }
875         NSString *input = [NSString stringWithFormat:@"<C-\\><C-N>"
876                 ":let oldswb=&swb|let &swb=\"useopen,usetab\"|"
877                 "%@ %@|let &swb=oldswb|unl oldswb|"
878                 "cal foreground()<CR>", bufCmd, firstFile];
880         [firstController addVimInput:input];
882         return YES;
883     }
885     // Add filenames to "Recent Files" menu, unless they are being edited
886     // remotely (using ODB).
887     if ([arguments objectForKey:@"remoteID"] == nil) {
888         [[NSDocumentController sharedDocumentController]
889                 noteNewRecentFilePaths:filenames];
890     }
892     //
893     // b) Open any remaining files
894     //
896     [arguments setObject:[NSNumber numberWithInt:layout] forKey:@"layout"];
897     [arguments setObject:filenames forKey:@"filenames"];
898     // (Indicate that files should be opened from now on.)
899     [arguments setObject:[NSNumber numberWithBool:NO] forKey:@"dontOpen"];
901     MMVimController *vc;
902     if (openInCurrentWindow && (vc = [self topmostVimController])) {
903         // Open files in an already open window.
904         [[[vc windowController] window] makeKeyAndOrderFront:self];
905         [vc passArguments:arguments];
906         return YES;
907     }
909     BOOL openOk = YES;
910     int numFiles = [filenames count];
911     if (MMLayoutWindows == layout && numFiles > 1) {
912         // Open one file at a time in a new window, but don't open too many at
913         // once (at most cap+1 windows will open).  If the user has increased
914         // the preload cache size we'll take that as a hint that more windows
915         // should be able to open at once.
916         int cap = [self maxPreloadCacheSize] - 1;
917         if (cap < 4) cap = 4;
918         if (cap > numFiles) cap = numFiles;
920         int i;
921         for (i = 0; i < cap; ++i) {
922             NSArray *a = [NSArray arrayWithObject:[filenames objectAtIndex:i]];
923             [arguments setObject:a forKey:@"filenames"];
925             // NOTE: We have to copy the args since we'll mutate them in the
926             // next loop and the below call may retain the arguments while
927             // waiting for a process to start.
928             NSDictionary *args = [[arguments copy] autorelease];
930             openOk = [self openVimControllerWithArguments:args];
931             if (!openOk) break;
932         }
934         // Open remaining files in tabs in a new window.
935         if (openOk && numFiles > cap) {
936             NSRange range = { i, numFiles-cap };
937             NSArray *a = [filenames subarrayWithRange:range];
938             [arguments setObject:a forKey:@"filenames"];
939             [arguments setObject:[NSNumber numberWithInt:MMLayoutTabs]
940                           forKey:@"layout"];
942             openOk = [self openVimControllerWithArguments:arguments];
943         }
944     } else {
945         // Open all files at once.
946         openOk = [self openVimControllerWithArguments:arguments];
947     }
949     return openOk;
952 #ifdef MM_ENABLE_PLUGINS
953 - (void)addItemToPlugInMenu:(NSMenuItem *)item
955     NSMenu *menu = [plugInMenuItem submenu];
956     [menu addItem:item];
957     if ([menu numberOfItems] == 1)
958         [self addPlugInMenuToMenu:[NSApp mainMenu]];
961 - (void)removeItemFromPlugInMenu:(NSMenuItem *)item
963     NSMenu *menu = [plugInMenuItem submenu];
964     [menu removeItem:item];
965     if ([menu numberOfItems] == 0)
966         [self removePlugInMenu];
968 #endif
970 - (IBAction)newWindow:(id)sender
972     // A cached controller requires no loading times and results in the new
973     // window popping up instantaneously.  If the cache is empty it may take
974     // 1-2 seconds to start a new Vim process.
975     MMVimController *vc = [self takeVimControllerFromCache];
976     if (vc) {
977         [[vc backendProxy] acknowledgeConnection];
978     } else {
979         [self launchVimProcessWithArguments:nil];
980     }
983 - (IBAction)newWindowAndActivate:(id)sender
985     [self activateWhenNextWindowOpens];
986     [self newWindow:sender];
989 - (IBAction)fileOpen:(id)sender
991     NSString *dir = nil;
992     BOOL trackPwd = [[NSUserDefaults standardUserDefaults]
993             boolForKey:MMDialogsTrackPwdKey];
994     if (trackPwd) {
995         MMVimController *vc = [self keyVimController];
996         if (vc) dir = [vc objectForVimStateKey:@"pwd"];
997     }
999     NSOpenPanel *panel = [NSOpenPanel openPanel];
1000     [panel setAllowsMultipleSelection:YES];
1001     [panel setAccessoryView:showHiddenFilesView()];
1003     int result = [panel runModalForDirectory:dir file:nil types:nil];
1004     if (NSOKButton == result)
1005         [self application:NSApp openFiles:[panel filenames]];
1008 - (IBAction)selectNextWindow:(id)sender
1010     unsigned i, count = [vimControllers count];
1011     if (!count) return;
1013     NSWindow *keyWindow = [NSApp keyWindow];
1014     for (i = 0; i < count; ++i) {
1015         MMVimController *vc = [vimControllers objectAtIndex:i];
1016         if ([[[vc windowController] window] isEqual:keyWindow])
1017             break;
1018     }
1020     if (i < count) {
1021         if (++i >= count)
1022             i = 0;
1023         MMVimController *vc = [vimControllers objectAtIndex:i];
1024         [[vc windowController] showWindow:self];
1025     }
1028 - (IBAction)selectPreviousWindow:(id)sender
1030     unsigned i, count = [vimControllers count];
1031     if (!count) return;
1033     NSWindow *keyWindow = [NSApp keyWindow];
1034     for (i = 0; i < count; ++i) {
1035         MMVimController *vc = [vimControllers objectAtIndex:i];
1036         if ([[[vc windowController] window] isEqual:keyWindow])
1037             break;
1038     }
1040     if (i < count) {
1041         if (i > 0) {
1042             --i;
1043         } else {
1044             i = count - 1;
1045         }
1046         MMVimController *vc = [vimControllers objectAtIndex:i];
1047         [[vc windowController] showWindow:self];
1048     }
1051 - (IBAction)orderFrontPreferencePanel:(id)sender
1053     [[MMPreferenceController sharedPrefsWindowController] showWindow:self];
1056 - (IBAction)openWebsite:(id)sender
1058     [[NSWorkspace sharedWorkspace] openURL:
1059             [NSURL URLWithString:MMWebsiteString]];
1062 - (IBAction)showVimHelp:(id)sender
1064     // Open a new window with the help window maximized.
1065     [self launchVimProcessWithArguments:[NSArray arrayWithObjects:
1066             @"-c", @":h gui_mac", @"-c", @":res", nil]];
1069 - (IBAction)zoomAll:(id)sender
1071     [NSApp makeWindowsPerform:@selector(performZoom:) inOrder:YES];
1074 - (IBAction)atsuiButtonClicked:(id)sender
1076     // This action is called when the user clicks the "use ATSUI renderer"
1077     // button in the advanced preferences pane.
1078     [self rebuildPreloadCache];
1081 - (IBAction)loginShellButtonClicked:(id)sender
1083     // This action is called when the user clicks the "use login shell" button
1084     // in the advanced preferences pane.
1085     [self rebuildPreloadCache];
1088 - (IBAction)quickstartButtonClicked:(id)sender
1090     if ([self maxPreloadCacheSize] > 0) {
1091         [self scheduleVimControllerPreloadAfterDelay:1.0];
1092         [self startWatchingVimDir];
1093     } else {
1094         [self cancelVimControllerPreloadRequests];
1095         [self clearPreloadCacheWithCount:-1];
1096         [self stopWatchingVimDir];
1097     }
1100 - (MMVimController *)keyVimController
1102     NSWindow *keyWindow = [NSApp keyWindow];
1103     if (keyWindow) {
1104         unsigned i, count = [vimControllers count];
1105         for (i = 0; i < count; ++i) {
1106             MMVimController *vc = [vimControllers objectAtIndex:i];
1107             if ([[[vc windowController] window] isEqual:keyWindow])
1108                 return vc;
1109         }
1110     }
1112     return nil;
1115 - (unsigned)connectBackend:(byref in id <MMBackendProtocol>)proxy pid:(int)pid
1117     //NSLog(@"[%s] pid=%d", _cmd, pid);
1119     [(NSDistantObject*)proxy setProtocolForProxy:@protocol(MMBackendProtocol)];
1121     // NOTE: Allocate the vim controller now but don't add it to the list of
1122     // controllers since this is a distributed object call and as such can
1123     // arrive at unpredictable times (e.g. while iterating the list of vim
1124     // controllers).
1125     // (What if input arrives before the vim controller is added to the list of
1126     // controllers?  This should not be a problem since the input isn't
1127     // processed immediately (see processInput:forIdentifier:).)
1128     MMVimController *vc = [[MMVimController alloc] initWithBackend:proxy
1129                                                                pid:pid];
1130     [self performSelector:@selector(addVimController:)
1131                withObject:vc
1132                afterDelay:0];
1134     [vc release];
1136     return [vc identifier];
1139 - (oneway void)processInput:(in bycopy NSArray *)queue
1140               forIdentifier:(unsigned)identifier
1142     // NOTE: Input is not handled immediately since this is a distribued object
1143     // call and as such can arrive at unpredictable times.  Instead, queue the
1144     // input and process it when the run loop is updated.
1146     if (!(queue && identifier)) {
1147         NSLog(@"[%s] Bad input for identifier=%d", _cmd, identifier);
1148         return;
1149     }
1151     //NSLog(@"[%s] QUEUE for identifier=%d: <<< %@>>>", _cmd, identifier,
1152     //        debugStringForMessageQueue(queue));
1154     NSNumber *key = [NSNumber numberWithUnsignedInt:identifier];
1155     NSArray *q = [inputQueues objectForKey:key];
1156     if (q) {
1157         q = [q arrayByAddingObjectsFromArray:queue];
1158         [inputQueues setObject:q forKey:key];
1159     } else {
1160         [inputQueues setObject:queue forKey:key];
1161     }
1163     // NOTE: We must use "event tracking mode" as well as "default mode",
1164     // otherwise the input queue will not be processed e.g. during live
1165     // resizing.
1166     [self performSelector:@selector(processInputQueues:)
1167                withObject:nil
1168                afterDelay:0
1169                   inModes:[NSArray arrayWithObjects:NSDefaultRunLoopMode,
1170                                             NSEventTrackingRunLoopMode, nil]];
1173 - (NSArray *)serverList
1175     NSMutableArray *array = [NSMutableArray array];
1177     unsigned i, count = [vimControllers count];
1178     for (i = 0; i < count; ++i) {
1179         MMVimController *controller = [vimControllers objectAtIndex:i];
1180         if ([controller serverName])
1181             [array addObject:[controller serverName]];
1182     }
1184     return array;
1187 @end // MMAppController
1192 @implementation MMAppController (MMServices)
1194 - (void)openSelection:(NSPasteboard *)pboard userData:(NSString *)userData
1195                 error:(NSString **)error
1197     if (![[pboard types] containsObject:NSStringPboardType]) {
1198         NSLog(@"WARNING: Pasteboard contains no object of type "
1199                 "NSStringPboardType");
1200         return;
1201     }
1203     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
1204     BOOL openInCurrentWindow = [ud boolForKey:MMOpenInCurrentWindowKey];
1205     MMVimController *vc;
1207     if (openInCurrentWindow && (vc = [self topmostVimController])) {
1208         [vc sendMessage:AddNewTabMsgID data:nil];
1209         [vc dropString:[pboard stringForType:NSStringPboardType]];
1210     } else {
1211         // Save the text, open a new window, and paste the text when the next
1212         // window opens.  (If this is called several times in a row, then all
1213         // but the last call may be ignored.)
1214         if (openSelectionString) [openSelectionString release];
1215         openSelectionString = [[pboard stringForType:NSStringPboardType] copy];
1217         [self newWindow:self];
1218     }
1221 - (void)openFile:(NSPasteboard *)pboard userData:(NSString *)userData
1222            error:(NSString **)error
1224     if (![[pboard types] containsObject:NSStringPboardType]) {
1225         NSLog(@"WARNING: Pasteboard contains no object of type "
1226                 "NSStringPboardType");
1227         return;
1228     }
1230     // TODO: Parse multiple filenames and create array with names.
1231     NSString *string = [pboard stringForType:NSStringPboardType];
1232     string = [string stringByTrimmingCharactersInSet:
1233             [NSCharacterSet whitespaceAndNewlineCharacterSet]];
1234     string = [string stringByStandardizingPath];
1236     NSArray *filenames = [self filterFilesAndNotify:
1237             [NSArray arrayWithObject:string]];
1238     if ([filenames count] == 0)
1239         return;
1241     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
1242     BOOL openInCurrentWindow = [ud boolForKey:MMOpenInCurrentWindowKey];
1243     MMVimController *vc;
1245     if (openInCurrentWindow && (vc = [self topmostVimController])) {
1246         [vc dropFiles:filenames forceOpen:YES];
1247     } else {
1248         [self openFiles:filenames withArguments:nil];
1249     }
1252 - (void)newFileHere:(NSPasteboard *)pboard userData:(NSString *)userData
1253               error:(NSString **)error
1255     if (![[pboard types] containsObject:NSStringPboardType]) {
1256         NSLog(@"WARNING: Pasteboard contains no object of type "
1257               "NSStringPboardType");
1258         return;
1259     }
1261     NSString *path = [pboard stringForType:NSStringPboardType];
1263     BOOL dirIndicator;
1264     if (![[NSFileManager defaultManager] fileExistsAtPath:path
1265                                               isDirectory:&dirIndicator]) {
1266         NSLog(@"Invalid path. Cannot open new document at: %@", path);
1267         return;
1268     }
1270     if (!dirIndicator)
1271         path = [path stringByDeletingLastPathComponent];
1273     path = [path stringByEscapingSpecialFilenameCharacters];
1275     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
1276     BOOL openInCurrentWindow = [ud boolForKey:MMOpenInCurrentWindowKey];
1277     MMVimController *vc;
1279     if (openInCurrentWindow && (vc = [self topmostVimController])) {
1280         NSString *input = [NSString stringWithFormat:@"<C-\\><C-N>"
1281                 ":tabe|cd %@<CR>", path];
1282         [vc addVimInput:input];
1283     } else {
1284         NSString *input = [NSString stringWithFormat:@":cd %@", path];
1285         [self launchVimProcessWithArguments:[NSArray arrayWithObjects:
1286                                              @"-c", input, nil]];
1287     }
1290 @end // MMAppController (MMServices)
1295 @implementation MMAppController (Private)
1297 - (MMVimController *)topmostVimController
1299     // Find the topmost visible window which has an associated vim controller.
1300     NSEnumerator *e = [[NSApp orderedWindows] objectEnumerator];
1301     id window;
1302     while ((window = [e nextObject]) && [window isVisible]) {
1303         unsigned i, count = [vimControllers count];
1304         for (i = 0; i < count; ++i) {
1305             MMVimController *vc = [vimControllers objectAtIndex:i];
1306             if ([[[vc windowController] window] isEqual:window])
1307                 return vc;
1308         }
1309     }
1311     return nil;
1314 - (int)launchVimProcessWithArguments:(NSArray *)args
1316     int pid = -1;
1317     NSString *path = [[NSBundle mainBundle] pathForAuxiliaryExecutable:@"Vim"];
1319     if (!path) {
1320         NSLog(@"ERROR: Vim executable could not be found inside app bundle!");
1321         return -1;
1322     }
1324     NSArray *taskArgs = [NSArray arrayWithObjects:@"-g", @"-f", nil];
1325     if (args)
1326         taskArgs = [taskArgs arrayByAddingObjectsFromArray:args];
1328     BOOL useLoginShell = [[NSUserDefaults standardUserDefaults]
1329             boolForKey:MMLoginShellKey];
1330     if (useLoginShell) {
1331         // Run process with a login shell, roughly:
1332         //   echo "exec Vim -g -f args" | ARGV0=-`basename $SHELL` $SHELL [-l]
1333         pid = [self executeInLoginShell:path arguments:taskArgs];
1334     } else {
1335         // Run process directly:
1336         //   Vim -g -f args
1337         NSTask *task = [NSTask launchedTaskWithLaunchPath:path
1338                                                 arguments:taskArgs];
1339         pid = task ? [task processIdentifier] : -1;
1340     }
1342     if (-1 != pid) {
1343         // The 'pidArguments' dictionary keeps arguments to be passed to the
1344         // process when it connects (this is in contrast to arguments which are
1345         // passed on the command line, like '-f' and '-g').
1346         // If this method is called with nil arguments we take this as a hint
1347         // that this is an "untitled window" being launched and add a null
1348         // object to the 'pidArguments' dictionary.  This way we can detect if
1349         // an untitled window is being launched by looking for null objects in
1350         // this dictionary.
1351         // If this method is called with non-nil arguments then it is assumed
1352         // that the caller takes care of adding items to 'pidArguments' as
1353         // necessary (only some arguments are passed on connect, e.g. files to
1354         // open).
1355         if (!args)
1356             [pidArguments setObject:[NSNull null]
1357                              forKey:[NSNumber numberWithInt:pid]];
1358     } else {
1359         NSLog(@"WARNING: %s%@ failed (useLoginShell=%d)", _cmd, args,
1360                 useLoginShell);
1361     }
1363     return pid;
1366 - (NSArray *)filterFilesAndNotify:(NSArray *)filenames
1368     // Go trough 'filenames' array and make sure each file exists.  Present
1369     // warning dialog if some file was missing.
1371     NSString *firstMissingFile = nil;
1372     NSMutableArray *files = [NSMutableArray array];
1373     unsigned i, count = [filenames count];
1375     for (i = 0; i < count; ++i) {
1376         NSString *name = [filenames objectAtIndex:i];
1377         if ([[NSFileManager defaultManager] fileExistsAtPath:name]) {
1378             [files addObject:name];
1379         } else if (!firstMissingFile) {
1380             firstMissingFile = name;
1381         }
1382     }
1384     if (firstMissingFile) {
1385         NSAlert *alert = [[NSAlert alloc] init];
1386         [alert addButtonWithTitle:NSLocalizedString(@"OK",
1387                 @"Dialog button")];
1389         NSString *text;
1390         if ([files count] >= count-1) {
1391             [alert setMessageText:NSLocalizedString(@"File not found",
1392                     @"File not found dialog, title")];
1393             text = [NSString stringWithFormat:NSLocalizedString(
1394                     @"Could not open file with name %@.",
1395                     @"File not found dialog, text"), firstMissingFile];
1396         } else {
1397             [alert setMessageText:NSLocalizedString(@"Multiple files not found",
1398                     @"File not found dialog, title")];
1399             text = [NSString stringWithFormat:NSLocalizedString(
1400                     @"Could not open file with name %@, and %d other files.",
1401                     @"File not found dialog, text"),
1402                 firstMissingFile, count-[files count]-1];
1403         }
1405         [alert setInformativeText:text];
1406         [alert setAlertStyle:NSWarningAlertStyle];
1408         [alert runModal];
1409         [alert release];
1411         [NSApp replyToOpenOrPrint:NSApplicationDelegateReplyFailure];
1412     }
1414     return files;
1417 - (NSArray *)filterOpenFiles:(NSArray *)filenames
1418                openFilesDict:(NSDictionary **)openFiles
1420     // Filter out any files in the 'filenames' array that are open and return
1421     // all files that are not already open.  On return, the 'openFiles'
1422     // parameter (if non-nil) will point to a dictionary of open files, indexed
1423     // by Vim controller.
1425     NSMutableDictionary *dict = [NSMutableDictionary dictionary];
1426     NSMutableArray *files = [filenames mutableCopy];
1428     // TODO: Escape special characters in 'files'?
1429     NSString *expr = [NSString stringWithFormat:
1430             @"map([\"%@\"],\"bufloaded(v:val)\")",
1431             [files componentsJoinedByString:@"\",\""]];
1433     unsigned i, count = [vimControllers count];
1434     for (i = 0; i < count && [files count] > 0; ++i) {
1435         MMVimController *vc = [vimControllers objectAtIndex:i];
1437         // Query Vim for which files in the 'files' array are open.
1438         NSString *eval = [vc evaluateVimExpression:expr];
1439         if (!eval) continue;
1441         NSIndexSet *idxSet = [NSIndexSet indexSetWithVimList:eval];
1442         if ([idxSet count] > 0) {
1443             [dict setObject:[files objectsAtIndexes:idxSet]
1444                      forKey:[NSValue valueWithPointer:vc]];
1446             // Remove all the files that were open in this Vim process and
1447             // create a new expression to evaluate.
1448             [files removeObjectsAtIndexes:idxSet];
1449             expr = [NSString stringWithFormat:
1450                     @"map([\"%@\"],\"bufloaded(v:val)\")",
1451                     [files componentsJoinedByString:@"\",\""]];
1452         }
1453     }
1455     if (openFiles != nil)
1456         *openFiles = dict;
1458     return files;
1461 #if MM_HANDLE_XCODE_MOD_EVENT
1462 - (void)handleXcodeModEvent:(NSAppleEventDescriptor *)event
1463                  replyEvent:(NSAppleEventDescriptor *)reply
1465 #if 0
1466     // Xcode sends this event to query MacVim which open files have been
1467     // modified.
1468     NSLog(@"reply:%@", reply);
1469     NSLog(@"event:%@", event);
1471     NSEnumerator *e = [vimControllers objectEnumerator];
1472     id vc;
1473     while ((vc = [e nextObject])) {
1474         DescType type = [reply descriptorType];
1475         unsigned len = [[type data] length];
1476         NSMutableData *data = [NSMutableData data];
1478         [data appendBytes:&type length:sizeof(DescType)];
1479         [data appendBytes:&len length:sizeof(unsigned)];
1480         [data appendBytes:[reply data] length:len];
1482         [vc sendMessage:XcodeModMsgID data:data];
1483     }
1484 #endif
1486 #endif
1488 - (void)handleGetURLEvent:(NSAppleEventDescriptor *)event
1489                replyEvent:(NSAppleEventDescriptor *)reply
1491     NSString *urlString = [[event paramDescriptorForKeyword:keyDirectObject]
1492         stringValue];
1493     NSURL *url = [NSURL URLWithString:urlString];
1495     // We try to be compatible with TextMate's URL scheme here, as documented
1496     // at http://blog.macromates.com/2007/the-textmate-url-scheme/ . Currently,
1497     // this means that:
1498     //
1499     // The format is: mvim://open?<arguments> where arguments can be:
1500     //
1501     // * url â€” the actual file to open (i.e. a file://… URL), if you leave
1502     //         out this argument, the frontmost document is implied.
1503     // * line â€” line number to go to (one based).
1504     // * column â€” column number to go to (one based).
1505     //
1506     // Example: mvim://open?url=file:///etc/profile&line=20
1508     if ([[url host] isEqualToString:@"open"]) {
1509         NSMutableDictionary *dict = [NSMutableDictionary dictionary];
1511         // Parse query ("url=file://...&line=14") into a dictionary
1512         NSArray *queries = [[url query] componentsSeparatedByString:@"&"];
1513         NSEnumerator *enumerator = [queries objectEnumerator];
1514         NSString *param;
1515         while( param = [enumerator nextObject] ) {
1516             NSArray *arr = [param componentsSeparatedByString:@"="];
1517             if ([arr count] == 2) {
1518                 [dict setValue:[[arr lastObject]
1519                             stringByReplacingPercentEscapesUsingEncoding:
1520                                 NSUTF8StringEncoding]
1521                         forKey:[[arr objectAtIndex:0]
1522                             stringByReplacingPercentEscapesUsingEncoding:
1523                                 NSUTF8StringEncoding]];
1524             }
1525         }
1527         // Actually open the file.
1528         NSString *file = [dict objectForKey:@"url"];
1529         if (file != nil) {
1530             NSURL *fileUrl= [NSURL URLWithString:file];
1531             // TextMate only opens files that already exist.
1532             if ([fileUrl isFileURL]
1533                     && [[NSFileManager defaultManager] fileExistsAtPath:
1534                            [fileUrl path]]) {
1535                 // Strip 'file://' path, else application:openFiles: might think
1536                 // the file is not yet open.
1537                 NSArray *filenames = [NSArray arrayWithObject:[fileUrl path]];
1539                 // Look for the line and column options.
1540                 NSDictionary *args = nil;
1541                 NSString *line = [dict objectForKey:@"line"];
1542                 if (line) {
1543                     NSString *column = [dict objectForKey:@"column"];
1544                     if (column)
1545                         args = [NSDictionary dictionaryWithObjectsAndKeys:
1546                                 line, @"cursorLine",
1547                                 column, @"cursorColumn",
1548                                 nil];
1549                     else
1550                         args = [NSDictionary dictionaryWithObject:line
1551                                 forKey:@"cursorLine"];
1552                 }
1554                 [self openFiles:filenames withArguments:args];
1555             }
1556         }
1557     } else {
1558         NSAlert *alert = [[NSAlert alloc] init];
1559         [alert addButtonWithTitle:NSLocalizedString(@"OK",
1560             @"Dialog button")];
1562         [alert setMessageText:NSLocalizedString(@"Unknown URL Scheme",
1563             @"Unknown URL Scheme dialog, title")];
1564         [alert setInformativeText:[NSString stringWithFormat:NSLocalizedString(
1565             @"This version of MacVim does not support \"%@\""
1566             @" in its URL scheme.",
1567             @"Unknown URL Scheme dialog, text"),
1568             [url host]]];
1570         [alert setAlertStyle:NSWarningAlertStyle];
1571         [alert runModal];
1572         [alert release];
1573     }
1577 - (int)findLaunchingProcessWithoutArguments
1579     NSArray *keys = [pidArguments allKeysForObject:[NSNull null]];
1580     if ([keys count] > 0) {
1581         //NSLog(@"found launching process without arguments");
1582         return [[keys objectAtIndex:0] intValue];
1583     }
1585     return -1;
1588 - (MMVimController *)findUnusedEditor
1590     NSEnumerator *e = [vimControllers objectEnumerator];
1591     id vc;
1592     while ((vc = [e nextObject])) {
1593         if ([[vc objectForVimStateKey:@"unusedEditor"] boolValue])
1594             return vc;
1595     }
1597     return nil;
1600 - (NSMutableDictionary *)extractArgumentsFromOdocEvent:
1601     (NSAppleEventDescriptor *)desc
1603     NSMutableDictionary *dict = [NSMutableDictionary dictionary];
1605     // 1. Extract ODB parameters (if any)
1606     NSAppleEventDescriptor *odbdesc = desc;
1607     if (![odbdesc paramDescriptorForKeyword:keyFileSender]) {
1608         // The ODB paramaters may hide inside the 'keyAEPropData' descriptor.
1609         odbdesc = [odbdesc paramDescriptorForKeyword:keyAEPropData];
1610         if (![odbdesc paramDescriptorForKeyword:keyFileSender])
1611             odbdesc = nil;
1612     }
1614     if (odbdesc) {
1615         NSAppleEventDescriptor *p =
1616                 [odbdesc paramDescriptorForKeyword:keyFileSender];
1617         if (p)
1618             [dict setObject:[NSNumber numberWithUnsignedInt:[p typeCodeValue]]
1619                      forKey:@"remoteID"];
1621         p = [odbdesc paramDescriptorForKeyword:keyFileCustomPath];
1622         if (p)
1623             [dict setObject:[p stringValue] forKey:@"remotePath"];
1625         p = [odbdesc paramDescriptorForKeyword:keyFileSenderToken];
1626         if (p) {
1627             [dict setObject:[NSNumber numberWithUnsignedLong:[p descriptorType]]
1628                      forKey:@"remoteTokenDescType"];
1629             [dict setObject:[p data] forKey:@"remoteTokenData"];
1630         }
1631     }
1633     // 2. Extract Xcode parameters (if any)
1634     NSAppleEventDescriptor *xcodedesc =
1635             [desc paramDescriptorForKeyword:keyAEPosition];
1636     if (xcodedesc) {
1637         NSRange range;
1638         MMSelectionRange *sr = (MMSelectionRange*)[[xcodedesc data] bytes];
1640         if (sr->lineNum < 0) {
1641             // Should select a range of lines.
1642             range.location = sr->startRange + 1;
1643             range.length = sr->endRange - sr->startRange + 1;
1644         } else {
1645             // Should only move cursor to a line.
1646             range.location = sr->lineNum + 1;
1647             range.length = 0;
1648         }
1650         [dict setObject:NSStringFromRange(range) forKey:@"selectionRange"];
1651     }
1653     // 3. Extract Spotlight search text (if any)
1654     NSAppleEventDescriptor *spotlightdesc = 
1655             [desc paramDescriptorForKeyword:keyAESearchText];
1656     if (spotlightdesc)
1657         [dict setObject:[spotlightdesc stringValue] forKey:@"searchText"];
1659     return dict;
1662 #ifdef MM_ENABLE_PLUGINS
1663 - (void)removePlugInMenu
1665     if ([plugInMenuItem menu])
1666         [[plugInMenuItem menu] removeItem:plugInMenuItem];
1669 - (void)addPlugInMenuToMenu:(NSMenu *)mainMenu
1671     NSMenu *windowsMenu = [mainMenu findWindowsMenu];
1673     if ([[plugInMenuItem submenu] numberOfItems] > 0) {
1674         int idx = windowsMenu ? [mainMenu indexOfItemWithSubmenu:windowsMenu]
1675                               : -1;
1676         if (idx > 0) {
1677             [mainMenu insertItem:plugInMenuItem atIndex:idx];
1678         } else {
1679             [mainMenu addItem:plugInMenuItem];
1680         }
1681     }
1683 #endif
1685 - (void)scheduleVimControllerPreloadAfterDelay:(NSTimeInterval)delay
1687     [self performSelector:@selector(preloadVimController:)
1688                withObject:nil
1689                afterDelay:delay];
1692 - (void)cancelVimControllerPreloadRequests
1694     [NSObject cancelPreviousPerformRequestsWithTarget:self
1695             selector:@selector(preloadVimController:)
1696               object:nil];
1699 - (void)preloadVimController:(id)sender
1701     // We only allow preloading of one Vim process at a time (to avoid hogging
1702     // CPU), so schedule another preload in a little while if necessary.
1703     if (-1 != preloadPid) {
1704         [self scheduleVimControllerPreloadAfterDelay:2];
1705         return;
1706     }
1708     if ([cachedVimControllers count] >= [self maxPreloadCacheSize])
1709         return;
1711     preloadPid = [self launchVimProcessWithArguments:
1712             [NSArray arrayWithObject:@"--mmwaitforack"]];
1715 - (int)maxPreloadCacheSize
1717     // The maximum number of Vim processes to keep in the cache can be
1718     // controlled via the user default "MMPreloadCacheSize".
1719     int maxCacheSize = [[NSUserDefaults standardUserDefaults]
1720             integerForKey:MMPreloadCacheSizeKey];
1721     if (maxCacheSize < 0) maxCacheSize = 0;
1722     else if (maxCacheSize > 10) maxCacheSize = 10;
1724     return maxCacheSize;
1727 - (MMVimController *)takeVimControllerFromCache
1729     // NOTE: After calling this message the backend corresponding to the
1730     // returned vim controller must be sent an acknowledgeConnection message,
1731     // else the vim process will be stuck.
1732     //
1733     // This method may return nil even though the cache might be non-empty; the
1734     // caller should handle this by starting a new Vim process.
1736     int i, count = [cachedVimControllers count];
1737     if (0 == count) return nil;
1739     // Locate the first Vim controller with up-to-date rc-files sourced.
1740     NSDate *rcDate = [self rcFilesModificationDate];
1741     for (i = 0; i < count; ++i) {
1742         MMVimController *vc = [cachedVimControllers objectAtIndex:i];
1743         NSDate *date = [vc creationDate];
1744         if ([date compare:rcDate] != NSOrderedAscending)
1745             break;
1746     }
1748     if (i > 0) {
1749         // Clear out cache entries whose vimrc/gvimrc files were sourced before
1750         // the latest modification date for those files.  This ensures that the
1751         // latest rc-files are always sourced for new windows.
1752         [self clearPreloadCacheWithCount:i];
1753     }
1755     if ([cachedVimControllers count] == 0) {
1756         [self scheduleVimControllerPreloadAfterDelay:2.0];
1757         return nil;
1758     }
1760     MMVimController *vc = [cachedVimControllers objectAtIndex:0];
1761     [vimControllers addObject:vc];
1762     [cachedVimControllers removeObjectAtIndex:0];
1763     [vc setIsPreloading:NO];
1765     // If the Vim process has finished loading then the window will displayed
1766     // now, otherwise it will be displayed when the OpenWindowMsgID message is
1767     // received.
1768     [[vc windowController] showWindow];
1770     // Since we've taken one controller from the cache we take the opportunity
1771     // to preload another.
1772     [self scheduleVimControllerPreloadAfterDelay:1];
1774     return vc;
1777 - (void)clearPreloadCacheWithCount:(int)count
1779     // Remove the 'count' first entries in the preload cache.  It is assumed
1780     // that objects are added/removed from the cache in a FIFO manner so that
1781     // this effectively clears the 'count' oldest entries.
1782     // If 'count' is negative, then the entire cache is cleared.
1784     if ([cachedVimControllers count] == 0 || count == 0)
1785         return;
1787     if (count < 0)
1788         count = [cachedVimControllers count];
1790     // Make sure the preloaded Vim processes get killed or they'll just hang
1791     // around being useless until MacVim is terminated.
1792     NSEnumerator *e = [cachedVimControllers objectEnumerator];
1793     MMVimController *vc;
1794     int n = count;
1795     while ((vc = [e nextObject]) && n-- > 0) {
1796         [[NSNotificationCenter defaultCenter] removeObserver:vc];
1797         [vc sendMessage:TerminateNowMsgID data:nil];
1799         // Since the preloaded processes were killed "prematurely" we have to
1800         // manually tell them to cleanup (it is not enough to simply release
1801         // them since deallocation and cleanup are separated).
1802         [vc cleanup];
1803     }
1805     n = count;
1806     while (n-- > 0 && [cachedVimControllers count] > 0)
1807         [cachedVimControllers removeObjectAtIndex:0];
1809     // There is a small delay before the Vim process actually exits so wait a
1810     // little before trying to reap the child process.  If the process still
1811     // hasn't exited after this wait it won't be reaped until the next time
1812     // reapChildProcesses: is called (but this should be harmless).
1813     [self performSelector:@selector(reapChildProcesses:)
1814                withObject:nil
1815                afterDelay:0.1];
1818 - (void)rebuildPreloadCache
1820     if ([self maxPreloadCacheSize] > 0) {
1821         [self clearPreloadCacheWithCount:-1];
1822         [self cancelVimControllerPreloadRequests];
1823         [self scheduleVimControllerPreloadAfterDelay:1.0];
1824     }
1827 - (NSDate *)rcFilesModificationDate
1829     // Check modification dates for ~/.vimrc and ~/.gvimrc and return the
1830     // latest modification date.  If ~/.vimrc does not exist, check ~/_vimrc
1831     // and similarly for gvimrc.
1832     // Returns distantPath if no rc files were found.
1834     NSDate *date = [NSDate distantPast];
1835     NSFileManager *fm = [NSFileManager defaultManager];
1837     NSString *path = [@"~/.vimrc" stringByExpandingTildeInPath];
1838     NSDictionary *attr = [fm fileAttributesAtPath:path traverseLink:YES];
1839     if (!attr) {
1840         path = [@"~/_vimrc" stringByExpandingTildeInPath];
1841         attr = [fm fileAttributesAtPath:path traverseLink:YES];
1842     }
1843     NSDate *modDate = [attr objectForKey:NSFileModificationDate];
1844     if (modDate)
1845         date = modDate;
1847     path = [@"~/.gvimrc" stringByExpandingTildeInPath];
1848     attr = [fm fileAttributesAtPath:path traverseLink:YES];
1849     if (!attr) {
1850         path = [@"~/_gvimrc" stringByExpandingTildeInPath];
1851         attr = [fm fileAttributesAtPath:path traverseLink:YES];
1852     }
1853     modDate = [attr objectForKey:NSFileModificationDate];
1854     if (modDate)
1855         date = [date laterDate:modDate];
1857     return date;
1860 - (BOOL)openVimControllerWithArguments:(NSDictionary *)arguments
1862     MMVimController *vc = [self findUnusedEditor];
1863     if (vc) {
1864         // Open files in an already open window.
1865         [[[vc windowController] window] makeKeyAndOrderFront:self];
1866         [vc passArguments:arguments];
1867     } else if ((vc = [self takeVimControllerFromCache])) {
1868         // Open files in a new window using a cached vim controller.  This
1869         // requires virtually no loading time so the new window will pop up
1870         // instantaneously.
1871         [vc passArguments:arguments];
1872         [[vc backendProxy] acknowledgeConnection];
1873     } else {
1874         // Open files in a launching Vim process or start a new process.  This
1875         // may take 1-2 seconds so there will be a visible delay before the
1876         // window appears on screen.
1877         int pid = [self findLaunchingProcessWithoutArguments];
1878         if (-1 == pid) {
1879             pid = [self launchVimProcessWithArguments:nil];
1880             if (-1 == pid)
1881                 return NO;
1882         }
1884         // TODO: If the Vim process fails to start, or if it changes PID,
1885         // then the memory allocated for these parameters will leak.
1886         // Ensure that this cannot happen or somehow detect it.
1888         if ([arguments count] > 0)
1889             [pidArguments setObject:arguments
1890                              forKey:[NSNumber numberWithInt:pid]];
1891     }
1893     return YES;
1896 - (void)activateWhenNextWindowOpens
1898     shouldActivateWhenNextWindowOpens = YES;
1901 - (void)startWatchingVimDir
1903     //NSLog(@"%s", _cmd);
1904 #if (MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_4)
1905     if (fsEventStream)
1906         return;
1907     if (NULL == FSEventStreamStart)
1908         return; // FSEvent functions are weakly linked
1910     NSString *path = [@"~/.vim" stringByExpandingTildeInPath];
1911     NSArray *pathsToWatch = [NSArray arrayWithObject:path];
1913     fsEventStream = FSEventStreamCreate(NULL, &fsEventCallback, NULL,
1914             (CFArrayRef)pathsToWatch, kFSEventStreamEventIdSinceNow,
1915             MMEventStreamLatency, kFSEventStreamCreateFlagNone);
1917     FSEventStreamScheduleWithRunLoop(fsEventStream,
1918             [[NSRunLoop currentRunLoop] getCFRunLoop],
1919             kCFRunLoopDefaultMode);
1921     FSEventStreamStart(fsEventStream);
1922     //NSLog(@"Started FS event stream");
1923 #endif
1926 - (void)stopWatchingVimDir
1928     //NSLog(@"%s", _cmd);
1929 #if (MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_4)
1930     if (NULL == FSEventStreamStop)
1931         return; // FSEvent functions are weakly linked
1933     if (fsEventStream) {
1934         FSEventStreamStop(fsEventStream);
1935         FSEventStreamInvalidate(fsEventStream);
1936         FSEventStreamRelease(fsEventStream);
1937         fsEventStream = NULL;
1938         //NSLog(@"Stopped FS event stream");
1939     }
1940 #endif
1944 - (void)handleFSEvent
1946     //NSLog(@"%s", _cmd);
1947     [self clearPreloadCacheWithCount:-1];
1949     // Several FS events may arrive in quick succession so make sure to cancel
1950     // any previous preload requests before making a new one.
1951     [self cancelVimControllerPreloadRequests];
1952     [self scheduleVimControllerPreloadAfterDelay:0.5];
1955 - (void)loadDefaultFont
1957     // It is possible to set a user default to avoid loading the default font
1958     // (this cuts down on startup time).
1959     if (![[NSUserDefaults standardUserDefaults] boolForKey:MMLoadDefaultFontKey]
1960             || fontContainerRef)
1961         return;
1963     // Load all fonts in the Resouces folder of the app bundle.
1964     NSString *fontsFolder = [[NSBundle mainBundle] resourcePath];
1965     if (fontsFolder) {
1966         NSURL *fontsURL = [NSURL fileURLWithPath:fontsFolder];
1967         if (fontsURL) {
1968             FSRef fsRef;
1969             CFURLGetFSRef((CFURLRef)fontsURL, &fsRef);
1971 #if (MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_4)
1972             // This is the font activation API for OS X 10.5.  Only compile
1973             // this code if we're building on OS X 10.5 or later.
1974             if (NULL != ATSFontActivateFromFileReference) { // Weakly linked
1975                 ATSFontActivateFromFileReference(&fsRef, kATSFontContextLocal,
1976                                                  kATSFontFormatUnspecified,
1977                                                  NULL, kATSOptionFlagsDefault,
1978                                                  &fontContainerRef);
1979             }
1980 #endif
1981 #if (MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_4)
1982             // The following font activation API was deprecated in OS X 10.5.
1983             // Don't compile this code unless we're targeting OS X 10.4.
1984             FSSpec fsSpec;
1985             if (fontContainerRef == 0 &&
1986                     FSGetCatalogInfo(&fsRef, kFSCatInfoNone, NULL, NULL,
1987                                      &fsSpec, NULL) == noErr) {
1988                 ATSFontActivateFromFileSpecification(&fsSpec,
1989                         kATSFontContextLocal, kATSFontFormatUnspecified, NULL,
1990                         kATSOptionFlagsDefault, &fontContainerRef);
1991             }
1992 #endif
1993         }
1994     }
1996     if (!fontContainerRef)
1997         NSLog(@"WARNING: Failed to activate the default font (the app bundle "
1998                 "may be incomplete)");
2001 - (int)executeInLoginShell:(NSString *)path arguments:(NSArray *)args
2003     // Start a login shell and execute the command 'path' with arguments 'args'
2004     // in the shell.  This ensures that user environment variables are set even
2005     // when MacVim was started from the Finder.
2007     int pid = -1;
2008     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
2010     // Determine which shell to use to execute the command.  The user
2011     // may decide which shell to use by setting a user default or the
2012     // $SHELL environment variable.
2013     NSString *shell = [ud stringForKey:MMLoginShellCommandKey];
2014     if (!shell || [shell length] == 0)
2015         shell = [[[NSProcessInfo processInfo] environment]
2016             objectForKey:@"SHELL"];
2017     if (!shell)
2018         shell = @"/bin/bash";
2020     //NSLog(@"shell = %@", shell);
2022     // Bash needs the '-l' flag to launch a login shell.  The user may add
2023     // flags by setting a user default.
2024     NSString *shellArgument = [ud stringForKey:MMLoginShellArgumentKey];
2025     if (!shellArgument || [shellArgument length] == 0) {
2026         if ([[shell lastPathComponent] isEqual:@"bash"])
2027             shellArgument = @"-l";
2028         else
2029             shellArgument = nil;
2030     }
2032     //NSLog(@"shellArgument = %@", shellArgument);
2034     // Build input string to pipe to the login shell.
2035     NSMutableString *input = [NSMutableString stringWithFormat:
2036             @"exec \"%@\"", path];
2037     if (args) {
2038         // Append all arguments, making sure they are properly quoted, even
2039         // when they contain single quotes.
2040         NSEnumerator *e = [args objectEnumerator];
2041         id obj;
2043         while ((obj = [e nextObject])) {
2044             NSMutableString *arg = [NSMutableString stringWithString:obj];
2045             [arg replaceOccurrencesOfString:@"'" withString:@"'\"'\"'"
2046                                     options:NSLiteralSearch
2047                                       range:NSMakeRange(0, [arg length])];
2048             [input appendFormat:@" '%@'", arg];
2049         }
2050     }
2052     // Build the argument vector used to start the login shell.
2053     NSString *shellArg0 = [NSString stringWithFormat:@"-%@",
2054              [shell lastPathComponent]];
2055     char *shellArgv[3] = { (char *)[shellArg0 UTF8String], NULL, NULL };
2056     if (shellArgument)
2057         shellArgv[1] = (char *)[shellArgument UTF8String];
2059     // Get the C string representation of the shell path before the fork since
2060     // we must not call Foundation functions after a fork.
2061     const char *shellPath = [shell fileSystemRepresentation];
2063     // Fork and execute the process.
2064     int ds[2];
2065     if (pipe(ds)) return -1;
2067     pid = fork();
2068     if (pid == -1) {
2069         return -1;
2070     } else if (pid == 0) {
2071         // Child process
2073         if (close(ds[1]) == -1) exit(255);
2074         if (dup2(ds[0], 0) == -1) exit(255);
2076         // Without the following call warning messages like this appear on the
2077         // console:
2078         //     com.apple.launchd[69] : Stray process with PGID equal to this
2079         //                             dead job: PID 1589 PPID 1 Vim
2080         setsid();
2082         execv(shellPath, shellArgv);
2084         // Never reached unless execv fails
2085         exit(255);
2086     } else {
2087         // Parent process
2088         if (close(ds[0]) == -1) return -1;
2090         // Send input to execute to the child process
2091         [input appendString:@"\n"];
2092         int bytes = [input lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
2094         if (write(ds[1], [input UTF8String], bytes) != bytes) return -1;
2095         if (close(ds[1]) == -1) return -1;
2097         ++numChildProcesses;
2098         //NSLog(@"new process pid=%d (count=%d)", pid, numChildProcesses);
2099     }
2101     return pid;
2104 - (void)reapChildProcesses:(id)sender
2106     // NOTE: numChildProcesses (currently) only counts the number of Vim
2107     // processes that have been started with executeInLoginShell::.  If other
2108     // processes are spawned this code may need to be adjusted (or
2109     // numChildProcesses needs to be incremented when such a process is
2110     // started).
2111     while (numChildProcesses > 0) {
2112         int status = 0;
2113         int pid = waitpid(-1, &status, WNOHANG);
2114         if (pid <= 0)
2115             break;
2117         //NSLog(@"WAIT for pid=%d complete", pid);
2118         --numChildProcesses;
2119     }
2122 - (void)processInputQueues:(id)sender
2124     // NOTE: Because we use distributed objects it is quite possible for this
2125     // function to be re-entered.  This can cause all sorts of unexpected
2126     // problems so we guard against it here so that the rest of the code does
2127     // not need to worry about it.
2129     // The processing flag is > 0 if this function is already on the call
2130     // stack; < 0 if this function was also re-entered.
2131     if (processingFlag != 0) {
2132         NSLog(@"[%s] BUSY!", _cmd);
2133         processingFlag = -1;
2134         return;
2135     }
2137     // NOTE: Be _very_ careful that no exceptions can be raised between here
2138     // and the point at which 'processingFlag' is reset.  Otherwise the above
2139     // test could end up always failing and no input queues would ever be
2140     // processed!
2141     processingFlag = 1;
2143     // NOTE: New input may arrive while we're busy processing; we deal with
2144     // this by putting the current queue aside and creating a new input queue
2145     // for future input.
2146     NSDictionary *queues = inputQueues;
2147     inputQueues = [NSMutableDictionary new];
2149     // Pass each input queue on to the vim controller with matching
2150     // identifier (and note that it could be cached).
2151     NSEnumerator *e = [queues keyEnumerator];
2152     NSNumber *key;
2153     while ((key = [e nextObject])) {
2154         unsigned ukey = [key unsignedIntValue];
2155         int i = 0, count = [vimControllers count];
2156         for (i = 0; i < count; ++i) {
2157             MMVimController *vc = [vimControllers objectAtIndex:i];
2158             if (ukey == [vc identifier]) {
2159                 [vc processInputQueue:[queues objectForKey:key]]; // !exceptions
2160                 break;
2161             }
2162         }
2164         if (i < count) continue;
2166         count = [cachedVimControllers count];
2167         for (i = 0; i < count; ++i) {
2168             MMVimController *vc = [cachedVimControllers objectAtIndex:i];
2169             if (ukey == [vc identifier]) {
2170                 [vc processInputQueue:[queues objectForKey:key]]; // !exceptions
2171                 break;
2172             }
2173         }
2175         if (i == count)
2176             NSLog(@"[%s] WARNING: No Vim controller for identifier=%d",
2177                     _cmd, ukey);
2178     }
2180     [queues release];
2182     // If new input arrived while we were processing it would have been
2183     // blocked so we have to schedule it to be processed again.
2184     if (processingFlag < 0)
2185         [self performSelector:@selector(processInputQueues:)
2186                    withObject:nil
2187                    afterDelay:0
2188                       inModes:[NSArray arrayWithObjects:NSDefaultRunLoopMode,
2189                                             NSEventTrackingRunLoopMode, nil]];
2191     processingFlag = 0;
2194 - (void)addVimController:(MMVimController *)vc
2196     int pid = [vc pid];
2197     NSNumber *pidKey = [NSNumber numberWithInt:pid];
2199     if (preloadPid == pid) {
2200         // This controller was preloaded, so add it to the cache and
2201         // schedule another vim process to be preloaded.
2202         preloadPid = -1;
2203         [vc setIsPreloading:YES];
2204         [cachedVimControllers addObject:vc];
2205         [self scheduleVimControllerPreloadAfterDelay:1];
2206     } else {
2207         [vimControllers addObject:vc];
2209         id args = [pidArguments objectForKey:pidKey];
2210         if (args && [NSNull null] != args)
2211             [vc passArguments:args];
2213         // HACK!  MacVim does not get activated if it is launched from the
2214         // terminal, so we forcibly activate here unless it is an untitled
2215         // window opening.  Untitled windows are treated differently, else
2216         // MacVim would steal the focus if another app was activated while the
2217         // untitled window was loading.
2218         if (!args || args != [NSNull null])
2219             [self activateWhenNextWindowOpens];
2221         if (args)
2222             [pidArguments removeObjectForKey:pidKey];
2223     }
2226 @end // MMAppController (Private)