Add user default to toggle the "add tab" button
[MacVim.git] / src / MacVim / MMAppController.m
blob93fe8a4842b6ab71919d98c58e3bbf93cf008ad4
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 // When terminating, notify Vim processes then sleep for these many
65 // microseconds.
66 static useconds_t MMTerminationSleepPeriod = 10000;
68 #if (MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_4)
69 // Latency (in s) between FS event occuring and being reported to MacVim.
70 // Should be small so that MacVim is notified of changes to the ~/.vim
71 // directory more or less immediately.
72 static CFTimeInterval MMEventStreamLatency = 0.1;
73 #endif
76 #pragma options align=mac68k
77 typedef struct
79     short unused1;      // 0 (not used)
80     short lineNum;      // line to select (< 0 to specify range)
81     long  startRange;   // start of selection range (if line < 0)
82     long  endRange;     // end of selection range (if line < 0)
83     long  unused2;      // 0 (not used)
84     long  theDate;      // modification date/time
85 } MMSelectionRange;
86 #pragma options align=reset
89 static int executeInLoginShell(NSString *path, NSArray *args);
92 @interface MMAppController (MMServices)
93 - (void)openSelection:(NSPasteboard *)pboard userData:(NSString *)userData
94                 error:(NSString **)error;
95 - (void)openFile:(NSPasteboard *)pboard userData:(NSString *)userData
96            error:(NSString **)error;
97 - (void)newFileHere:(NSPasteboard *)pboard userData:(NSString *)userData
98               error:(NSString **)error;
99 @end
102 @interface MMAppController (Private)
103 - (MMVimController *)topmostVimController;
104 - (int)launchVimProcessWithArguments:(NSArray *)args;
105 - (NSArray *)filterFilesAndNotify:(NSArray *)files;
106 - (NSArray *)filterOpenFiles:(NSArray *)filenames
107                openFilesDict:(NSDictionary **)openFiles;
108 #if MM_HANDLE_XCODE_MOD_EVENT
109 - (void)handleXcodeModEvent:(NSAppleEventDescriptor *)event
110                  replyEvent:(NSAppleEventDescriptor *)reply;
111 #endif
112 - (void)handleGetURLEvent:(NSAppleEventDescriptor *)event
113                replyEvent:(NSAppleEventDescriptor *)reply;
114 - (int)findLaunchingProcessWithoutArguments;
115 - (MMVimController *)findUnusedEditor;
116 - (NSMutableDictionary *)extractArgumentsFromOdocEvent:
117     (NSAppleEventDescriptor *)desc;
118 - (void)scheduleVimControllerPreloadAfterDelay:(NSTimeInterval)delay;
119 - (void)cancelVimControllerPreloadRequests;
120 - (void)preloadVimController:(id)sender;
121 - (int)maxPreloadCacheSize;
122 - (MMVimController *)takeVimControllerFromCache;
123 - (void)clearPreloadCacheWithCount:(int)count;
124 - (void)rebuildPreloadCache;
125 - (NSDate *)rcFilesModificationDate;
126 - (BOOL)openVimControllerWithArguments:(NSDictionary *)arguments;
127 - (void)activateWhenNextWindowOpens;
128 - (void)startWatchingVimDir;
129 - (void)stopWatchingVimDir;
130 - (void)handleFSEvent;
132 #ifdef MM_ENABLE_PLUGINS
133 - (void)removePlugInMenu;
134 - (void)addPlugInMenuToMenu:(NSMenu *)mainMenu;
135 #endif
136 @end
140 #if (MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_4)
141     static void
142 fsEventCallback(ConstFSEventStreamRef streamRef,
143                 void *clientCallBackInfo,
144                 size_t numEvents,
145                 void *eventPaths,
146                 const FSEventStreamEventFlags eventFlags[],
147                 const FSEventStreamEventId eventIds[])
149     [[MMAppController sharedInstance] handleFSEvent];
151 #endif
153 @implementation MMAppController
155 + (void)initialize
157     // Avoid zombies (we fork Vim processes which we don't want to wait for).
158     signal(SIGCHLD, SIG_IGN);
160     NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:
161         [NSNumber numberWithBool:NO],   MMNoWindowKey,
162         [NSNumber numberWithInt:64],    MMTabMinWidthKey,
163         [NSNumber numberWithInt:6*64],  MMTabMaxWidthKey,
164         [NSNumber numberWithInt:132],   MMTabOptimumWidthKey,
165         [NSNumber numberWithBool:YES],  MMShowAddTabButtonKey,
166         [NSNumber numberWithInt:2],     MMTextInsetLeftKey,
167         [NSNumber numberWithInt:1],     MMTextInsetRightKey,
168         [NSNumber numberWithInt:1],     MMTextInsetTopKey,
169         [NSNumber numberWithInt:1],     MMTextInsetBottomKey,
170         @"MMTypesetter",                MMTypesetterKey,
171         [NSNumber numberWithFloat:1],   MMCellWidthMultiplierKey,
172         [NSNumber numberWithFloat:-1],  MMBaselineOffsetKey,
173         [NSNumber numberWithBool:YES],  MMTranslateCtrlClickKey,
174         [NSNumber numberWithInt:0],     MMOpenInCurrentWindowKey,
175         [NSNumber numberWithBool:NO],   MMNoFontSubstitutionKey,
176         [NSNumber numberWithBool:YES],  MMLoginShellKey,
177         [NSNumber numberWithBool:NO],   MMAtsuiRendererKey,
178         [NSNumber numberWithInt:MMUntitledWindowAlways],
179                                         MMUntitledWindowKey,
180         [NSNumber numberWithBool:NO],   MMTexturedWindowKey,
181         [NSNumber numberWithBool:NO],   MMZoomBothKey,
182         @"",                            MMLoginShellCommandKey,
183         @"",                            MMLoginShellArgumentKey,
184         [NSNumber numberWithBool:YES],  MMDialogsTrackPwdKey,
185 #ifdef MM_ENABLE_PLUGINS
186         [NSNumber numberWithBool:YES],  MMShowLeftPlugInContainerKey,
187 #endif
188         [NSNumber numberWithInt:3],     MMOpenLayoutKey,
189         [NSNumber numberWithBool:NO],   MMVerticalSplitKey,
190         [NSNumber numberWithInt:0],     MMPreloadCacheSizeKey,
191         [NSNumber numberWithInt:0],     MMLastWindowClosedBehaviorKey,
192         nil];
194     [[NSUserDefaults standardUserDefaults] registerDefaults:dict];
196     NSArray *types = [NSArray arrayWithObject:NSStringPboardType];
197     [NSApp registerServicesMenuSendTypes:types returnTypes:types];
199     // NOTE: Set the current directory to user's home directory, otherwise it
200     // will default to the root directory.  (This matters since new Vim
201     // processes inherit MacVim's environment variables.)
202     [[NSFileManager defaultManager] changeCurrentDirectoryPath:
203             NSHomeDirectory()];
206 - (id)init
208     if (!(self = [super init])) return nil;
210     fontContainerRef = loadFonts();
212     vimControllers = [NSMutableArray new];
213     cachedVimControllers = [NSMutableArray new];
214     preloadPid = -1;
215     pidArguments = [NSMutableDictionary new];
217 #ifdef MM_ENABLE_PLUGINS
218     NSString *plugInTitle = NSLocalizedString(@"Plug-In",
219                                               @"Plug-In menu title");
220     plugInMenuItem = [[NSMenuItem alloc] initWithTitle:plugInTitle
221                                                 action:NULL
222                                          keyEquivalent:@""];
223     NSMenu *submenu = [[NSMenu alloc] initWithTitle:plugInTitle];
224     [plugInMenuItem setSubmenu:submenu];
225     [submenu release];
226 #endif
228     // NOTE: Do not use the default connection since the Logitech Control
229     // Center (LCC) input manager steals and this would cause MacVim to
230     // never open any windows.  (This is a bug in LCC but since they are
231     // unlikely to fix it, we graciously give them the default connection.)
232     connection = [[NSConnection alloc] initWithReceivePort:[NSPort port]
233                                                   sendPort:nil];
234     [connection setRootObject:self];
235     [connection setRequestTimeout:MMRequestTimeout];
236     [connection setReplyTimeout:MMReplyTimeout];
238     // NOTE!  If the name of the connection changes here it must also be
239     // updated in MMBackend.m.
240     NSString *name = [NSString stringWithFormat:@"%@-connection",
241              [[NSBundle mainBundle] bundlePath]];
242     //NSLog(@"Registering connection with name '%@'", name);
243     if (![connection registerName:name]) {
244         NSLog(@"FATAL ERROR: Failed to register connection with name '%@'",
245                 name);
246         [connection release];  connection = nil;
247     }
249     return self;
252 - (void)dealloc
254     //NSLog(@"MMAppController dealloc");
256     [connection release];  connection = nil;
257     [pidArguments release];  pidArguments = nil;
258     [vimControllers release];  vimControllers = nil;
259     [cachedVimControllers release];  cachedVimControllers = nil;
260     [openSelectionString release];  openSelectionString = nil;
261     [recentFilesMenuItem release];  recentFilesMenuItem = nil;
262     [defaultMainMenu release];  defaultMainMenu = nil;
263 #ifdef MM_ENABLE_PLUGINS
264     [plugInMenuItem release];  plugInMenuItem = nil;
265 #endif
266     [appMenuItemTemplate release];  appMenuItemTemplate = nil;
268     [super dealloc];
271 - (void)applicationWillFinishLaunching:(NSNotification *)notification
273     // Remember the default menu so that it can be restored if the user closes
274     // all editor windows.
275     defaultMainMenu = [[NSApp mainMenu] retain];
277     // Store a copy of the default app menu so we can use this as a template
278     // for all other menus.  We make a copy here because the "Services" menu
279     // will not yet have been populated at this time.  If we don't we get
280     // problems trying to set key equivalents later on because they might clash
281     // with items on the "Services" menu.
282     appMenuItemTemplate = [defaultMainMenu itemAtIndex:0];
283     appMenuItemTemplate = [appMenuItemTemplate copy];
285     // Set up the "Open Recent" menu. See
286     //   http://lapcatsoftware.com/blog/2007/07/10/
287     //     working-without-a-nib-part-5-open-recent-menu/
288     // and
289     //   http://www.cocoabuilder.com/archive/message/cocoa/2007/8/15/187793
290     // for more information.
291     //
292     // The menu itself is created in MainMenu.nib but we still seem to have to
293     // hack around a bit to get it to work.  (This has to be done in
294     // applicationWillFinishLaunching at the latest, otherwise it doesn't
295     // work.)
296     NSMenu *fileMenu = [defaultMainMenu findFileMenu];
297     if (fileMenu) {
298         int idx = [fileMenu indexOfItemWithAction:@selector(fileOpen:)];
299         if (idx >= 0 && idx+1 < [fileMenu numberOfItems])
301         recentFilesMenuItem = [fileMenu itemWithTitle:@"Open Recent"];
302         [[recentFilesMenuItem submenu] performSelector:@selector(_setMenuName:)
303                                         withObject:@"NSRecentDocumentsMenu"];
305         // Note: The "Recent Files" menu must be moved around since there is no
306         // -[NSApp setRecentFilesMenu:] method.  We keep a reference to it to
307         // facilitate this move (see setMainMenu: below).
308         [recentFilesMenuItem retain];
309     }
311 #if MM_HANDLE_XCODE_MOD_EVENT
312     [[NSAppleEventManager sharedAppleEventManager]
313             setEventHandler:self
314                 andSelector:@selector(handleXcodeModEvent:replyEvent:)
315               forEventClass:'KAHL'
316                  andEventID:'MOD '];
317 #endif
319     // Register 'mvim://' URL handler
320     [[NSAppleEventManager sharedAppleEventManager]
321             setEventHandler:self
322                 andSelector:@selector(handleGetURLEvent:replyEvent:)
323               forEventClass:kInternetEventClass
324                  andEventID:kAEGetURL];
327 - (void)applicationDidFinishLaunching:(NSNotification *)notification
329     [NSApp setServicesProvider:self];
330 #ifdef MM_ENABLE_PLUGINS
331     [[MMPlugInManager sharedManager] loadAllPlugIns];
332 #endif
334     if ([self maxPreloadCacheSize] > 0) {
335         [self scheduleVimControllerPreloadAfterDelay:2];
336         [self startWatchingVimDir];
337     }
340 - (BOOL)applicationShouldOpenUntitledFile:(NSApplication *)sender
342     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
343     NSAppleEventManager *aem = [NSAppleEventManager sharedAppleEventManager];
344     NSAppleEventDescriptor *desc = [aem currentAppleEvent];
346     // The user default MMUntitledWindow can be set to control whether an
347     // untitled window should open on 'Open' and 'Reopen' events.
348     int untitledWindowFlag = [ud integerForKey:MMUntitledWindowKey];
349     if ([desc eventID] == kAEOpenApplication
350             && (untitledWindowFlag & MMUntitledWindowOnOpen) == 0)
351         return NO;
352     else if ([desc eventID] == kAEReopenApplication
353             && (untitledWindowFlag & MMUntitledWindowOnReopen) == 0)
354         return NO;
356     // When a process is started from the command line, the 'Open' event will
357     // contain a parameter to surpress the opening of an untitled window.
358     desc = [desc paramDescriptorForKeyword:keyAEPropData];
359     desc = [desc paramDescriptorForKeyword:keyMMUntitledWindow];
360     if (desc && ![desc booleanValue])
361         return NO;
363     // Never open an untitled window if there is at least one open window or if
364     // there are processes that are currently launching.
365     if ([vimControllers count] > 0 || [pidArguments count] > 0)
366         return NO;
368     // NOTE!  This way it possible to start the app with the command-line
369     // argument '-nowindow yes' and no window will be opened by default.
370     return ![ud boolForKey:MMNoWindowKey];
373 - (BOOL)applicationOpenUntitledFile:(NSApplication *)sender
375     [self newWindow:self];
376     return YES;
379 - (void)application:(NSApplication *)sender openFiles:(NSArray *)filenames
381     // Extract ODB/Xcode/Spotlight parameters from the current Apple event,
382     // sort the filenames, and then let openFiles:withArguments: do the heavy
383     // lifting.
385     if (!(filenames && [filenames count] > 0))
386         return;
388     // Sort filenames since the Finder doesn't take care in preserving the
389     // order in which files are selected anyway (and "sorted" is more
390     // predictable than "random").
391     if ([filenames count] > 1)
392         filenames = [filenames sortedArrayUsingSelector:
393                 @selector(localizedCompare:)];
395     // Extract ODB/Xcode/Spotlight parameters from the current Apple event
396     NSMutableDictionary *arguments = [self extractArgumentsFromOdocEvent:
397             [[NSAppleEventManager sharedAppleEventManager] currentAppleEvent]];
399     if ([self openFiles:filenames withArguments:arguments]) {
400         [NSApp replyToOpenOrPrint:NSApplicationDelegateReplySuccess];
401     } else {
402         // TODO: Notify user of failure?
403         [NSApp replyToOpenOrPrint:NSApplicationDelegateReplyFailure];
404     }
407 - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender
409     return (MMTerminateWhenLastWindowClosed ==
410             [[NSUserDefaults standardUserDefaults]
411                 integerForKey:MMLastWindowClosedBehaviorKey]);
414 - (NSApplicationTerminateReply)applicationShouldTerminate:
415     (NSApplication *)sender
417     // TODO: Follow Apple's guidelines for 'Graceful Application Termination'
418     // (in particular, allow user to review changes and save).
419     int reply = NSTerminateNow;
420     BOOL modifiedBuffers = NO;
422     // Go through windows, checking for modified buffers.  (Each Vim process
423     // tells MacVim when any buffer has been modified and MacVim sets the
424     // 'documentEdited' flag of the window correspondingly.)
425     NSEnumerator *e = [[NSApp windows] objectEnumerator];
426     id window;
427     while ((window = [e nextObject])) {
428         if ([window isDocumentEdited]) {
429             modifiedBuffers = YES;
430             break;
431         }
432     }
434     if (modifiedBuffers) {
435         NSAlert *alert = [[NSAlert alloc] init];
436         [alert setAlertStyle:NSWarningAlertStyle];
437         [alert addButtonWithTitle:NSLocalizedString(@"Quit",
438                 @"Dialog button")];
439         [alert addButtonWithTitle:NSLocalizedString(@"Cancel",
440                 @"Dialog button")];
441         [alert setMessageText:NSLocalizedString(@"Quit without saving?",
442                 @"Quit dialog with changed buffers, title")];
443         [alert setInformativeText:NSLocalizedString(
444                 @"There are modified buffers, "
445                 "if you quit now all changes will be lost.  Quit anyway?",
446                 @"Quit dialog with changed buffers, text")];
448         if ([alert runModal] != NSAlertFirstButtonReturn)
449             reply = NSTerminateCancel;
451         [alert release];
452     } else {
453         // No unmodified buffers, but give a warning if there are multiple
454         // windows and/or tabs open.
455         int numWindows = [vimControllers count];
456         int numTabs = 0;
458         // Count the number of open tabs
459         e = [vimControllers objectEnumerator];
460         id vc;
461         while ((vc = [e nextObject])) {
462             NSString *eval = [vc evaluateVimExpression:@"tabpagenr('$')"];
463             if (eval) {
464                 int count = [eval intValue];
465                 if (count > 0 && count < INT_MAX)
466                     numTabs += count;
467             }
468         }
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             [vc sendMessage:TerminateNowMsgID data:nil];
522         e = [cachedVimControllers objectEnumerator];
523         while ((vc = [e nextObject]))
524             [vc sendMessage:TerminateNowMsgID data:nil];
526         // Give Vim processes a chance to terminate before MacVim.  If they
527         // haven't terminated by the time applicationWillTerminate: is sent,
528         // they may be forced to quit (see below).
529         usleep(MMTerminationSleepPeriod);
530     }
532     return reply;
535 - (void)applicationWillTerminate:(NSNotification *)notification
537     [self stopWatchingVimDir];
539 #ifdef MM_ENABLE_PLUGINS
540     [[MMPlugInManager sharedManager] unloadAllPlugIns];
541 #endif
543 #if MM_HANDLE_XCODE_MOD_EVENT
544     [[NSAppleEventManager sharedAppleEventManager]
545             removeEventHandlerForEventClass:'KAHL'
546                                  andEventID:'MOD '];
547 #endif
549     // This will invalidate all connections (since they were spawned from this
550     // connection).
551     [connection invalidate];
553     // Send a SIGINT to all running Vim processes, so that they are sure to
554     // receive the connectionDidDie: notification (a process has to be checking
555     // the run-loop for this to happen).
556     unsigned i, count = [vimControllers count];
557     for (i = 0; i < count; ++i) {
558         MMVimController *controller = [vimControllers objectAtIndex:i];
559         int pid = [controller pid];
560         if (-1 != pid)
561             kill(pid, SIGINT);
562     }
564     if (fontContainerRef) {
565         ATSFontDeactivate(fontContainerRef, NULL, kATSOptionFlagsDefault);
566         fontContainerRef = 0;
567     }
569     [NSApp setDelegate:nil];
572 + (MMAppController *)sharedInstance
574     // Note: The app controller is a singleton which is instantiated in
575     // MainMenu.nib where it is also connected as the delegate of NSApp.
576     id delegate = [NSApp delegate];
577     return [delegate isKindOfClass:self] ? (MMAppController*)delegate : nil;
580 - (NSMenu *)defaultMainMenu
582     return defaultMainMenu;
585 - (NSMenuItem *)appMenuItemTemplate
587     return appMenuItemTemplate;
590 - (void)removeVimController:(id)controller
592     int idx = [vimControllers indexOfObject:controller];
593     if (NSNotFound == idx)
594         return;
596     [controller cleanup];
598     [vimControllers removeObjectAtIndex:idx];
600     if (![vimControllers count]) {
601         // The last editor window just closed so restore the main menu back to
602         // its default state (which is defined in MainMenu.nib).
603         [self setMainMenu:defaultMainMenu];
605         BOOL hide = (MMHideWhenLastWindowClosed ==
606                     [[NSUserDefaults standardUserDefaults]
607                         integerForKey:MMLastWindowClosedBehaviorKey]);
608         if (hide)
609             [NSApp hide:self];
610     }
613 - (void)windowControllerWillOpen:(MMWindowController *)windowController
615     NSPoint topLeft = NSZeroPoint;
616     NSWindow *topWin = [[[self topmostVimController] windowController] window];
617     NSWindow *win = [windowController window];
619     if (!win) return;
621     // If there is a window belonging to a Vim process, cascade from it,
622     // otherwise use the autosaved window position (if any).
623     if (topWin) {
624         NSRect frame = [topWin frame];
625         topLeft = NSMakePoint(frame.origin.x, NSMaxY(frame));
626     } else {
627         NSString *topLeftString = [[NSUserDefaults standardUserDefaults]
628             stringForKey:MMTopLeftPointKey];
629         if (topLeftString)
630             topLeft = NSPointFromString(topLeftString);
631     }
633     if (!NSEqualPoints(topLeft, NSZeroPoint)) {
634         NSPoint oldTopLeft = topLeft;
635         if (topWin)
636             topLeft = [win cascadeTopLeftFromPoint:topLeft];
638         [win setFrameTopLeftPoint:topLeft];
640         NSPoint screenOrigin = [[win screen] frame].origin;
641         if ([win frame].origin.y < screenOrigin.y) {
642             // Try to avoid shifting the new window downwards if it means that
643             // the bottom of the window will be off the screen.  E.g. if the
644             // user has set windows to open maximized in the vertical direction
645             // then the new window will cascade horizontally only.
646             topLeft.y = oldTopLeft.y;
647             [win setFrameTopLeftPoint:topLeft];
648         }
650         if ([win frame].origin.y < screenOrigin.y) {
651             // Move the window to the top of the screen if the bottom of the
652             // window is still obscured.
653             topLeft.y = NSMaxY([[win screen] frame]);
654             [win setFrameTopLeftPoint:topLeft];
655         }
656     }
658     if (1 == [vimControllers count]) {
659         // The first window autosaves its position.  (The autosaving
660         // features of Cocoa are not used because we need more control over
661         // what is autosaved and when it is restored.)
662         [windowController setWindowAutosaveKey:MMTopLeftPointKey];
663     }
665     if (openSelectionString) {
666         // TODO: Pass this as a parameter instead!  Get rid of
667         // 'openSelectionString' etc.
668         //
669         // There is some text to paste into this window as a result of the
670         // services menu "Open selection ..." being used.
671         [[windowController vimController] dropString:openSelectionString];
672         [openSelectionString release];
673         openSelectionString = nil;
674     }
676     if (shouldActivateWhenNextWindowOpens) {
677         [NSApp activateIgnoringOtherApps:YES];
678         shouldActivateWhenNextWindowOpens = NO;
679     }
682 - (void)setMainMenu:(NSMenu *)mainMenu
684     if ([NSApp mainMenu] == mainMenu) return;
686     // If the new menu has a "Recent Files" dummy item, then swap the real item
687     // for the dummy.  We are forced to do this since Cocoa initializes the
688     // "Recent Files" menu and there is no way to simply point Cocoa to a new
689     // item each time the menus are swapped.
690     NSMenu *fileMenu = [mainMenu findFileMenu];
691     if (recentFilesMenuItem && fileMenu) {
692         int dummyIdx =
693                 [fileMenu indexOfItemWithAction:@selector(recentFilesDummy:)];
694         if (dummyIdx >= 0) {
695             NSMenuItem *dummyItem = [[fileMenu itemAtIndex:dummyIdx] retain];
696             [fileMenu removeItemAtIndex:dummyIdx];
698             NSMenu *recentFilesParentMenu = [recentFilesMenuItem menu];
699             int idx = [recentFilesParentMenu indexOfItem:recentFilesMenuItem];
700             if (idx >= 0) {
701                 [[recentFilesMenuItem retain] autorelease];
702                 [recentFilesParentMenu removeItemAtIndex:idx];
703                 [recentFilesParentMenu insertItem:dummyItem atIndex:idx];
704             }
706             [fileMenu insertItem:recentFilesMenuItem atIndex:dummyIdx];
707             [dummyItem release];
708         }
709     }
711     // Now set the new menu.  Notice that we keep one menu for each editor
712     // window since each editor can have its own set of menus.  When swapping
713     // menus we have to tell Cocoa where the new "MacVim", "Windows", and
714     // "Services" menu are.
715     [NSApp setMainMenu:mainMenu];
717     // Setting the "MacVim" (or "Application") menu ensures that it is typeset
718     // in boldface.  (The setAppleMenu: method used to be public but is now
719     // private so this will have to be considered a bit of a hack!)
720     NSMenu *appMenu = [mainMenu findApplicationMenu];
721     [NSApp performSelector:@selector(setAppleMenu:) withObject:appMenu];
723     NSMenu *servicesMenu = [mainMenu findServicesMenu];
724     [NSApp setServicesMenu:servicesMenu];
726     NSMenu *windowsMenu = [mainMenu findWindowsMenu];
727     if (windowsMenu) {
728         // Cocoa isn't clever enough to get rid of items it has added to the
729         // "Windows" menu so we have to do it ourselves otherwise there will be
730         // multiple menu items for each window in the "Windows" menu.
731         //   This code assumes that the only items Cocoa add are ones which
732         // send off the action makeKeyAndOrderFront:.  (Cocoa will not add
733         // another separator item if the last item on the "Windows" menu
734         // already is a separator, so we needen't worry about separators.)
735         int i, count = [windowsMenu numberOfItems];
736         for (i = count-1; i >= 0; --i) {
737             NSMenuItem *item = [windowsMenu itemAtIndex:i];
738             if ([item action] == @selector(makeKeyAndOrderFront:))
739                 [windowsMenu removeItem:item];
740         }
741     }
742     [NSApp setWindowsMenu:windowsMenu];
744 #ifdef MM_ENABLE_PLUGINS
745     // Move plugin menu from old to new main menu.
746     [self removePlugInMenu];
747     [self addPlugInMenuToMenu:mainMenu];
748 #endif
751 - (NSArray *)filterOpenFiles:(NSArray *)filenames
753     return [self filterOpenFiles:filenames openFilesDict:nil];
756 - (BOOL)openFiles:(NSArray *)filenames withArguments:(NSDictionary *)args
758     // Opening files works like this:
759     //  a) filter out any already open files
760     //  b) open any remaining files
761     //
762     // A file is opened in an untitled window if there is one (it may be
763     // currently launching, or it may already be visible), otherwise a new
764     // window is opened.
765     //
766     // Each launching Vim process has a dictionary of arguments that are passed
767     // to the process when in checks in (via connectBackend:pid:).  The
768     // arguments for each launching process can be looked up by its PID (in the
769     // pidArguments dictionary).
771     NSMutableDictionary *arguments = (args ? [[args mutableCopy] autorelease]
772                                            : [NSMutableDictionary dictionary]);
774     //
775     // a) Filter out any already open files
776     //
777     NSString *firstFile = [filenames objectAtIndex:0];
778     MMVimController *firstController = nil;
779     NSDictionary *openFilesDict = nil;
780     filenames = [self filterOpenFiles:filenames openFilesDict:&openFilesDict];
782     // Pass arguments to vim controllers that had files open.
783     id key;
784     NSEnumerator *e = [openFilesDict keyEnumerator];
786     // (Indicate that we do not wish to open any files at the moment.)
787     [arguments setObject:[NSNumber numberWithBool:YES] forKey:@"dontOpen"];
789     while ((key = [e nextObject])) {
790         NSArray *files = [openFilesDict objectForKey:key];
791         [arguments setObject:files forKey:@"filenames"];
793         MMVimController *vc = [key pointerValue];
794         [vc passArguments:arguments];
796         // If this controller holds the first file, then remember it for later.
797         if ([files containsObject:firstFile])
798             firstController = vc;
799     }
801     if ([filenames count] == 0) {
802         // Raise the window containing the first file that was already open,
803         // and make sure that the tab containing that file is selected.  Only
804         // do this when there are no more files to open, otherwise sometimes
805         // the window with 'firstFile' will be raised, other times it might be
806         // the window that will open with the files in the 'filenames' array.
807         firstFile = [firstFile stringByEscapingSpecialFilenameCharacters];
808         NSString *input = [NSString stringWithFormat:@"<C-\\><C-N>"
809                 ":let oldswb=&swb|let &swb=\"useopen,usetab\"|"
810                 "tab sb %@|let &swb=oldswb|unl oldswb|"
811                 "cal foreground()|redr|f<CR>", firstFile];
813         [firstController addVimInput:input];
815         return YES;
816     }
818     // Add filenames to "Recent Files" menu, unless they are being edited
819     // remotely (using ODB).
820     if ([arguments objectForKey:@"remoteID"] == nil) {
821         [[NSDocumentController sharedDocumentController]
822                 noteNewRecentFilePaths:filenames];
823     }
825     //
826     // b) Open any remaining files
827     //
828     MMVimController *vc;
829     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
830     BOOL openInCurrentWindow = [ud boolForKey:MMOpenInCurrentWindowKey];
832     // The meaning of "layout" is defined by the WIN_* defines in main.c.
833     int layout = [ud integerForKey:MMOpenLayoutKey];
834     BOOL splitVert = [ud boolForKey:MMVerticalSplitKey];
835     if (splitVert && MMLayoutHorizontalSplit == layout)
836         layout = MMLayoutVerticalSplit;
837     if (layout < 0 || (layout > MMLayoutTabs && openInCurrentWindow))
838         layout = MMLayoutTabs;
840     [arguments setObject:[NSNumber numberWithInt:layout] forKey:@"layout"];
841     [arguments setObject:filenames forKey:@"filenames"];
842     // (Indicate that files should be opened from now on.)
843     [arguments setObject:[NSNumber numberWithBool:NO] forKey:@"dontOpen"];
845     if (openInCurrentWindow && (vc = [self topmostVimController])) {
846         // Open files in an already open window.
847         [[[vc windowController] window] makeKeyAndOrderFront:self];
848         [vc passArguments:arguments];
849         return YES;
850     }
852     BOOL openOk = YES;
853     int numFiles = [filenames count];
854     if (MMLayoutWindows == layout && numFiles > 1) {
855         // Open one file at a time in a new window, but don't open too many at
856         // once (at most cap+1 windows will open).  If the user has increased
857         // the preload cache size we'll take that as a hint that more windows
858         // should be able to open at once.
859         int cap = [self maxPreloadCacheSize] - 1;
860         if (cap < 4) cap = 4;
861         if (cap > numFiles) cap = numFiles;
863         int i;
864         for (i = 0; i < cap; ++i) {
865             NSArray *a = [NSArray arrayWithObject:[filenames objectAtIndex:i]];
866             [arguments setObject:a forKey:@"filenames"];
868             // NOTE: We have to copy the args since we'll mutate them in the
869             // next loop and the below call may retain the arguments while
870             // waiting for a process to start.
871             NSDictionary *args = [[arguments copy] autorelease];
873             openOk = [self openVimControllerWithArguments:args];
874             if (!openOk) break;
875         }
877         // Open remaining files in tabs in a new window.
878         if (openOk && numFiles > cap) {
879             NSRange range = { i, numFiles-cap };
880             NSArray *a = [filenames subarrayWithRange:range];
881             [arguments setObject:a forKey:@"filenames"];
882             [arguments setObject:[NSNumber numberWithInt:MMLayoutTabs]
883                           forKey:@"layout"];
885             openOk = [self openVimControllerWithArguments:arguments];
886         }
887     } else {
888         // Open all files at once.
889         openOk = [self openVimControllerWithArguments:arguments];
890     }
892     return openOk;
895 #ifdef MM_ENABLE_PLUGINS
896 - (void)addItemToPlugInMenu:(NSMenuItem *)item
898     NSMenu *menu = [plugInMenuItem submenu];
899     [menu addItem:item];
900     if ([menu numberOfItems] == 1)
901         [self addPlugInMenuToMenu:[NSApp mainMenu]];
904 - (void)removeItemFromPlugInMenu:(NSMenuItem *)item
906     NSMenu *menu = [plugInMenuItem submenu];
907     [menu removeItem:item];
908     if ([menu numberOfItems] == 0)
909         [self removePlugInMenu];
911 #endif
913 - (IBAction)newWindow:(id)sender
915     // A cached controller requires no loading times and results in the new
916     // window popping up instantaneously.  If the cache is empty it may take
917     // 1-2 seconds to start a new Vim process.
918     MMVimController *vc = [self takeVimControllerFromCache];
919     if (vc) {
920         [[vc backendProxy] acknowledgeConnection];
921     } else {
922         [self launchVimProcessWithArguments:nil];
923     }
926 - (IBAction)newWindowAndActivate:(id)sender
928     [self activateWhenNextWindowOpens];
929     [self newWindow:sender];
932 - (IBAction)fileOpen:(id)sender
934     NSString *dir = nil;
935     BOOL trackPwd = [[NSUserDefaults standardUserDefaults]
936             boolForKey:MMDialogsTrackPwdKey];
937     if (trackPwd) {
938         MMVimController *vc = [self keyVimController];
939         if (vc) dir = [[vc vimState] objectForKey:@"pwd"];
940     }
942     NSOpenPanel *panel = [NSOpenPanel openPanel];
943     [panel setAllowsMultipleSelection:YES];
944     [panel setAccessoryView:openPanelAccessoryView()];
946     int result = [panel runModalForDirectory:dir file:nil types:nil];
947     if (NSOKButton == result)
948         [self application:NSApp openFiles:[panel filenames]];
951 - (IBAction)selectNextWindow:(id)sender
953     unsigned i, count = [vimControllers count];
954     if (!count) return;
956     NSWindow *keyWindow = [NSApp keyWindow];
957     for (i = 0; i < count; ++i) {
958         MMVimController *vc = [vimControllers objectAtIndex:i];
959         if ([[[vc windowController] window] isEqual:keyWindow])
960             break;
961     }
963     if (i < count) {
964         if (++i >= count)
965             i = 0;
966         MMVimController *vc = [vimControllers objectAtIndex:i];
967         [[vc windowController] showWindow:self];
968     }
971 - (IBAction)selectPreviousWindow:(id)sender
973     unsigned i, count = [vimControllers count];
974     if (!count) return;
976     NSWindow *keyWindow = [NSApp keyWindow];
977     for (i = 0; i < count; ++i) {
978         MMVimController *vc = [vimControllers objectAtIndex:i];
979         if ([[[vc windowController] window] isEqual:keyWindow])
980             break;
981     }
983     if (i < count) {
984         if (i > 0) {
985             --i;
986         } else {
987             i = count - 1;
988         }
989         MMVimController *vc = [vimControllers objectAtIndex:i];
990         [[vc windowController] showWindow:self];
991     }
994 - (IBAction)orderFrontPreferencePanel:(id)sender
996     [[MMPreferenceController sharedPrefsWindowController] showWindow:self];
999 - (IBAction)openWebsite:(id)sender
1001     [[NSWorkspace sharedWorkspace] openURL:
1002             [NSURL URLWithString:MMWebsiteString]];
1005 - (IBAction)showVimHelp:(id)sender
1007     // Open a new window with the help window maximized.
1008     [self launchVimProcessWithArguments:[NSArray arrayWithObjects:
1009             @"-c", @":h gui_mac", @"-c", @":res", nil]];
1012 - (IBAction)zoomAll:(id)sender
1014     [NSApp makeWindowsPerform:@selector(performZoom:) inOrder:YES];
1017 - (IBAction)atsuiButtonClicked:(id)sender
1019     // This action is called when the user clicks the "use ATSUI renderer"
1020     // button in the advanced preferences pane.
1021     [self rebuildPreloadCache];
1024 - (IBAction)loginShellButtonClicked:(id)sender
1026     // This action is called when the user clicks the "use login shell" button
1027     // in the advanced preferences pane.
1028     [self rebuildPreloadCache];
1031 - (IBAction)quickstartButtonClicked:(id)sender
1033     if ([self maxPreloadCacheSize] > 0) {
1034         [self scheduleVimControllerPreloadAfterDelay:1.0];
1035         [self startWatchingVimDir];
1036     } else {
1037         [self cancelVimControllerPreloadRequests];
1038         [self clearPreloadCacheWithCount:-1];
1039         [self stopWatchingVimDir];
1040     }
1043 - (byref id <MMFrontendProtocol>)
1044     connectBackend:(byref in id <MMBackendProtocol>)backend
1045                pid:(int)pid
1047     //NSLog(@"Connect backend (pid=%d)", pid);
1048     NSNumber *pidKey = [NSNumber numberWithInt:pid];
1049     MMVimController *vc = nil;
1051     @try {
1052         [(NSDistantObject*)backend
1053                 setProtocolForProxy:@protocol(MMBackendProtocol)];
1055         vc = [[[MMVimController alloc] initWithBackend:backend pid:pid]
1056                 autorelease];
1058         if (preloadPid == pid) {
1059             // This backend was preloaded, so add it to the cache and schedule
1060             // another vim process to be preloaded.
1061             preloadPid = -1;
1062             [vc setIsPreloading:YES];
1063             [cachedVimControllers addObject:vc];
1064             [self scheduleVimControllerPreloadAfterDelay:1];
1066             return vc;
1067         }
1069         [vimControllers addObject:vc];
1071         id args = [pidArguments objectForKey:pidKey];
1072         if (args && [NSNull null] != args)
1073             [vc passArguments:args];
1075         // HACK!  MacVim does not get activated if it is launched from the
1076         // terminal, so we forcibly activate here unless it is an untitled
1077         // window opening.  Untitled windows are treated differently, else
1078         // MacVim would steal the focus if another app was activated while the
1079         // untitled window was loading.
1080         if (!args || args != [NSNull null])
1081             [self activateWhenNextWindowOpens];
1083         if (args)
1084             [pidArguments removeObjectForKey:pidKey];
1086         return vc;
1087     }
1089     @catch (NSException *e) {
1090         NSLog(@"Exception caught in %s: \"%@\"", _cmd, e);
1092         if (vc)
1093             [vimControllers removeObject:vc];
1095         [pidArguments removeObjectForKey:pidKey];
1096     }
1098     return nil;
1101 - (NSArray *)serverList
1103     NSMutableArray *array = [NSMutableArray array];
1105     unsigned i, count = [vimControllers count];
1106     for (i = 0; i < count; ++i) {
1107         MMVimController *controller = [vimControllers objectAtIndex:i];
1108         if ([controller serverName])
1109             [array addObject:[controller serverName]];
1110     }
1112     return array;
1115 - (MMVimController *)keyVimController
1117     NSWindow *keyWindow = [NSApp keyWindow];
1118     if (keyWindow) {
1119         unsigned i, count = [vimControllers count];
1120         for (i = 0; i < count; ++i) {
1121             MMVimController *vc = [vimControllers objectAtIndex:i];
1122             if ([[[vc windowController] window] isEqual:keyWindow])
1123                 return vc;
1124         }
1125     }
1127     return nil;
1130 @end // MMAppController
1135 @implementation MMAppController (MMServices)
1137 - (void)openSelection:(NSPasteboard *)pboard userData:(NSString *)userData
1138                 error:(NSString **)error
1140     if (![[pboard types] containsObject:NSStringPboardType]) {
1141         NSLog(@"WARNING: Pasteboard contains no object of type "
1142                 "NSStringPboardType");
1143         return;
1144     }
1146     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
1147     BOOL openInCurrentWindow = [ud boolForKey:MMOpenInCurrentWindowKey];
1148     MMVimController *vc;
1150     if (openInCurrentWindow && (vc = [self topmostVimController])) {
1151         [vc sendMessage:AddNewTabMsgID data:nil];
1152         [vc dropString:[pboard stringForType:NSStringPboardType]];
1153     } else {
1154         // Save the text, open a new window, and paste the text when the next
1155         // window opens.  (If this is called several times in a row, then all
1156         // but the last call may be ignored.)
1157         if (openSelectionString) [openSelectionString release];
1158         openSelectionString = [[pboard stringForType:NSStringPboardType] copy];
1160         [self newWindow:self];
1161     }
1164 - (void)openFile:(NSPasteboard *)pboard userData:(NSString *)userData
1165            error:(NSString **)error
1167     if (![[pboard types] containsObject:NSStringPboardType]) {
1168         NSLog(@"WARNING: Pasteboard contains no object of type "
1169                 "NSStringPboardType");
1170         return;
1171     }
1173     // TODO: Parse multiple filenames and create array with names.
1174     NSString *string = [pboard stringForType:NSStringPboardType];
1175     string = [string stringByTrimmingCharactersInSet:
1176             [NSCharacterSet whitespaceAndNewlineCharacterSet]];
1177     string = [string stringByStandardizingPath];
1179     NSArray *filenames = [self filterFilesAndNotify:
1180             [NSArray arrayWithObject:string]];
1181     if ([filenames count] == 0)
1182         return;
1184     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
1185     BOOL openInCurrentWindow = [ud boolForKey:MMOpenInCurrentWindowKey];
1186     MMVimController *vc;
1188     if (openInCurrentWindow && (vc = [self topmostVimController])) {
1189         [vc dropFiles:filenames forceOpen:YES];
1190     } else {
1191         [self openFiles:filenames withArguments:nil];
1192     }
1195 - (void)newFileHere:(NSPasteboard *)pboard userData:(NSString *)userData
1196               error:(NSString **)error
1198     if (![[pboard types] containsObject:NSStringPboardType]) {
1199         NSLog(@"WARNING: Pasteboard contains no object of type "
1200               "NSStringPboardType");
1201         return;
1202     }
1204     NSString *path = [pboard stringForType:NSStringPboardType];
1206     BOOL dirIndicator;
1207     if (![[NSFileManager defaultManager] fileExistsAtPath:path
1208                                               isDirectory:&dirIndicator]) {
1209         NSLog(@"Invalid path. Cannot open new document at: %@", path);
1210         return;
1211     }
1213     if (!dirIndicator)
1214         path = [path stringByDeletingLastPathComponent];
1216     path = [path stringByEscapingSpecialFilenameCharacters];
1218     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
1219     BOOL openInCurrentWindow = [ud boolForKey:MMOpenInCurrentWindowKey];
1220     MMVimController *vc;
1222     if (openInCurrentWindow && (vc = [self topmostVimController])) {
1223         NSString *input = [NSString stringWithFormat:@"<C-\\><C-N>"
1224                 ":tabe|cd %@<CR>", path];
1225         [vc addVimInput:input];
1226     } else {
1227         NSString *input = [NSString stringWithFormat:@":cd %@", path];
1228         [self launchVimProcessWithArguments:[NSArray arrayWithObjects:
1229                                              @"-c", input, nil]];
1230     }
1233 @end // MMAppController (MMServices)
1238 @implementation MMAppController (Private)
1240 - (MMVimController *)topmostVimController
1242     // Find the topmost visible window which has an associated vim controller.
1243     NSEnumerator *e = [[NSApp orderedWindows] objectEnumerator];
1244     id window;
1245     while ((window = [e nextObject]) && [window isVisible]) {
1246         unsigned i, count = [vimControllers count];
1247         for (i = 0; i < count; ++i) {
1248             MMVimController *vc = [vimControllers objectAtIndex:i];
1249             if ([[[vc windowController] window] isEqual:window])
1250                 return vc;
1251         }
1252     }
1254     return nil;
1257 - (int)launchVimProcessWithArguments:(NSArray *)args
1259     int pid = -1;
1260     NSString *path = [[NSBundle mainBundle] pathForAuxiliaryExecutable:@"Vim"];
1262     if (!path) {
1263         NSLog(@"ERROR: Vim executable could not be found inside app bundle!");
1264         return -1;
1265     }
1267     NSArray *taskArgs = [NSArray arrayWithObjects:@"-g", @"-f", nil];
1268     if (args)
1269         taskArgs = [taskArgs arrayByAddingObjectsFromArray:args];
1271     BOOL useLoginShell = [[NSUserDefaults standardUserDefaults]
1272             boolForKey:MMLoginShellKey];
1273     if (useLoginShell) {
1274         // Run process with a login shell, roughly:
1275         //   echo "exec Vim -g -f args" | ARGV0=-`basename $SHELL` $SHELL [-l]
1276         pid = executeInLoginShell(path, taskArgs);
1277     } else {
1278         // Run process directly:
1279         //   Vim -g -f args
1280         NSTask *task = [NSTask launchedTaskWithLaunchPath:path
1281                                                 arguments:taskArgs];
1282         pid = task ? [task processIdentifier] : -1;
1283     }
1285     if (-1 != pid) {
1286         // NOTE: If the process has no arguments, then add a null argument to
1287         // the pidArguments dictionary.  This is later used to detect that a
1288         // process without arguments is being launched.
1289         if (!args)
1290             [pidArguments setObject:[NSNull null]
1291                              forKey:[NSNumber numberWithInt:pid]];
1292     } else {
1293         NSLog(@"WARNING: %s%@ failed (useLoginShell=%d)", _cmd, args,
1294                 useLoginShell);
1295     }
1297     return pid;
1300 - (NSArray *)filterFilesAndNotify:(NSArray *)filenames
1302     // Go trough 'filenames' array and make sure each file exists.  Present
1303     // warning dialog if some file was missing.
1305     NSString *firstMissingFile = nil;
1306     NSMutableArray *files = [NSMutableArray array];
1307     unsigned i, count = [filenames count];
1309     for (i = 0; i < count; ++i) {
1310         NSString *name = [filenames objectAtIndex:i];
1311         if ([[NSFileManager defaultManager] fileExistsAtPath:name]) {
1312             [files addObject:name];
1313         } else if (!firstMissingFile) {
1314             firstMissingFile = name;
1315         }
1316     }
1318     if (firstMissingFile) {
1319         NSAlert *alert = [[NSAlert alloc] init];
1320         [alert addButtonWithTitle:NSLocalizedString(@"OK",
1321                 @"Dialog button")];
1323         NSString *text;
1324         if ([files count] >= count-1) {
1325             [alert setMessageText:NSLocalizedString(@"File not found",
1326                     @"File not found dialog, title")];
1327             text = [NSString stringWithFormat:NSLocalizedString(
1328                     @"Could not open file with name %@.",
1329                     @"File not found dialog, text"), firstMissingFile];
1330         } else {
1331             [alert setMessageText:NSLocalizedString(@"Multiple files not found",
1332                     @"File not found dialog, title")];
1333             text = [NSString stringWithFormat:NSLocalizedString(
1334                     @"Could not open file with name %@, and %d other files.",
1335                     @"File not found dialog, text"),
1336                 firstMissingFile, count-[files count]-1];
1337         }
1339         [alert setInformativeText:text];
1340         [alert setAlertStyle:NSWarningAlertStyle];
1342         [alert runModal];
1343         [alert release];
1345         [NSApp replyToOpenOrPrint:NSApplicationDelegateReplyFailure];
1346     }
1348     return files;
1351 - (NSArray *)filterOpenFiles:(NSArray *)filenames
1352                openFilesDict:(NSDictionary **)openFiles
1354     // Filter out any files in the 'filenames' array that are open and return
1355     // all files that are not already open.  On return, the 'openFiles'
1356     // parameter (if non-nil) will point to a dictionary of open files, indexed
1357     // by Vim controller.
1359     NSMutableDictionary *dict = [NSMutableDictionary dictionary];
1360     NSMutableArray *files = [filenames mutableCopy];
1362     // TODO: Escape special characters in 'files'?
1363     NSString *expr = [NSString stringWithFormat:
1364             @"map([\"%@\"],\"bufloaded(v:val)\")",
1365             [files componentsJoinedByString:@"\",\""]];
1367     unsigned i, count = [vimControllers count];
1368     for (i = 0; i < count && [files count] > 0; ++i) {
1369         MMVimController *vc = [vimControllers objectAtIndex:i];
1371         // Query Vim for which files in the 'files' array are open.
1372         NSString *eval = [vc evaluateVimExpression:expr];
1373         if (!eval) continue;
1375         NSIndexSet *idxSet = [NSIndexSet indexSetWithVimList:eval];
1376         if ([idxSet count] > 0) {
1377             [dict setObject:[files objectsAtIndexes:idxSet]
1378                      forKey:[NSValue valueWithPointer:vc]];
1380             // Remove all the files that were open in this Vim process and
1381             // create a new expression to evaluate.
1382             [files removeObjectsAtIndexes:idxSet];
1383             expr = [NSString stringWithFormat:
1384                     @"map([\"%@\"],\"bufloaded(v:val)\")",
1385                     [files componentsJoinedByString:@"\",\""]];
1386         }
1387     }
1389     if (openFiles != nil)
1390         *openFiles = dict;
1392     return files;
1395 #if MM_HANDLE_XCODE_MOD_EVENT
1396 - (void)handleXcodeModEvent:(NSAppleEventDescriptor *)event
1397                  replyEvent:(NSAppleEventDescriptor *)reply
1399 #if 0
1400     // Xcode sends this event to query MacVim which open files have been
1401     // modified.
1402     NSLog(@"reply:%@", reply);
1403     NSLog(@"event:%@", event);
1405     NSEnumerator *e = [vimControllers objectEnumerator];
1406     id vc;
1407     while ((vc = [e nextObject])) {
1408         DescType type = [reply descriptorType];
1409         unsigned len = [[type data] length];
1410         NSMutableData *data = [NSMutableData data];
1412         [data appendBytes:&type length:sizeof(DescType)];
1413         [data appendBytes:&len length:sizeof(unsigned)];
1414         [data appendBytes:[reply data] length:len];
1416         [vc sendMessage:XcodeModMsgID data:data];
1417     }
1418 #endif
1420 #endif
1422 - (void)handleGetURLEvent:(NSAppleEventDescriptor *)event
1423                replyEvent:(NSAppleEventDescriptor *)reply
1425     NSString *urlString = [[event paramDescriptorForKeyword:keyDirectObject]
1426         stringValue];
1427     NSURL *url = [NSURL URLWithString:urlString];
1429     // We try to be compatible with TextMate's URL scheme here, as documented
1430     // at http://blog.macromates.com/2007/the-textmate-url-scheme/ . Currently,
1431     // this means that:
1432     //
1433     // The format is: mvim://open?<arguments> where arguments can be:
1434     //
1435     // * url â€” the actual file to open (i.e. a file://… URL), if you leave
1436     //         out this argument, the frontmost document is implied.
1437     // * line â€” line number to go to (one based).
1438     // * column â€” column number to go to (one based).
1439     //
1440     // Example: mvim://open?url=file:///etc/profile&line=20
1442     if ([[url host] isEqualToString:@"open"]) {
1443         NSMutableDictionary *dict = [NSMutableDictionary dictionary];
1445         // Parse query ("url=file://...&line=14") into a dictionary
1446         NSArray *queries = [[url query] componentsSeparatedByString:@"&"];
1447         NSEnumerator *enumerator = [queries objectEnumerator];
1448         NSString *param;
1449         while( param = [enumerator nextObject] ) {
1450             NSArray *arr = [param componentsSeparatedByString:@"="];
1451             if ([arr count] == 2) {
1452                 [dict setValue:[[arr lastObject]
1453                             stringByReplacingPercentEscapesUsingEncoding:
1454                                 NSUTF8StringEncoding]
1455                         forKey:[[arr objectAtIndex:0]
1456                             stringByReplacingPercentEscapesUsingEncoding:
1457                                 NSUTF8StringEncoding]];
1458             }
1459         }
1461         // Actually open the file.
1462         NSString *file = [dict objectForKey:@"url"];
1463         if (file != nil) {
1464             NSURL *fileUrl= [NSURL URLWithString:file];
1465             // TextMate only opens files that already exist.
1466             if ([fileUrl isFileURL]
1467                     && [[NSFileManager defaultManager] fileExistsAtPath:
1468                            [fileUrl path]]) {
1469                 // Strip 'file://' path, else application:openFiles: might think
1470                 // the file is not yet open.
1471                 NSArray *filenames = [NSArray arrayWithObject:[fileUrl path]];
1473                 // Look for the line and column options.
1474                 NSDictionary *args = nil;
1475                 NSString *line = [dict objectForKey:@"line"];
1476                 if (line) {
1477                     NSString *column = [dict objectForKey:@"column"];
1478                     if (column)
1479                         args = [NSDictionary dictionaryWithObjectsAndKeys:
1480                                 line, @"cursorLine",
1481                                 column, @"cursorColumn",
1482                                 nil];
1483                     else
1484                         args = [NSDictionary dictionaryWithObject:line
1485                                 forKey:@"cursorLine"];
1486                 }
1488                 [self openFiles:filenames withArguments:args];
1489             }
1490         }
1491     } else {
1492         NSAlert *alert = [[NSAlert alloc] init];
1493         [alert addButtonWithTitle:NSLocalizedString(@"OK",
1494             @"Dialog button")];
1496         [alert setMessageText:NSLocalizedString(@"Unknown URL Scheme",
1497             @"Unknown URL Scheme dialog, title")];
1498         [alert setInformativeText:[NSString stringWithFormat:NSLocalizedString(
1499             @"This version of MacVim does not support \"%@\""
1500             @" in its URL scheme.",
1501             @"Unknown URL Scheme dialog, text"),
1502             [url host]]];
1504         [alert setAlertStyle:NSWarningAlertStyle];
1505         [alert runModal];
1506         [alert release];
1507     }
1511 - (int)findLaunchingProcessWithoutArguments
1513     NSArray *keys = [pidArguments allKeysForObject:[NSNull null]];
1514     if ([keys count] > 0) {
1515         //NSLog(@"found launching process without arguments");
1516         return [[keys objectAtIndex:0] intValue];
1517     }
1519     return -1;
1522 - (MMVimController *)findUnusedEditor
1524     NSEnumerator *e = [vimControllers objectEnumerator];
1525     id vc;
1526     while ((vc = [e nextObject])) {
1527         if ([[[vc vimState] objectForKey:@"unusedEditor"] boolValue])
1528             return vc;
1529     }
1531     return nil;
1534 - (NSMutableDictionary *)extractArgumentsFromOdocEvent:
1535     (NSAppleEventDescriptor *)desc
1537     NSMutableDictionary *dict = [NSMutableDictionary dictionary];
1539     // 1. Extract ODB parameters (if any)
1540     NSAppleEventDescriptor *odbdesc = desc;
1541     if (![odbdesc paramDescriptorForKeyword:keyFileSender]) {
1542         // The ODB paramaters may hide inside the 'keyAEPropData' descriptor.
1543         odbdesc = [odbdesc paramDescriptorForKeyword:keyAEPropData];
1544         if (![odbdesc paramDescriptorForKeyword:keyFileSender])
1545             odbdesc = nil;
1546     }
1548     if (odbdesc) {
1549         NSAppleEventDescriptor *p =
1550                 [odbdesc paramDescriptorForKeyword:keyFileSender];
1551         if (p)
1552             [dict setObject:[NSNumber numberWithUnsignedInt:[p typeCodeValue]]
1553                      forKey:@"remoteID"];
1555         p = [odbdesc paramDescriptorForKeyword:keyFileCustomPath];
1556         if (p)
1557             [dict setObject:[p stringValue] forKey:@"remotePath"];
1559         p = [odbdesc paramDescriptorForKeyword:keyFileSenderToken];
1560         if (p) {
1561             [dict setObject:[NSNumber numberWithUnsignedLong:[p descriptorType]]
1562                      forKey:@"remoteTokenDescType"];
1563             [dict setObject:[p data] forKey:@"remoteTokenData"];
1564         }
1565     }
1567     // 2. Extract Xcode parameters (if any)
1568     NSAppleEventDescriptor *xcodedesc =
1569             [desc paramDescriptorForKeyword:keyAEPosition];
1570     if (xcodedesc) {
1571         NSRange range;
1572         MMSelectionRange *sr = (MMSelectionRange*)[[xcodedesc data] bytes];
1574         if (sr->lineNum < 0) {
1575             // Should select a range of lines.
1576             range.location = sr->startRange + 1;
1577             range.length = sr->endRange - sr->startRange + 1;
1578         } else {
1579             // Should only move cursor to a line.
1580             range.location = sr->lineNum + 1;
1581             range.length = 0;
1582         }
1584         [dict setObject:NSStringFromRange(range) forKey:@"selectionRange"];
1585     }
1587     // 3. Extract Spotlight search text (if any)
1588     NSAppleEventDescriptor *spotlightdesc = 
1589             [desc paramDescriptorForKeyword:keyAESearchText];
1590     if (spotlightdesc)
1591         [dict setObject:[spotlightdesc stringValue] forKey:@"searchText"];
1593     return dict;
1596 #ifdef MM_ENABLE_PLUGINS
1597 - (void)removePlugInMenu
1599     if ([plugInMenuItem menu])
1600         [[plugInMenuItem menu] removeItem:plugInMenuItem];
1603 - (void)addPlugInMenuToMenu:(NSMenu *)mainMenu
1605     NSMenu *windowsMenu = [mainMenu findWindowsMenu];
1607     if ([[plugInMenuItem submenu] numberOfItems] > 0) {
1608         int idx = windowsMenu ? [mainMenu indexOfItemWithSubmenu:windowsMenu]
1609                               : -1;
1610         if (idx > 0) {
1611             [mainMenu insertItem:plugInMenuItem atIndex:idx];
1612         } else {
1613             [mainMenu addItem:plugInMenuItem];
1614         }
1615     }
1617 #endif
1619 - (void)scheduleVimControllerPreloadAfterDelay:(NSTimeInterval)delay
1621     [self performSelector:@selector(preloadVimController:)
1622                withObject:nil
1623                afterDelay:delay];
1626 - (void)cancelVimControllerPreloadRequests
1628     [NSObject cancelPreviousPerformRequestsWithTarget:self
1629             selector:@selector(preloadVimController:)
1630               object:nil];
1633 - (void)preloadVimController:(id)sender
1635     // We only allow preloading of one Vim process at a time (to avoid hogging
1636     // CPU), so schedule another preload in a little while if necessary.
1637     if (-1 != preloadPid) {
1638         [self scheduleVimControllerPreloadAfterDelay:2];
1639         return;
1640     }
1642     if ([cachedVimControllers count] >= [self maxPreloadCacheSize])
1643         return;
1645     preloadPid = [self launchVimProcessWithArguments:
1646             [NSArray arrayWithObject:@"--mmwaitforack"]];
1649 - (int)maxPreloadCacheSize
1651     // The maximum number of Vim processes to keep in the cache can be
1652     // controlled via the user default "MMPreloadCacheSize".
1653     int maxCacheSize = [[NSUserDefaults standardUserDefaults]
1654             integerForKey:MMPreloadCacheSizeKey];
1655     if (maxCacheSize < 0) maxCacheSize = 0;
1656     else if (maxCacheSize > 10) maxCacheSize = 10;
1658     return maxCacheSize;
1661 - (MMVimController *)takeVimControllerFromCache
1663     // NOTE: After calling this message the backend corresponding to the
1664     // returned vim controller must be sent an acknowledgeConnection message,
1665     // else the vim process will be stuck.
1666     //
1667     // This method may return nil even though the cache might be non-empty; the
1668     // caller should handle this by starting a new Vim process.
1670     int i, count = [cachedVimControllers count];
1671     if (0 == count) return nil;
1673     // Locate the first Vim controller with up-to-date rc-files sourced.
1674     NSDate *rcDate = [self rcFilesModificationDate];
1675     for (i = 0; i < count; ++i) {
1676         MMVimController *vc = [cachedVimControllers objectAtIndex:i];
1677         NSDate *date = [vc creationDate];
1678         if ([date compare:rcDate] != NSOrderedAscending)
1679             break;
1680     }
1682     if (i > 0) {
1683         // Clear out cache entries whose vimrc/gvimrc files were sourced before
1684         // the latest modification date for those files.  This ensures that the
1685         // latest rc-files are always sourced for new windows.
1686         [self clearPreloadCacheWithCount:i];
1687     }
1689     if ([cachedVimControllers count] == 0) {
1690         [self scheduleVimControllerPreloadAfterDelay:2.0];
1691         return nil;
1692     }
1694     MMVimController *vc = [cachedVimControllers objectAtIndex:0];
1695     [vimControllers addObject:vc];
1696     [cachedVimControllers removeObjectAtIndex:0];
1697     [vc setIsPreloading:NO];
1699     // If the Vim process has finished loading then the window will displayed
1700     // now, otherwise it will be displayed when the OpenWindowMsgID message is
1701     // received.
1702     [[vc windowController] showWindow];
1704     // Since we've taken one controller from the cache we take the opportunity
1705     // to preload another.
1706     [self scheduleVimControllerPreloadAfterDelay:1];
1708     return vc;
1711 - (void)clearPreloadCacheWithCount:(int)count
1713     // Remove the 'count' first entries in the preload cache.  It is assumed
1714     // that objects are added/removed from the cache in a FIFO manner so that
1715     // this effectively clears the 'count' oldest entries.
1716     // If 'count' is negative, then the entire cache is cleared.
1718     if ([cachedVimControllers count] == 0 || count == 0)
1719         return;
1721     if (count < 0)
1722         count = [cachedVimControllers count];
1724     // Make sure the preloaded Vim processes get killed or they'll just hang
1725     // around being useless until MacVim is terminated.
1726     NSEnumerator *e = [cachedVimControllers objectEnumerator];
1727     MMVimController *vc;
1728     int n = count;
1729     while ((vc = [e nextObject]) && n-- > 0) {
1730         [[NSNotificationCenter defaultCenter] removeObserver:vc];
1731         [vc sendMessage:TerminateNowMsgID data:nil];
1733         // Since the preloaded processes were killed "prematurely" we have to
1734         // manually tell them to cleanup (it is not enough to simply release
1735         // them since deallocation and cleanup are separated).
1736         [vc cleanup];
1737     }
1739     n = count;
1740     while (n-- > 0 && [cachedVimControllers count] > 0)
1741         [cachedVimControllers removeObjectAtIndex:0];
1744 - (void)rebuildPreloadCache
1746     if ([self maxPreloadCacheSize] > 0) {
1747         [self clearPreloadCacheWithCount:-1];
1748         [self cancelVimControllerPreloadRequests];
1749         [self scheduleVimControllerPreloadAfterDelay:1.0];
1750     }
1753 - (NSDate *)rcFilesModificationDate
1755     // Check modification dates for ~/.vimrc and ~/.gvimrc and return the
1756     // latest modification date.  If ~/.vimrc does not exist, check ~/_vimrc
1757     // and similarly for gvimrc.
1758     // Returns distantPath if no rc files were found.
1760     NSDate *date = [NSDate distantPast];
1761     NSFileManager *fm = [NSFileManager defaultManager];
1763     NSString *path = [@"~/.vimrc" stringByExpandingTildeInPath];
1764     NSDictionary *attr = [fm fileAttributesAtPath:path traverseLink:YES];
1765     if (!attr) {
1766         path = [@"~/_vimrc" stringByExpandingTildeInPath];
1767         attr = [fm fileAttributesAtPath:path traverseLink:YES];
1768     }
1769     NSDate *modDate = [attr objectForKey:NSFileModificationDate];
1770     if (modDate)
1771         date = modDate;
1773     path = [@"~/.gvimrc" stringByExpandingTildeInPath];
1774     attr = [fm fileAttributesAtPath:path traverseLink:YES];
1775     if (!attr) {
1776         path = [@"~/_gvimrc" stringByExpandingTildeInPath];
1777         attr = [fm fileAttributesAtPath:path traverseLink:YES];
1778     }
1779     modDate = [attr objectForKey:NSFileModificationDate];
1780     if (modDate)
1781         date = [date laterDate:modDate];
1783     return date;
1786 - (BOOL)openVimControllerWithArguments:(NSDictionary *)arguments
1788     MMVimController *vc = [self findUnusedEditor];
1789     if (vc) {
1790         // Open files in an already open window.
1791         [[[vc windowController] window] makeKeyAndOrderFront:self];
1792         [vc passArguments:arguments];
1793     } else if ((vc = [self takeVimControllerFromCache])) {
1794         // Open files in a new window using a cached vim controller.  This
1795         // requires virtually no loading time so the new window will pop up
1796         // instantaneously.
1797         [vc passArguments:arguments];
1798         [[vc backendProxy] acknowledgeConnection];
1799     } else {
1800         // Open files in a launching Vim process or start a new process.  This
1801         // may take 1-2 seconds so there will be a visible delay before the
1802         // window appears on screen.
1803         int pid = [self findLaunchingProcessWithoutArguments];
1804         if (-1 == pid) {
1805             pid = [self launchVimProcessWithArguments:nil];
1806             if (-1 == pid)
1807                 return NO;
1808         }
1810         // TODO: If the Vim process fails to start, or if it changes PID,
1811         // then the memory allocated for these parameters will leak.
1812         // Ensure that this cannot happen or somehow detect it.
1814         if ([arguments count] > 0)
1815             [pidArguments setObject:arguments
1816                              forKey:[NSNumber numberWithInt:pid]];
1817     }
1819     return YES;
1822 - (void)activateWhenNextWindowOpens
1824     shouldActivateWhenNextWindowOpens = YES;
1827 - (void)startWatchingVimDir
1829     //NSLog(@"%s", _cmd);
1830 #if (MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_4)
1831     if (fsEventStream)
1832         return;
1833     if (NULL == FSEventStreamStart)
1834         return; // FSEvent functions are weakly linked
1836     NSString *path = [@"~/.vim" stringByExpandingTildeInPath];
1837     NSArray *pathsToWatch = [NSArray arrayWithObject:path];
1839     fsEventStream = FSEventStreamCreate(NULL, &fsEventCallback, NULL,
1840             (CFArrayRef)pathsToWatch, kFSEventStreamEventIdSinceNow,
1841             MMEventStreamLatency, kFSEventStreamCreateFlagNone);
1843     FSEventStreamScheduleWithRunLoop(fsEventStream,
1844             [[NSRunLoop currentRunLoop] getCFRunLoop],
1845             kCFRunLoopDefaultMode);
1847     FSEventStreamStart(fsEventStream);
1848     //NSLog(@"Started FS event stream");
1849 #endif
1852 - (void)stopWatchingVimDir
1854     //NSLog(@"%s", _cmd);
1855 #if (MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_4)
1856     if (NULL == FSEventStreamStop)
1857         return; // FSEvent functions are weakly linked
1859     if (fsEventStream) {
1860         FSEventStreamStop(fsEventStream);
1861         FSEventStreamInvalidate(fsEventStream);
1862         FSEventStreamRelease(fsEventStream);
1863         fsEventStream = NULL;
1864         //NSLog(@"Stopped FS event stream");
1865     }
1866 #endif
1870 - (void)handleFSEvent
1872     //NSLog(@"%s", _cmd);
1873     [self clearPreloadCacheWithCount:-1];
1875     // Several FS events may arrive in quick succession so make sure to cancel
1876     // any previous preload requests before making a new one.
1877     [self cancelVimControllerPreloadRequests];
1878     [self scheduleVimControllerPreloadAfterDelay:0.5];
1881 @end // MMAppController (Private)
1886     static int
1887 executeInLoginShell(NSString *path, NSArray *args)
1889     // Start a login shell and execute the command 'path' with arguments 'args'
1890     // in the shell.  This ensures that user environment variables are set even
1891     // when MacVim was started from the Finder.
1893     int pid = -1;
1894     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
1896     // Determine which shell to use to execute the command.  The user
1897     // may decide which shell to use by setting a user default or the
1898     // $SHELL environment variable.
1899     NSString *shell = [ud stringForKey:MMLoginShellCommandKey];
1900     if (!shell || [shell length] == 0)
1901         shell = [[[NSProcessInfo processInfo] environment]
1902             objectForKey:@"SHELL"];
1903     if (!shell)
1904         shell = @"/bin/bash";
1906     //NSLog(@"shell = %@", shell);
1908     // Bash needs the '-l' flag to launch a login shell.  The user may add
1909     // flags by setting a user default.
1910     NSString *shellArgument = [ud stringForKey:MMLoginShellArgumentKey];
1911     if (!shellArgument || [shellArgument length] == 0) {
1912         if ([[shell lastPathComponent] isEqual:@"bash"])
1913             shellArgument = @"-l";
1914         else
1915             shellArgument = nil;
1916     }
1918     //NSLog(@"shellArgument = %@", shellArgument);
1920     // Build input string to pipe to the login shell.
1921     NSMutableString *input = [NSMutableString stringWithFormat:
1922             @"exec \"%@\"", path];
1923     if (args) {
1924         // Append all arguments, making sure they are properly quoted, even
1925         // when they contain single quotes.
1926         NSEnumerator *e = [args objectEnumerator];
1927         id obj;
1929         while ((obj = [e nextObject])) {
1930             NSMutableString *arg = [NSMutableString stringWithString:obj];
1931             [arg replaceOccurrencesOfString:@"'" withString:@"'\"'\"'"
1932                                     options:NSLiteralSearch
1933                                       range:NSMakeRange(0, [arg length])];
1934             [input appendFormat:@" '%@'", arg];
1935         }
1936     }
1938     // Build the argument vector used to start the login shell.
1939     NSString *shellArg0 = [NSString stringWithFormat:@"-%@",
1940              [shell lastPathComponent]];
1941     char *shellArgv[3] = { (char *)[shellArg0 UTF8String], NULL, NULL };
1942     if (shellArgument)
1943         shellArgv[1] = (char *)[shellArgument UTF8String];
1945     // Get the C string representation of the shell path before the fork since
1946     // we must not call Foundation functions after a fork.
1947     const char *shellPath = [shell fileSystemRepresentation];
1949     // Fork and execute the process.
1950     int ds[2];
1951     if (pipe(ds)) return -1;
1953     pid = fork();
1954     if (pid == -1) {
1955         return -1;
1956     } else if (pid == 0) {
1957         // Child process
1959         // We need to undo our zombie avoidance as Vim waits for and needs
1960         // the exit statuses of processes it spawns.
1961         signal(SIGCHLD, SIG_DFL);
1963         if (close(ds[1]) == -1) exit(255);
1964         if (dup2(ds[0], 0) == -1) exit(255);
1966         // Without the following call warning messages like this appear on the
1967         // console:
1968         //     com.apple.launchd[69] : Stray process with PGID equal to this
1969         //                             dead job: PID 1589 PPID 1 Vim
1970         setsid();
1972         execv(shellPath, shellArgv);
1974         // Never reached unless execv fails
1975         exit(255);
1976     } else {
1977         // Parent process
1978         if (close(ds[0]) == -1) return -1;
1980         // Send input to execute to the child process
1981         [input appendString:@"\n"];
1982         int bytes = [input lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
1984         if (write(ds[1], [input UTF8String], bytes) != bytes) return -1;
1985         if (close(ds[1]) == -1) return -1;
1986     }
1988     return pid;