Modifier key sends Esc
[MacVim.git] / src / MacVim / MMAppController.m
blob6f15abafb7deb3b528ac53f94f40354037e41e18
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     NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:
158         [NSNumber numberWithBool:NO],   MMNoWindowKey,
159         [NSNumber numberWithInt:64],    MMTabMinWidthKey,
160         [NSNumber numberWithInt:6*64],  MMTabMaxWidthKey,
161         [NSNumber numberWithInt:132],   MMTabOptimumWidthKey,
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 numberWithInt:MMDisableFakeEsc],
189                                         MMFakeEscModifierKey,
190         [NSNumber numberWithFloat:0.3], MMFakeEscTimeoutKey,
191         [NSNumber numberWithBool:NO],   MMFakeEscOnKeyDownKey,
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: When the user is resizing the window the AppKit puts the run
239     // loop in event tracking mode.  Unless the connection listens to
240     // request in this mode, live resizing won't work.
241     [connection addRequestMode:NSEventTrackingRunLoopMode];
243     // NOTE!  If the name of the connection changes here it must also be
244     // updated in MMBackend.m.
245     NSString *name = [NSString stringWithFormat:@"%@-connection",
246              [[NSBundle mainBundle] bundlePath]];
247     //NSLog(@"Registering connection with name '%@'", name);
248     if (![connection registerName:name]) {
249         NSLog(@"FATAL ERROR: Failed to register connection with name '%@'",
250                 name);
251         [connection release];  connection = nil;
252     }
254     return self;
257 - (void)dealloc
259     //NSLog(@"MMAppController dealloc");
261     [connection release];  connection = nil;
262     [pidArguments release];  pidArguments = nil;
263     [vimControllers release];  vimControllers = nil;
264     [cachedVimControllers release];  cachedVimControllers = nil;
265     [openSelectionString release];  openSelectionString = nil;
266     [recentFilesMenuItem release];  recentFilesMenuItem = nil;
267     [defaultMainMenu release];  defaultMainMenu = nil;
268 #ifdef MM_ENABLE_PLUGINS
269     [plugInMenuItem release];  plugInMenuItem = nil;
270 #endif
271     [appMenuItemTemplate release];  appMenuItemTemplate = nil;
273     [super dealloc];
276 - (void)applicationWillFinishLaunching:(NSNotification *)notification
278     // Remember the default menu so that it can be restored if the user closes
279     // all editor windows.
280     defaultMainMenu = [[NSApp mainMenu] retain];
282     // Store a copy of the default app menu so we can use this as a template
283     // for all other menus.  We make a copy here because the "Services" menu
284     // will not yet have been populated at this time.  If we don't we get
285     // problems trying to set key equivalents later on because they might clash
286     // with items on the "Services" menu.
287     appMenuItemTemplate = [defaultMainMenu itemAtIndex:0];
288     appMenuItemTemplate = [appMenuItemTemplate copy];
290     // Set up the "Open Recent" menu. See
291     //   http://lapcatsoftware.com/blog/2007/07/10/
292     //     working-without-a-nib-part-5-open-recent-menu/
293     // and
294     //   http://www.cocoabuilder.com/archive/message/cocoa/2007/8/15/187793
295     // for more information.
296     //
297     // The menu itself is created in MainMenu.nib but we still seem to have to
298     // hack around a bit to get it to work.  (This has to be done in
299     // applicationWillFinishLaunching at the latest, otherwise it doesn't
300     // work.)
301     NSMenu *fileMenu = [defaultMainMenu findFileMenu];
302     if (fileMenu) {
303         int idx = [fileMenu indexOfItemWithAction:@selector(fileOpen:)];
304         if (idx >= 0 && idx+1 < [fileMenu numberOfItems])
306         recentFilesMenuItem = [fileMenu itemWithTitle:@"Open Recent"];
307         [[recentFilesMenuItem submenu] performSelector:@selector(_setMenuName:)
308                                         withObject:@"NSRecentDocumentsMenu"];
310         // Note: The "Recent Files" menu must be moved around since there is no
311         // -[NSApp setRecentFilesMenu:] method.  We keep a reference to it to
312         // facilitate this move (see setMainMenu: below).
313         [recentFilesMenuItem retain];
314     }
316 #if MM_HANDLE_XCODE_MOD_EVENT
317     [[NSAppleEventManager sharedAppleEventManager]
318             setEventHandler:self
319                 andSelector:@selector(handleXcodeModEvent:replyEvent:)
320               forEventClass:'KAHL'
321                  andEventID:'MOD '];
322 #endif
324     // Register 'mvim://' URL handler
325     [[NSAppleEventManager sharedAppleEventManager]
326             setEventHandler:self
327                 andSelector:@selector(handleGetURLEvent:replyEvent:)
328               forEventClass:kInternetEventClass
329                  andEventID:kAEGetURL];
332 - (void)applicationDidFinishLaunching:(NSNotification *)notification
334     [NSApp setServicesProvider:self];
335 #ifdef MM_ENABLE_PLUGINS
336     [[MMPlugInManager sharedManager] loadAllPlugIns];
337 #endif
339     if ([self maxPreloadCacheSize] > 0) {
340         [self scheduleVimControllerPreloadAfterDelay:2];
341         [self startWatchingVimDir];
342     }
345 - (BOOL)applicationShouldOpenUntitledFile:(NSApplication *)sender
347     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
348     NSAppleEventManager *aem = [NSAppleEventManager sharedAppleEventManager];
349     NSAppleEventDescriptor *desc = [aem currentAppleEvent];
351     // The user default MMUntitledWindow can be set to control whether an
352     // untitled window should open on 'Open' and 'Reopen' events.
353     int untitledWindowFlag = [ud integerForKey:MMUntitledWindowKey];
354     if ([desc eventID] == kAEOpenApplication
355             && (untitledWindowFlag & MMUntitledWindowOnOpen) == 0)
356         return NO;
357     else if ([desc eventID] == kAEReopenApplication
358             && (untitledWindowFlag & MMUntitledWindowOnReopen) == 0)
359         return NO;
361     // When a process is started from the command line, the 'Open' event will
362     // contain a parameter to surpress the opening of an untitled window.
363     desc = [desc paramDescriptorForKeyword:keyAEPropData];
364     desc = [desc paramDescriptorForKeyword:keyMMUntitledWindow];
365     if (desc && ![desc booleanValue])
366         return NO;
368     // Never open an untitled window if there is at least one open window or if
369     // there are processes that are currently launching.
370     if ([vimControllers count] > 0 || [pidArguments count] > 0)
371         return NO;
373     // NOTE!  This way it possible to start the app with the command-line
374     // argument '-nowindow yes' and no window will be opened by default.
375     return ![ud boolForKey:MMNoWindowKey];
378 - (BOOL)applicationOpenUntitledFile:(NSApplication *)sender
380     [self newWindow:self];
381     return YES;
384 - (void)application:(NSApplication *)sender openFiles:(NSArray *)filenames
386     // Extract ODB/Xcode/Spotlight parameters from the current Apple event,
387     // sort the filenames, and then let openFiles:withArguments: do the heavy
388     // lifting.
390     if (!(filenames && [filenames count] > 0))
391         return;
393     // Sort filenames since the Finder doesn't take care in preserving the
394     // order in which files are selected anyway (and "sorted" is more
395     // predictable than "random").
396     if ([filenames count] > 1)
397         filenames = [filenames sortedArrayUsingSelector:
398                 @selector(localizedCompare:)];
400     // Extract ODB/Xcode/Spotlight parameters from the current Apple event
401     NSMutableDictionary *arguments = [self extractArgumentsFromOdocEvent:
402             [[NSAppleEventManager sharedAppleEventManager] currentAppleEvent]];
404     if ([self openFiles:filenames withArguments:arguments]) {
405         [NSApp replyToOpenOrPrint:NSApplicationDelegateReplySuccess];
406     } else {
407         // TODO: Notify user of failure?
408         [NSApp replyToOpenOrPrint:NSApplicationDelegateReplyFailure];
409     }
412 - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender
414     return (MMTerminateWhenLastWindowClosed ==
415             [[NSUserDefaults standardUserDefaults]
416                 integerForKey:MMLastWindowClosedBehaviorKey]);
419 - (NSApplicationTerminateReply)applicationShouldTerminate:
420     (NSApplication *)sender
422     // TODO: Follow Apple's guidelines for 'Graceful Application Termination'
423     // (in particular, allow user to review changes and save).
424     int reply = NSTerminateNow;
425     BOOL modifiedBuffers = NO;
427     // Go through windows, checking for modified buffers.  (Each Vim process
428     // tells MacVim when any buffer has been modified and MacVim sets the
429     // 'documentEdited' flag of the window correspondingly.)
430     NSEnumerator *e = [[NSApp windows] objectEnumerator];
431     id window;
432     while ((window = [e nextObject])) {
433         if ([window isDocumentEdited]) {
434             modifiedBuffers = YES;
435             break;
436         }
437     }
439     if (modifiedBuffers) {
440         NSAlert *alert = [[NSAlert alloc] init];
441         [alert setAlertStyle:NSWarningAlertStyle];
442         [alert addButtonWithTitle:NSLocalizedString(@"Quit",
443                 @"Dialog button")];
444         [alert addButtonWithTitle:NSLocalizedString(@"Cancel",
445                 @"Dialog button")];
446         [alert setMessageText:NSLocalizedString(@"Quit without saving?",
447                 @"Quit dialog with changed buffers, title")];
448         [alert setInformativeText:NSLocalizedString(
449                 @"There are modified buffers, "
450                 "if you quit now all changes will be lost.  Quit anyway?",
451                 @"Quit dialog with changed buffers, text")];
453         if ([alert runModal] != NSAlertFirstButtonReturn)
454             reply = NSTerminateCancel;
456         [alert release];
457     } else {
458         // No unmodified buffers, but give a warning if there are multiple
459         // windows and/or tabs open.
460         int numWindows = [vimControllers count];
461         int numTabs = 0;
463         // Count the number of open tabs
464         e = [vimControllers objectEnumerator];
465         id vc;
466         while ((vc = [e nextObject])) {
467             NSString *eval = [vc evaluateVimExpression:@"tabpagenr('$')"];
468             if (eval) {
469                 int count = [eval intValue];
470                 if (count > 0 && count < INT_MAX)
471                     numTabs += count;
472             }
473         }
475         if (numWindows > 1 || numTabs > 1) {
476             NSAlert *alert = [[NSAlert alloc] init];
477             [alert setAlertStyle:NSWarningAlertStyle];
478             [alert addButtonWithTitle:NSLocalizedString(@"Quit",
479                     @"Dialog button")];
480             [alert addButtonWithTitle:NSLocalizedString(@"Cancel",
481                     @"Dialog button")];
482             [alert setMessageText:NSLocalizedString(
483                     @"Are you sure you want to quit MacVim?",
484                     @"Quit dialog with no changed buffers, title")];
486             NSString *info = nil;
487             if (numWindows > 1) {
488                 if (numTabs > numWindows)
489                     info = [NSString stringWithFormat:NSLocalizedString(
490                             @"There are %d windows open in MacVim, with a "
491                             "total of %d tabs. Do you want to quit anyway?",
492                             @"Quit dialog with no changed buffers, text"),
493                          numWindows, numTabs];
494                 else
495                     info = [NSString stringWithFormat:NSLocalizedString(
496                             @"There are %d windows open in MacVim. "
497                             "Do you want to quit anyway?",
498                             @"Quit dialog with no changed buffers, text"),
499                         numWindows];
501             } else {
502                 info = [NSString stringWithFormat:NSLocalizedString(
503                         @"There are %d tabs open in MacVim. "
504                         "Do you want to quit anyway?",
505                         @"Quit dialog with no changed buffers, text"), 
506                      numTabs];
507             }
509             [alert setInformativeText:info];
511             if ([alert runModal] != NSAlertFirstButtonReturn)
512                 reply = NSTerminateCancel;
514             [alert release];
515         }
516     }
519     // Tell all Vim processes to terminate now (otherwise they'll leave swap
520     // files behind).
521     if (NSTerminateNow == reply) {
522         e = [vimControllers objectEnumerator];
523         id vc;
524         while ((vc = [e nextObject]))
525             [vc sendMessage:TerminateNowMsgID data:nil];
527         e = [cachedVimControllers objectEnumerator];
528         while ((vc = [e nextObject]))
529             [vc sendMessage:TerminateNowMsgID data:nil];
531         // Give Vim processes a chance to terminate before MacVim.  If they
532         // haven't terminated by the time applicationWillTerminate: is sent,
533         // they may be forced to quit (see below).
534         usleep(MMTerminationSleepPeriod);
535     }
537     return reply;
540 - (void)applicationWillTerminate:(NSNotification *)notification
542     [self stopWatchingVimDir];
544 #ifdef MM_ENABLE_PLUGINS
545     [[MMPlugInManager sharedManager] unloadAllPlugIns];
546 #endif
548 #if MM_HANDLE_XCODE_MOD_EVENT
549     [[NSAppleEventManager sharedAppleEventManager]
550             removeEventHandlerForEventClass:'KAHL'
551                                  andEventID:'MOD '];
552 #endif
554     // This will invalidate all connections (since they were spawned from this
555     // connection).
556     [connection invalidate];
558     // Send a SIGINT to all running Vim processes, so that they are sure to
559     // receive the connectionDidDie: notification (a process has to be checking
560     // the run-loop for this to happen).
561     unsigned i, count = [vimControllers count];
562     for (i = 0; i < count; ++i) {
563         MMVimController *controller = [vimControllers objectAtIndex:i];
564         int pid = [controller pid];
565         if (-1 != pid)
566             kill(pid, SIGINT);
567     }
569     if (fontContainerRef) {
570         ATSFontDeactivate(fontContainerRef, NULL, kATSOptionFlagsDefault);
571         fontContainerRef = 0;
572     }
574     [NSApp setDelegate:nil];
577 + (MMAppController *)sharedInstance
579     // Note: The app controller is a singleton which is instantiated in
580     // MainMenu.nib where it is also connected as the delegate of NSApp.
581     id delegate = [NSApp delegate];
582     return [delegate isKindOfClass:self] ? (MMAppController*)delegate : nil;
585 - (NSMenu *)defaultMainMenu
587     return defaultMainMenu;
590 - (NSMenuItem *)appMenuItemTemplate
592     return appMenuItemTemplate;
595 - (void)removeVimController:(id)controller
597     int idx = [vimControllers indexOfObject:controller];
598     if (NSNotFound == idx)
599         return;
601     [controller cleanup];
603     [vimControllers removeObjectAtIndex:idx];
605     if (![vimControllers count]) {
606         // The last editor window just closed so restore the main menu back to
607         // its default state (which is defined in MainMenu.nib).
608         [self setMainMenu:defaultMainMenu];
610         BOOL hide = (MMHideWhenLastWindowClosed ==
611                     [[NSUserDefaults standardUserDefaults]
612                         integerForKey:MMLastWindowClosedBehaviorKey]);
613         if (hide)
614             [NSApp hide:self];
615     }
618 - (void)windowControllerWillOpen:(MMWindowController *)windowController
620     NSPoint topLeft = NSZeroPoint;
621     NSWindow *topWin = [[[self topmostVimController] windowController] window];
622     NSWindow *win = [windowController window];
624     if (!win) return;
626     // If there is a window belonging to a Vim process, cascade from it,
627     // otherwise use the autosaved window position (if any).
628     if (topWin) {
629         NSRect frame = [topWin frame];
630         topLeft = NSMakePoint(frame.origin.x, NSMaxY(frame));
631     } else {
632         NSString *topLeftString = [[NSUserDefaults standardUserDefaults]
633             stringForKey:MMTopLeftPointKey];
634         if (topLeftString)
635             topLeft = NSPointFromString(topLeftString);
636     }
638     if (!NSEqualPoints(topLeft, NSZeroPoint)) {
639         NSPoint oldTopLeft = topLeft;
640         if (topWin)
641             topLeft = [win cascadeTopLeftFromPoint:topLeft];
643         [win setFrameTopLeftPoint:topLeft];
645         if ([win frame].origin.y < [[win screen] frame].origin.y) {
646             // Try to avoid shifting the new window downwards if it means that
647             // the bottom of the window will be off the screen.  E.g. if the
648             // user has set windows to open maximized in the vertical direction
649             // then the new window will cascade horizontally only.
650             topLeft.y = oldTopLeft.y;
651             [win setFrameTopLeftPoint:topLeft];
652         }
653     }
655     if (1 == [vimControllers count]) {
656         // The first window autosaves its position.  (The autosaving
657         // features of Cocoa are not used because we need more control over
658         // what is autosaved and when it is restored.)
659         [windowController setWindowAutosaveKey:MMTopLeftPointKey];
660     }
662     if (openSelectionString) {
663         // TODO: Pass this as a parameter instead!  Get rid of
664         // 'openSelectionString' etc.
665         //
666         // There is some text to paste into this window as a result of the
667         // services menu "Open selection ..." being used.
668         [[windowController vimController] dropString:openSelectionString];
669         [openSelectionString release];
670         openSelectionString = nil;
671     }
673     if (shouldActivateWhenNextWindowOpens) {
674         [NSApp activateIgnoringOtherApps:YES];
675         shouldActivateWhenNextWindowOpens = NO;
676     }
679 - (void)setMainMenu:(NSMenu *)mainMenu
681     if ([NSApp mainMenu] == mainMenu) return;
683     // If the new menu has a "Recent Files" dummy item, then swap the real item
684     // for the dummy.  We are forced to do this since Cocoa initializes the
685     // "Recent Files" menu and there is no way to simply point Cocoa to a new
686     // item each time the menus are swapped.
687     NSMenu *fileMenu = [mainMenu findFileMenu];
688     if (recentFilesMenuItem && fileMenu) {
689         int dummyIdx =
690                 [fileMenu indexOfItemWithAction:@selector(recentFilesDummy:)];
691         if (dummyIdx >= 0) {
692             NSMenuItem *dummyItem = [[fileMenu itemAtIndex:dummyIdx] retain];
693             [fileMenu removeItemAtIndex:dummyIdx];
695             NSMenu *recentFilesParentMenu = [recentFilesMenuItem menu];
696             int idx = [recentFilesParentMenu indexOfItem:recentFilesMenuItem];
697             if (idx >= 0) {
698                 [[recentFilesMenuItem retain] autorelease];
699                 [recentFilesParentMenu removeItemAtIndex:idx];
700                 [recentFilesParentMenu insertItem:dummyItem atIndex:idx];
701             }
703             [fileMenu insertItem:recentFilesMenuItem atIndex:dummyIdx];
704             [dummyItem release];
705         }
706     }
708     // Now set the new menu.  Notice that we keep one menu for each editor
709     // window since each editor can have its own set of menus.  When swapping
710     // menus we have to tell Cocoa where the new "MacVim", "Windows", and
711     // "Services" menu are.
712     [NSApp setMainMenu:mainMenu];
714     // Setting the "MacVim" (or "Application") menu ensures that it is typeset
715     // in boldface.  (The setAppleMenu: method used to be public but is now
716     // private so this will have to be considered a bit of a hack!)
717     NSMenu *appMenu = [mainMenu findApplicationMenu];
718     [NSApp performSelector:@selector(setAppleMenu:) withObject:appMenu];
720     NSMenu *servicesMenu = [mainMenu findServicesMenu];
721     [NSApp setServicesMenu:servicesMenu];
723     NSMenu *windowsMenu = [mainMenu findWindowsMenu];
724     if (windowsMenu) {
725         // Cocoa isn't clever enough to get rid of items it has added to the
726         // "Windows" menu so we have to do it ourselves otherwise there will be
727         // multiple menu items for each window in the "Windows" menu.
728         //   This code assumes that the only items Cocoa add are ones which
729         // send off the action makeKeyAndOrderFront:.  (Cocoa will not add
730         // another separator item if the last item on the "Windows" menu
731         // already is a separator, so we needen't worry about separators.)
732         int i, count = [windowsMenu numberOfItems];
733         for (i = count-1; i >= 0; --i) {
734             NSMenuItem *item = [windowsMenu itemAtIndex:i];
735             if ([item action] == @selector(makeKeyAndOrderFront:))
736                 [windowsMenu removeItem:item];
737         }
738     }
739     [NSApp setWindowsMenu:windowsMenu];
741 #ifdef MM_ENABLE_PLUGINS
742     // Move plugin menu from old to new main menu.
743     [self removePlugInMenu];
744     [self addPlugInMenuToMenu:mainMenu];
745 #endif
748 - (NSArray *)filterOpenFiles:(NSArray *)filenames
750     return [self filterOpenFiles:filenames openFilesDict:nil];
753 - (BOOL)openFiles:(NSArray *)filenames withArguments:(NSDictionary *)args
755     // Opening files works like this:
756     //  a) filter out any already open files
757     //  b) open any remaining files
758     //
759     // A file is opened in an untitled window if there is one (it may be
760     // currently launching, or it may already be visible), otherwise a new
761     // window is opened.
762     //
763     // Each launching Vim process has a dictionary of arguments that are passed
764     // to the process when in checks in (via connectBackend:pid:).  The
765     // arguments for each launching process can be looked up by its PID (in the
766     // pidArguments dictionary).
768     NSMutableDictionary *arguments = (args ? [[args mutableCopy] autorelease]
769                                            : [NSMutableDictionary dictionary]);
771     //
772     // a) Filter out any already open files
773     //
774     NSString *firstFile = [filenames objectAtIndex:0];
775     MMVimController *firstController = nil;
776     NSDictionary *openFilesDict = nil;
777     filenames = [self filterOpenFiles:filenames openFilesDict:&openFilesDict];
779     // Pass arguments to vim controllers that had files open.
780     id key;
781     NSEnumerator *e = [openFilesDict keyEnumerator];
783     // (Indicate that we do not wish to open any files at the moment.)
784     [arguments setObject:[NSNumber numberWithBool:YES] forKey:@"dontOpen"];
786     while ((key = [e nextObject])) {
787         NSArray *files = [openFilesDict objectForKey:key];
788         [arguments setObject:files forKey:@"filenames"];
790         MMVimController *vc = [key pointerValue];
791         [vc passArguments:arguments];
793         // If this controller holds the first file, then remember it for later.
794         if ([files containsObject:firstFile])
795             firstController = vc;
796     }
798     if ([filenames count] == 0) {
799         // Raise the window containing the first file that was already open,
800         // and make sure that the tab containing that file is selected.  Only
801         // do this when there are no more files to open, otherwise sometimes
802         // the window with 'firstFile' will be raised, other times it might be
803         // the window that will open with the files in the 'filenames' array.
804         firstFile = [firstFile stringByEscapingSpecialFilenameCharacters];
805         NSString *input = [NSString stringWithFormat:@"<C-\\><C-N>"
806                 ":let oldswb=&swb|let &swb=\"useopen,usetab\"|"
807                 "tab sb %@|let &swb=oldswb|unl oldswb|"
808                 "cal foreground()|redr|f<CR>", firstFile];
810         [firstController addVimInput:input];
812         return YES;
813     }
815     // Add filenames to "Recent Files" menu, unless they are being edited
816     // remotely (using ODB).
817     if ([arguments objectForKey:@"remoteID"] == nil) {
818         [[NSDocumentController sharedDocumentController]
819                 noteNewRecentFilePaths:filenames];
820     }
822     //
823     // b) Open any remaining files
824     //
825     MMVimController *vc;
826     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
827     BOOL openInCurrentWindow = [ud boolForKey:MMOpenInCurrentWindowKey];
829     // The meaning of "layout" is defined by the WIN_* defines in main.c.
830     int layout = [ud integerForKey:MMOpenLayoutKey];
831     BOOL splitVert = [ud boolForKey:MMVerticalSplitKey];
832     if (splitVert && MMLayoutHorizontalSplit == layout)
833         layout = MMLayoutVerticalSplit;
834     if (layout < 0 || (layout > MMLayoutTabs && openInCurrentWindow))
835         layout = MMLayoutTabs;
837     [arguments setObject:[NSNumber numberWithInt:layout] forKey:@"layout"];
838     [arguments setObject:filenames forKey:@"filenames"];
839     // (Indicate that files should be opened from now on.)
840     [arguments setObject:[NSNumber numberWithBool:NO] forKey:@"dontOpen"];
842     if (openInCurrentWindow && (vc = [self topmostVimController])) {
843         // Open files in an already open window.
844         [[[vc windowController] window] makeKeyAndOrderFront:self];
845         [vc passArguments:arguments];
846         return YES;
847     }
849     BOOL openOk = YES;
850     int numFiles = [filenames count];
851     if (MMLayoutWindows == layout && numFiles > 1) {
852         // Open one file at a time in a new window, but don't open too many at
853         // once (at most cap+1 windows will open).  If the user has increased
854         // the preload cache size we'll take that as a hint that more windows
855         // should be able to open at once.
856         int cap = [self maxPreloadCacheSize] - 1;
857         if (cap < 4) cap = 4;
858         if (cap > numFiles) cap = numFiles;
860         int i;
861         for (i = 0; i < cap; ++i) {
862             NSArray *a = [NSArray arrayWithObject:[filenames objectAtIndex:i]];
863             [arguments setObject:a forKey:@"filenames"];
865             // NOTE: We have to copy the args since we'll mutate them in the
866             // next loop and the below call may retain the arguments while
867             // waiting for a process to start.
868             NSDictionary *args = [[arguments copy] autorelease];
870             openOk = [self openVimControllerWithArguments:args];
871             if (!openOk) break;
872         }
874         // Open remaining files in tabs in a new window.
875         if (openOk && numFiles > cap) {
876             NSRange range = { i, numFiles-cap };
877             NSArray *a = [filenames subarrayWithRange:range];
878             [arguments setObject:a forKey:@"filenames"];
879             [arguments setObject:[NSNumber numberWithInt:MMLayoutTabs]
880                           forKey:@"layout"];
882             openOk = [self openVimControllerWithArguments:arguments];
883         }
884     } else {
885         // Open all files at once.
886         openOk = [self openVimControllerWithArguments:arguments];
887     }
889     return openOk;
892 #ifdef MM_ENABLE_PLUGINS
893 - (void)addItemToPlugInMenu:(NSMenuItem *)item
895     NSMenu *menu = [plugInMenuItem submenu];
896     [menu addItem:item];
897     if ([menu numberOfItems] == 1)
898         [self addPlugInMenuToMenu:[NSApp mainMenu]];
901 - (void)removeItemFromPlugInMenu:(NSMenuItem *)item
903     NSMenu *menu = [plugInMenuItem submenu];
904     [menu removeItem:item];
905     if ([menu numberOfItems] == 0)
906         [self removePlugInMenu];
908 #endif
910 - (IBAction)newWindow:(id)sender
912     // A cached controller requires no loading times and results in the new
913     // window popping up instantaneously.  If the cache is empty it may take
914     // 1-2 seconds to start a new Vim process.
915     MMVimController *vc = [self takeVimControllerFromCache];
916     if (vc) {
917         [[vc backendProxy] acknowledgeConnection];
918     } else {
919         [self launchVimProcessWithArguments:nil];
920     }
923 - (IBAction)newWindowAndActivate:(id)sender
925     [self activateWhenNextWindowOpens];
926     [self newWindow:sender];
929 - (IBAction)fileOpen:(id)sender
931     NSString *dir = nil;
932     BOOL trackPwd = [[NSUserDefaults standardUserDefaults]
933             boolForKey:MMDialogsTrackPwdKey];
934     if (trackPwd) {
935         MMVimController *vc = [self keyVimController];
936         if (vc) dir = [[vc vimState] objectForKey:@"pwd"];
937     }
939     NSOpenPanel *panel = [NSOpenPanel openPanel];
940     [panel setAllowsMultipleSelection:YES];
941     [panel setAccessoryView:openPanelAccessoryView()];
943     int result = [panel runModalForDirectory:dir file:nil types:nil];
944     if (NSOKButton == result)
945         [self application:NSApp openFiles:[panel filenames]];
948 - (IBAction)selectNextWindow:(id)sender
950     unsigned i, count = [vimControllers count];
951     if (!count) return;
953     NSWindow *keyWindow = [NSApp keyWindow];
954     for (i = 0; i < count; ++i) {
955         MMVimController *vc = [vimControllers objectAtIndex:i];
956         if ([[[vc windowController] window] isEqual:keyWindow])
957             break;
958     }
960     if (i < count) {
961         if (++i >= count)
962             i = 0;
963         MMVimController *vc = [vimControllers objectAtIndex:i];
964         [[vc windowController] showWindow:self];
965     }
968 - (IBAction)selectPreviousWindow:(id)sender
970     unsigned i, count = [vimControllers count];
971     if (!count) return;
973     NSWindow *keyWindow = [NSApp keyWindow];
974     for (i = 0; i < count; ++i) {
975         MMVimController *vc = [vimControllers objectAtIndex:i];
976         if ([[[vc windowController] window] isEqual:keyWindow])
977             break;
978     }
980     if (i < count) {
981         if (i > 0) {
982             --i;
983         } else {
984             i = count - 1;
985         }
986         MMVimController *vc = [vimControllers objectAtIndex:i];
987         [[vc windowController] showWindow:self];
988     }
991 - (IBAction)orderFrontPreferencePanel:(id)sender
993     [[MMPreferenceController sharedPrefsWindowController] showWindow:self];
996 - (IBAction)openWebsite:(id)sender
998     [[NSWorkspace sharedWorkspace] openURL:
999             [NSURL URLWithString:MMWebsiteString]];
1002 - (IBAction)showVimHelp:(id)sender
1004     // Open a new window with the help window maximized.
1005     [self launchVimProcessWithArguments:[NSArray arrayWithObjects:
1006             @"-c", @":h gui_mac", @"-c", @":res", nil]];
1009 - (IBAction)zoomAll:(id)sender
1011     [NSApp makeWindowsPerform:@selector(performZoom:) inOrder:YES];
1014 - (IBAction)atsuiButtonClicked:(id)sender
1016     // This action is called when the user clicks the "use ATSUI renderer"
1017     // button in the advanced preferences pane.
1018     [self rebuildPreloadCache];
1021 - (IBAction)loginShellButtonClicked:(id)sender
1023     // This action is called when the user clicks the "use login shell" button
1024     // in the advanced preferences pane.
1025     [self rebuildPreloadCache];
1028 - (IBAction)quickstartButtonClicked:(id)sender
1030     if ([self maxPreloadCacheSize] > 0) {
1031         [self scheduleVimControllerPreloadAfterDelay:1.0];
1032         [self startWatchingVimDir];
1033     } else {
1034         [self cancelVimControllerPreloadRequests];
1035         [self clearPreloadCacheWithCount:-1];
1036         [self stopWatchingVimDir];
1037     }
1040 - (byref id <MMFrontendProtocol>)
1041     connectBackend:(byref in id <MMBackendProtocol>)backend
1042                pid:(int)pid
1044     //NSLog(@"Connect backend (pid=%d)", pid);
1045     NSNumber *pidKey = [NSNumber numberWithInt:pid];
1046     MMVimController *vc = nil;
1048     @try {
1049         [(NSDistantObject*)backend
1050                 setProtocolForProxy:@protocol(MMBackendProtocol)];
1052         vc = [[[MMVimController alloc] initWithBackend:backend pid:pid]
1053                 autorelease];
1055         if (preloadPid == pid) {
1056             // This backend was preloaded, so add it to the cache and schedule
1057             // another vim process to be preloaded.
1058             preloadPid = -1;
1059             [vc setIsPreloading:YES];
1060             [cachedVimControllers addObject:vc];
1061             [self scheduleVimControllerPreloadAfterDelay:1];
1063             return vc;
1064         }
1066         [vimControllers addObject:vc];
1068         id args = [pidArguments objectForKey:pidKey];
1069         if (args && [NSNull null] != args)
1070             [vc passArguments:args];
1072         // HACK!  MacVim does not get activated if it is launched from the
1073         // terminal, so we forcibly activate here unless it is an untitled
1074         // window opening.  Untitled windows are treated differently, else
1075         // MacVim would steal the focus if another app was activated while the
1076         // untitled window was loading.
1077         if (!args || args != [NSNull null])
1078             [NSApp activateIgnoringOtherApps:YES];
1080         if (args)
1081             [pidArguments removeObjectForKey:pidKey];
1083         return vc;
1084     }
1086     @catch (NSException *e) {
1087         NSLog(@"Exception caught in %s: \"%@\"", _cmd, e);
1089         if (vc)
1090             [vimControllers removeObject:vc];
1092         [pidArguments removeObjectForKey:pidKey];
1093     }
1095     return nil;
1098 - (NSArray *)serverList
1100     NSMutableArray *array = [NSMutableArray array];
1102     unsigned i, count = [vimControllers count];
1103     for (i = 0; i < count; ++i) {
1104         MMVimController *controller = [vimControllers objectAtIndex:i];
1105         if ([controller serverName])
1106             [array addObject:[controller serverName]];
1107     }
1109     return array;
1112 - (MMVimController *)keyVimController
1114     NSWindow *keyWindow = [NSApp keyWindow];
1115     if (keyWindow) {
1116         unsigned i, count = [vimControllers count];
1117         for (i = 0; i < count; ++i) {
1118             MMVimController *vc = [vimControllers objectAtIndex:i];
1119             if ([[[vc windowController] window] isEqual:keyWindow])
1120                 return vc;
1121         }
1122     }
1124     return nil;
1127 @end // MMAppController
1132 @implementation MMAppController (MMServices)
1134 - (void)openSelection:(NSPasteboard *)pboard userData:(NSString *)userData
1135                 error:(NSString **)error
1137     if (![[pboard types] containsObject:NSStringPboardType]) {
1138         NSLog(@"WARNING: Pasteboard contains no object of type "
1139                 "NSStringPboardType");
1140         return;
1141     }
1143     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
1144     BOOL openInCurrentWindow = [ud boolForKey:MMOpenInCurrentWindowKey];
1145     MMVimController *vc;
1147     if (openInCurrentWindow && (vc = [self topmostVimController])) {
1148         [vc sendMessage:AddNewTabMsgID data:nil];
1149         [vc dropString:[pboard stringForType:NSStringPboardType]];
1150     } else {
1151         // Save the text, open a new window, and paste the text when the next
1152         // window opens.  (If this is called several times in a row, then all
1153         // but the last call may be ignored.)
1154         if (openSelectionString) [openSelectionString release];
1155         openSelectionString = [[pboard stringForType:NSStringPboardType] copy];
1157         [self newWindow:self];
1158     }
1161 - (void)openFile:(NSPasteboard *)pboard userData:(NSString *)userData
1162            error:(NSString **)error
1164     if (![[pboard types] containsObject:NSStringPboardType]) {
1165         NSLog(@"WARNING: Pasteboard contains no object of type "
1166                 "NSStringPboardType");
1167         return;
1168     }
1170     // TODO: Parse multiple filenames and create array with names.
1171     NSString *string = [pboard stringForType:NSStringPboardType];
1172     string = [string stringByTrimmingCharactersInSet:
1173             [NSCharacterSet whitespaceAndNewlineCharacterSet]];
1174     string = [string stringByStandardizingPath];
1176     NSArray *filenames = [self filterFilesAndNotify:
1177             [NSArray arrayWithObject:string]];
1178     if ([filenames count] == 0)
1179         return;
1181     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
1182     BOOL openInCurrentWindow = [ud boolForKey:MMOpenInCurrentWindowKey];
1183     MMVimController *vc;
1185     if (openInCurrentWindow && (vc = [self topmostVimController])) {
1186         [vc dropFiles:filenames forceOpen:YES];
1187     } else {
1188         [self openFiles:filenames withArguments:nil];
1189     }
1192 - (void)newFileHere:(NSPasteboard *)pboard userData:(NSString *)userData
1193               error:(NSString **)error
1195     if (![[pboard types] containsObject:NSStringPboardType]) {
1196         NSLog(@"WARNING: Pasteboard contains no object of type "
1197               "NSStringPboardType");
1198         return;
1199     }
1201     NSString *path = [pboard stringForType:NSStringPboardType];
1203     BOOL dirIndicator;
1204     if (![[NSFileManager defaultManager] fileExistsAtPath:path
1205                                               isDirectory:&dirIndicator]) {
1206         NSLog(@"Invalid path. Cannot open new document at: %@", path);
1207         return;
1208     }
1210     if (!dirIndicator)
1211         path = [path stringByDeletingLastPathComponent];
1213     path = [path stringByReplacingOccurrencesOfString:@" " withString:@"\\ "];
1215     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
1216     BOOL openInCurrentWindow = [ud boolForKey:MMOpenInCurrentWindowKey];
1217     MMVimController *vc;
1219     if (openInCurrentWindow && (vc = [self topmostVimController])) {
1220         NSString *input = [NSString stringWithFormat:@"<C-\\><C-N>"
1221                 ":tabe|cd %@<CR>", path];
1222         [vc addVimInput:input];
1223     } else {
1224         NSString *input = [NSString stringWithFormat:@":cd %@", path];
1225         [self launchVimProcessWithArguments:[NSArray arrayWithObjects:
1226                                              @"-c", input, nil]];
1227     }
1230 @end // MMAppController (MMServices)
1235 @implementation MMAppController (Private)
1237 - (MMVimController *)topmostVimController
1239     // Find the topmost visible window which has an associated vim controller.
1240     NSEnumerator *e = [[NSApp orderedWindows] objectEnumerator];
1241     id window;
1242     while ((window = [e nextObject]) && [window isVisible]) {
1243         unsigned i, count = [vimControllers count];
1244         for (i = 0; i < count; ++i) {
1245             MMVimController *vc = [vimControllers objectAtIndex:i];
1246             if ([[[vc windowController] window] isEqual:window])
1247                 return vc;
1248         }
1249     }
1251     return nil;
1254 - (int)launchVimProcessWithArguments:(NSArray *)args
1256     int pid = -1;
1257     NSString *path = [[NSBundle mainBundle] pathForAuxiliaryExecutable:@"Vim"];
1259     if (!path) {
1260         NSLog(@"ERROR: Vim executable could not be found inside app bundle!");
1261         return -1;
1262     }
1264     NSArray *taskArgs = [NSArray arrayWithObjects:@"-g", @"-f", nil];
1265     if (args)
1266         taskArgs = [taskArgs arrayByAddingObjectsFromArray:args];
1268     BOOL useLoginShell = [[NSUserDefaults standardUserDefaults]
1269             boolForKey:MMLoginShellKey];
1270     if (useLoginShell) {
1271         // Run process with a login shell, roughly:
1272         //   echo "exec Vim -g -f args" | ARGV0=-`basename $SHELL` $SHELL [-l]
1273         pid = executeInLoginShell(path, taskArgs);
1274     } else {
1275         // Run process directly:
1276         //   Vim -g -f args
1277         NSTask *task = [NSTask launchedTaskWithLaunchPath:path
1278                                                 arguments:taskArgs];
1279         pid = task ? [task processIdentifier] : -1;
1280     }
1282     if (-1 != pid) {
1283         // NOTE: If the process has no arguments, then add a null argument to
1284         // the pidArguments dictionary.  This is later used to detect that a
1285         // process without arguments is being launched.
1286         if (!args)
1287             [pidArguments setObject:[NSNull null]
1288                              forKey:[NSNumber numberWithInt:pid]];
1289     } else {
1290         NSLog(@"WARNING: %s%@ failed (useLoginShell=%d)", _cmd, args,
1291                 useLoginShell);
1292     }
1294     return pid;
1297 - (NSArray *)filterFilesAndNotify:(NSArray *)filenames
1299     // Go trough 'filenames' array and make sure each file exists.  Present
1300     // warning dialog if some file was missing.
1302     NSString *firstMissingFile = nil;
1303     NSMutableArray *files = [NSMutableArray array];
1304     unsigned i, count = [filenames count];
1306     for (i = 0; i < count; ++i) {
1307         NSString *name = [filenames objectAtIndex:i];
1308         if ([[NSFileManager defaultManager] fileExistsAtPath:name]) {
1309             [files addObject:name];
1310         } else if (!firstMissingFile) {
1311             firstMissingFile = name;
1312         }
1313     }
1315     if (firstMissingFile) {
1316         NSAlert *alert = [[NSAlert alloc] init];
1317         [alert addButtonWithTitle:NSLocalizedString(@"OK",
1318                 @"Dialog button")];
1320         NSString *text;
1321         if ([files count] >= count-1) {
1322             [alert setMessageText:NSLocalizedString(@"File not found",
1323                     @"File not found dialog, title")];
1324             text = [NSString stringWithFormat:NSLocalizedString(
1325                     @"Could not open file with name %@.",
1326                     @"File not found dialog, text"), firstMissingFile];
1327         } else {
1328             [alert setMessageText:NSLocalizedString(@"Multiple files not found",
1329                     @"File not found dialog, title")];
1330             text = [NSString stringWithFormat:NSLocalizedString(
1331                     @"Could not open file with name %@, and %d other files.",
1332                     @"File not found dialog, text"),
1333                 firstMissingFile, count-[files count]-1];
1334         }
1336         [alert setInformativeText:text];
1337         [alert setAlertStyle:NSWarningAlertStyle];
1339         [alert runModal];
1340         [alert release];
1342         [NSApp replyToOpenOrPrint:NSApplicationDelegateReplyFailure];
1343     }
1345     return files;
1348 - (NSArray *)filterOpenFiles:(NSArray *)filenames
1349                openFilesDict:(NSDictionary **)openFiles
1351     // Filter out any files in the 'filenames' array that are open and return
1352     // all files that are not already open.  On return, the 'openFiles'
1353     // parameter (if non-nil) will point to a dictionary of open files, indexed
1354     // by Vim controller.
1356     NSMutableDictionary *dict = [NSMutableDictionary dictionary];
1357     NSMutableArray *files = [filenames mutableCopy];
1359     // TODO: Escape special characters in 'files'?
1360     NSString *expr = [NSString stringWithFormat:
1361             @"map([\"%@\"],\"bufloaded(v:val)\")",
1362             [files componentsJoinedByString:@"\",\""]];
1364     unsigned i, count = [vimControllers count];
1365     for (i = 0; i < count && [files count] > 0; ++i) {
1366         MMVimController *vc = [vimControllers objectAtIndex:i];
1368         // Query Vim for which files in the 'files' array are open.
1369         NSString *eval = [vc evaluateVimExpression:expr];
1370         if (!eval) continue;
1372         NSIndexSet *idxSet = [NSIndexSet indexSetWithVimList:eval];
1373         if ([idxSet count] > 0) {
1374             [dict setObject:[files objectsAtIndexes:idxSet]
1375                      forKey:[NSValue valueWithPointer:vc]];
1377             // Remove all the files that were open in this Vim process and
1378             // create a new expression to evaluate.
1379             [files removeObjectsAtIndexes:idxSet];
1380             expr = [NSString stringWithFormat:
1381                     @"map([\"%@\"],\"bufloaded(v:val)\")",
1382                     [files componentsJoinedByString:@"\",\""]];
1383         }
1384     }
1386     if (openFiles != nil)
1387         *openFiles = dict;
1389     return files;
1392 #if MM_HANDLE_XCODE_MOD_EVENT
1393 - (void)handleXcodeModEvent:(NSAppleEventDescriptor *)event
1394                  replyEvent:(NSAppleEventDescriptor *)reply
1396 #if 0
1397     // Xcode sends this event to query MacVim which open files have been
1398     // modified.
1399     NSLog(@"reply:%@", reply);
1400     NSLog(@"event:%@", event);
1402     NSEnumerator *e = [vimControllers objectEnumerator];
1403     id vc;
1404     while ((vc = [e nextObject])) {
1405         DescType type = [reply descriptorType];
1406         unsigned len = [[type data] length];
1407         NSMutableData *data = [NSMutableData data];
1409         [data appendBytes:&type length:sizeof(DescType)];
1410         [data appendBytes:&len length:sizeof(unsigned)];
1411         [data appendBytes:[reply data] length:len];
1413         [vc sendMessage:XcodeModMsgID data:data];
1414     }
1415 #endif
1417 #endif
1419 - (void)handleGetURLEvent:(NSAppleEventDescriptor *)event
1420                replyEvent:(NSAppleEventDescriptor *)reply
1422     NSString *urlString = [[event paramDescriptorForKeyword:keyDirectObject]
1423         stringValue];
1424     NSURL *url = [NSURL URLWithString:urlString];
1426     // We try to be compatible with TextMate's URL scheme here, as documented
1427     // at http://blog.macromates.com/2007/the-textmate-url-scheme/ . Currently,
1428     // this means that:
1429     //
1430     // The format is: mvim://open?<arguments> where arguments can be:
1431     //
1432     // * url â€” the actual file to open (i.e. a file://… URL), if you leave
1433     //         out this argument, the frontmost document is implied.
1434     // * line â€” line number to go to (one based).
1435     // * column â€” column number to go to (one based).
1436     //
1437     // Example: mvim://open?url=file:///etc/profile&line=20
1439     if ([[url host] isEqualToString:@"open"]) {
1440         NSMutableDictionary *dict = [NSMutableDictionary dictionary];
1442         // Parse query ("url=file://...&line=14") into a dictionary
1443         NSArray *queries = [[url query] componentsSeparatedByString:@"&"];
1444         NSEnumerator *enumerator = [queries objectEnumerator];
1445         NSString *param;
1446         while( param = [enumerator nextObject] ) {
1447             NSArray *arr = [param componentsSeparatedByString:@"="];
1448             if ([arr count] == 2) {
1449                 [dict setValue:[[arr lastObject]
1450                             stringByReplacingPercentEscapesUsingEncoding:
1451                                 NSUTF8StringEncoding]
1452                         forKey:[[arr objectAtIndex:0]
1453                             stringByReplacingPercentEscapesUsingEncoding:
1454                                 NSUTF8StringEncoding]];
1455             }
1456         }
1458         // Actually open the file.
1459         NSString *file = [dict objectForKey:@"url"];
1460         if (file != nil) {
1461             NSURL *fileUrl= [NSURL URLWithString:file];
1462             // TextMate only opens files that already exist.
1463             if ([fileUrl isFileURL]
1464                     && [[NSFileManager defaultManager] fileExistsAtPath:
1465                            [fileUrl path]]) {
1466                 // Strip 'file://' path, else application:openFiles: might think
1467                 // the file is not yet open.
1468                 NSArray *filenames = [NSArray arrayWithObject:[fileUrl path]];
1470                 // Look for the line and column options.
1471                 NSDictionary *args = nil;
1472                 NSString *line = [dict objectForKey:@"line"];
1473                 if (line) {
1474                     NSString *column = [dict objectForKey:@"column"];
1475                     if (column)
1476                         args = [NSDictionary dictionaryWithObjectsAndKeys:
1477                                 line, @"cursorLine",
1478                                 column, @"cursorColumn",
1479                                 nil];
1480                     else
1481                         args = [NSDictionary dictionaryWithObject:line
1482                                 forKey:@"cursorLine"];
1483                 }
1485                 [self openFiles:filenames withArguments:args];
1486             }
1487         }
1488     } else {
1489         NSAlert *alert = [[NSAlert alloc] init];
1490         [alert addButtonWithTitle:NSLocalizedString(@"OK",
1491             @"Dialog button")];
1493         [alert setMessageText:NSLocalizedString(@"Unknown URL Scheme",
1494             @"Unknown URL Scheme dialog, title")];
1495         [alert setInformativeText:[NSString stringWithFormat:NSLocalizedString(
1496             @"This version of MacVim does not support \"%@\""
1497             @" in its URL scheme.",
1498             @"Unknown URL Scheme dialog, text"),
1499             [url host]]];
1501         [alert setAlertStyle:NSWarningAlertStyle];
1502         [alert runModal];
1503         [alert release];
1504     }
1508 - (int)findLaunchingProcessWithoutArguments
1510     NSArray *keys = [pidArguments allKeysForObject:[NSNull null]];
1511     if ([keys count] > 0) {
1512         //NSLog(@"found launching process without arguments");
1513         return [[keys objectAtIndex:0] intValue];
1514     }
1516     return -1;
1519 - (MMVimController *)findUnusedEditor
1521     NSEnumerator *e = [vimControllers objectEnumerator];
1522     id vc;
1523     while ((vc = [e nextObject])) {
1524         if ([[[vc vimState] objectForKey:@"unusedEditor"] boolValue])
1525             return vc;
1526     }
1528     return nil;
1531 - (NSMutableDictionary *)extractArgumentsFromOdocEvent:
1532     (NSAppleEventDescriptor *)desc
1534     NSMutableDictionary *dict = [NSMutableDictionary dictionary];
1536     // 1. Extract ODB parameters (if any)
1537     NSAppleEventDescriptor *odbdesc = desc;
1538     if (![odbdesc paramDescriptorForKeyword:keyFileSender]) {
1539         // The ODB paramaters may hide inside the 'keyAEPropData' descriptor.
1540         odbdesc = [odbdesc paramDescriptorForKeyword:keyAEPropData];
1541         if (![odbdesc paramDescriptorForKeyword:keyFileSender])
1542             odbdesc = nil;
1543     }
1545     if (odbdesc) {
1546         NSAppleEventDescriptor *p =
1547                 [odbdesc paramDescriptorForKeyword:keyFileSender];
1548         if (p)
1549             [dict setObject:[NSNumber numberWithUnsignedInt:[p typeCodeValue]]
1550                      forKey:@"remoteID"];
1552         p = [odbdesc paramDescriptorForKeyword:keyFileCustomPath];
1553         if (p)
1554             [dict setObject:[p stringValue] forKey:@"remotePath"];
1556         p = [odbdesc paramDescriptorForKeyword:keyFileSenderToken];
1557         if (p) {
1558             [dict setObject:[NSNumber numberWithUnsignedLong:[p descriptorType]]
1559                      forKey:@"remoteTokenDescType"];
1560             [dict setObject:[p data] forKey:@"remoteTokenData"];
1561         }
1562     }
1564     // 2. Extract Xcode parameters (if any)
1565     NSAppleEventDescriptor *xcodedesc =
1566             [desc paramDescriptorForKeyword:keyAEPosition];
1567     if (xcodedesc) {
1568         NSRange range;
1569         MMSelectionRange *sr = (MMSelectionRange*)[[xcodedesc data] bytes];
1571         if (sr->lineNum < 0) {
1572             // Should select a range of lines.
1573             range.location = sr->startRange + 1;
1574             range.length = sr->endRange - sr->startRange + 1;
1575         } else {
1576             // Should only move cursor to a line.
1577             range.location = sr->lineNum + 1;
1578             range.length = 0;
1579         }
1581         [dict setObject:NSStringFromRange(range) forKey:@"selectionRange"];
1582     }
1584     // 3. Extract Spotlight search text (if any)
1585     NSAppleEventDescriptor *spotlightdesc = 
1586             [desc paramDescriptorForKeyword:keyAESearchText];
1587     if (spotlightdesc)
1588         [dict setObject:[spotlightdesc stringValue] forKey:@"searchText"];
1590     return dict;
1593 #ifdef MM_ENABLE_PLUGINS
1594 - (void)removePlugInMenu
1596     if ([plugInMenuItem menu])
1597         [[plugInMenuItem menu] removeItem:plugInMenuItem];
1600 - (void)addPlugInMenuToMenu:(NSMenu *)mainMenu
1602     NSMenu *windowsMenu = [mainMenu findWindowsMenu];
1604     if ([[plugInMenuItem submenu] numberOfItems] > 0) {
1605         int idx = windowsMenu ? [mainMenu indexOfItemWithSubmenu:windowsMenu]
1606                               : -1;
1607         if (idx > 0) {
1608             [mainMenu insertItem:plugInMenuItem atIndex:idx];
1609         } else {
1610             [mainMenu addItem:plugInMenuItem];
1611         }
1612     }
1614 #endif
1616 - (void)scheduleVimControllerPreloadAfterDelay:(NSTimeInterval)delay
1618     [self performSelector:@selector(preloadVimController:)
1619                withObject:nil
1620                afterDelay:delay];
1623 - (void)cancelVimControllerPreloadRequests
1625     [NSObject cancelPreviousPerformRequestsWithTarget:self
1626             selector:@selector(preloadVimController:)
1627               object:nil];
1630 - (void)preloadVimController:(id)sender
1632     // We only allow preloading of one Vim process at a time (to avoid hogging
1633     // CPU), so schedule another preload in a little while if necessary.
1634     if (-1 != preloadPid) {
1635         [self scheduleVimControllerPreloadAfterDelay:2];
1636         return;
1637     }
1639     if ([cachedVimControllers count] >= [self maxPreloadCacheSize])
1640         return;
1642     preloadPid = [self launchVimProcessWithArguments:
1643             [NSArray arrayWithObject:@"--mmwaitforack"]];
1646 - (int)maxPreloadCacheSize
1648     // The maximum number of Vim processes to keep in the cache can be
1649     // controlled via the user default "MMPreloadCacheSize".
1650     int maxCacheSize = [[NSUserDefaults standardUserDefaults]
1651             integerForKey:MMPreloadCacheSizeKey];
1652     if (maxCacheSize < 0) maxCacheSize = 0;
1653     else if (maxCacheSize > 10) maxCacheSize = 10;
1655     return maxCacheSize;
1658 - (MMVimController *)takeVimControllerFromCache
1660     // NOTE: After calling this message the backend corresponding to the
1661     // returned vim controller must be sent an acknowledgeConnection message,
1662     // else the vim process will be stuck.
1663     //
1664     // This method may return nil even though the cache might be non-empty; the
1665     // caller should handle this by starting a new Vim process.
1667     int i, count = [cachedVimControllers count];
1668     if (0 == count) return nil;
1670     // Locate the first Vim controller with up-to-date rc-files sourced.
1671     NSDate *rcDate = [self rcFilesModificationDate];
1672     for (i = 0; i < count; ++i) {
1673         MMVimController *vc = [cachedVimControllers objectAtIndex:i];
1674         NSDate *date = [vc creationDate];
1675         if ([date compare:rcDate] != NSOrderedAscending)
1676             break;
1677     }
1679     if (i > 0) {
1680         // Clear out cache entries whose vimrc/gvimrc files were sourced before
1681         // the latest modification date for those files.  This ensures that the
1682         // latest rc-files are always sourced for new windows.
1683         [self clearPreloadCacheWithCount:i];
1684     }
1686     if ([cachedVimControllers count] == 0) {
1687         [self scheduleVimControllerPreloadAfterDelay:2.0];
1688         return nil;
1689     }
1691     MMVimController *vc = [cachedVimControllers objectAtIndex:0];
1692     [vimControllers addObject:vc];
1693     [cachedVimControllers removeObjectAtIndex:0];
1694     [vc setIsPreloading:NO];
1696     // If the Vim process has finished loading then the window will displayed
1697     // now, otherwise it will be displayed when the OpenWindowMsgID message is
1698     // received.
1699     [[vc windowController] showWindow];
1701     // Since we've taken one controller from the cache we take the opportunity
1702     // to preload another.
1703     [self scheduleVimControllerPreloadAfterDelay:1];
1705     return vc;
1708 - (void)clearPreloadCacheWithCount:(int)count
1710     // Remove the 'count' first entries in the preload cache.  It is assumed
1711     // that objects are added/removed from the cache in a FIFO manner so that
1712     // this effectively clears the 'count' oldest entries.
1713     // If 'count' is negative, then the entire cache is cleared.
1715     if ([cachedVimControllers count] == 0 || count == 0)
1716         return;
1718     if (count < 0)
1719         count = [cachedVimControllers count];
1721     // Make sure the preloaded Vim processes get killed or they'll just hang
1722     // around being useless until MacVim is terminated.
1723     NSEnumerator *e = [cachedVimControllers objectEnumerator];
1724     MMVimController *vc;
1725     int n = count;
1726     while ((vc = [e nextObject]) && n-- > 0) {
1727         [[NSNotificationCenter defaultCenter] removeObserver:vc];
1728         [vc sendMessage:TerminateNowMsgID data:nil];
1730         // Since the preloaded processes were killed "prematurely" we have to
1731         // manually tell them to cleanup (it is not enough to simply release
1732         // them since deallocation and cleanup are separated).
1733         [vc cleanup];
1734     }
1736     n = count;
1737     while (n-- > 0 && [cachedVimControllers count] > 0)
1738         [cachedVimControllers removeObjectAtIndex:0];
1741 - (void)rebuildPreloadCache
1743     if ([self maxPreloadCacheSize] > 0) {
1744         [self clearPreloadCacheWithCount:-1];
1745         [self cancelVimControllerPreloadRequests];
1746         [self scheduleVimControllerPreloadAfterDelay:1.0];
1747     }
1750 - (NSDate *)rcFilesModificationDate
1752     // Check modification dates for ~/.vimrc and ~/.gvimrc and return the
1753     // latest modification date.  If ~/.vimrc does not exist, check ~/_vimrc
1754     // and similarly for gvimrc.
1755     // Returns distantPath if no rc files were found.
1757     NSDate *date = [NSDate distantPast];
1758     NSFileManager *fm = [NSFileManager defaultManager];
1760     NSString *path = [@"~/.vimrc" stringByExpandingTildeInPath];
1761     NSDictionary *attr = [fm fileAttributesAtPath:path traverseLink:YES];
1762     if (!attr) {
1763         path = [@"~/_vimrc" stringByExpandingTildeInPath];
1764         attr = [fm fileAttributesAtPath:path traverseLink:YES];
1765     }
1766     NSDate *modDate = [attr objectForKey:NSFileModificationDate];
1767     if (modDate)
1768         date = modDate;
1770     path = [@"~/.gvimrc" stringByExpandingTildeInPath];
1771     attr = [fm fileAttributesAtPath:path traverseLink:YES];
1772     if (!attr) {
1773         path = [@"~/_gvimrc" stringByExpandingTildeInPath];
1774         attr = [fm fileAttributesAtPath:path traverseLink:YES];
1775     }
1776     modDate = [attr objectForKey:NSFileModificationDate];
1777     if (modDate)
1778         date = [date laterDate:modDate];
1780     return date;
1783 - (BOOL)openVimControllerWithArguments:(NSDictionary *)arguments
1785     MMVimController *vc = [self findUnusedEditor];
1786     if (vc) {
1787         // Open files in an already open window.
1788         [[[vc windowController] window] makeKeyAndOrderFront:self];
1789         [vc passArguments:arguments];
1790     } else if ((vc = [self takeVimControllerFromCache])) {
1791         // Open files in a new window using a cached vim controller.  This
1792         // requires virtually no loading time so the new window will pop up
1793         // instantaneously.
1794         [vc passArguments:arguments];
1795         [[vc backendProxy] acknowledgeConnection];
1796     } else {
1797         // Open files in a launching Vim process or start a new process.  This
1798         // may take 1-2 seconds so there will be a visible delay before the
1799         // window appears on screen.
1800         int pid = [self findLaunchingProcessWithoutArguments];
1801         if (-1 == pid) {
1802             pid = [self launchVimProcessWithArguments:nil];
1803             if (-1 == pid)
1804                 return NO;
1805         }
1807         // TODO: If the Vim process fails to start, or if it changes PID,
1808         // then the memory allocated for these parameters will leak.
1809         // Ensure that this cannot happen or somehow detect it.
1811         if ([arguments count] > 0)
1812             [pidArguments setObject:arguments
1813                              forKey:[NSNumber numberWithInt:pid]];
1814     }
1816     return YES;
1819 - (void)activateWhenNextWindowOpens
1821     shouldActivateWhenNextWindowOpens = YES;
1824 - (void)startWatchingVimDir
1826     //NSLog(@"%s", _cmd);
1827 #if (MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_4)
1828     if (fsEventStream)
1829         return;
1830     if (NULL == FSEventStreamStart)
1831         return; // FSEvent functions are weakly linked
1833     NSString *path = [@"~/.vim" stringByExpandingTildeInPath];
1834     NSArray *pathsToWatch = [NSArray arrayWithObject:path];
1836     fsEventStream = FSEventStreamCreate(NULL, &fsEventCallback, NULL,
1837             (CFArrayRef)pathsToWatch, kFSEventStreamEventIdSinceNow,
1838             MMEventStreamLatency, kFSEventStreamCreateFlagNone);
1840     FSEventStreamScheduleWithRunLoop(fsEventStream,
1841             [[NSRunLoop currentRunLoop] getCFRunLoop],
1842             kCFRunLoopDefaultMode);
1844     FSEventStreamStart(fsEventStream);
1845     //NSLog(@"Started FS event stream");
1846 #endif
1849 - (void)stopWatchingVimDir
1851     //NSLog(@"%s", _cmd);
1852 #if (MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_4)
1853     if (NULL == FSEventStreamStop)
1854         return; // FSEvent functions are weakly linked
1856     if (fsEventStream) {
1857         FSEventStreamStop(fsEventStream);
1858         FSEventStreamInvalidate(fsEventStream);
1859         FSEventStreamRelease(fsEventStream);
1860         fsEventStream = NULL;
1861         //NSLog(@"Stopped FS event stream");
1862     }
1863 #endif
1867 - (void)handleFSEvent
1869     //NSLog(@"%s", _cmd);
1870     [self clearPreloadCacheWithCount:-1];
1872     // Several FS events may arrive in quick succession so make sure to cancel
1873     // any previous preload requests before making a new one.
1874     [self cancelVimControllerPreloadRequests];
1875     [self scheduleVimControllerPreloadAfterDelay:0.5];
1878 @end // MMAppController (Private)
1883     static int
1884 executeInLoginShell(NSString *path, NSArray *args)
1886     // Start a login shell and execute the command 'path' with arguments 'args'
1887     // in the shell.  This ensures that user environment variables are set even
1888     // when MacVim was started from the Finder.
1890     int pid = -1;
1891     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
1893     // Determine which shell to use to execute the command.  The user
1894     // may decide which shell to use by setting a user default or the
1895     // $SHELL environment variable.
1896     NSString *shell = [ud stringForKey:MMLoginShellCommandKey];
1897     if (!shell || [shell length] == 0)
1898         shell = [[[NSProcessInfo processInfo] environment]
1899             objectForKey:@"SHELL"];
1900     if (!shell)
1901         shell = @"/bin/bash";
1903     //NSLog(@"shell = %@", shell);
1905     // Bash needs the '-l' flag to launch a login shell.  The user may add
1906     // flags by setting a user default.
1907     NSString *shellArgument = [ud stringForKey:MMLoginShellArgumentKey];
1908     if (!shellArgument || [shellArgument length] == 0) {
1909         if ([[shell lastPathComponent] isEqual:@"bash"])
1910             shellArgument = @"-l";
1911         else
1912             shellArgument = nil;
1913     }
1915     //NSLog(@"shellArgument = %@", shellArgument);
1917     // Build input string to pipe to the login shell.
1918     NSMutableString *input = [NSMutableString stringWithFormat:
1919             @"exec \"%@\"", path];
1920     if (args) {
1921         // Append all arguments, making sure they are properly quoted, even
1922         // when they contain single quotes.
1923         NSEnumerator *e = [args objectEnumerator];
1924         id obj;
1926         while ((obj = [e nextObject])) {
1927             NSMutableString *arg = [NSMutableString stringWithString:obj];
1928             [arg replaceOccurrencesOfString:@"'" withString:@"'\"'\"'"
1929                                     options:NSLiteralSearch
1930                                       range:NSMakeRange(0, [arg length])];
1931             [input appendFormat:@" '%@'", arg];
1932         }
1933     }
1935     // Build the argument vector used to start the login shell.
1936     NSString *shellArg0 = [NSString stringWithFormat:@"-%@",
1937              [shell lastPathComponent]];
1938     char *shellArgv[3] = { (char *)[shellArg0 UTF8String], NULL, NULL };
1939     if (shellArgument)
1940         shellArgv[1] = (char *)[shellArgument UTF8String];
1942     // Get the C string representation of the shell path before the fork since
1943     // we must not call Foundation functions after a fork.
1944     const char *shellPath = [shell fileSystemRepresentation];
1946     // Fork and execute the process.
1947     int ds[2];
1948     if (pipe(ds)) return -1;
1950     pid = fork();
1951     if (pid == -1) {
1952         return -1;
1953     } else if (pid == 0) {
1954         // Child process
1955         if (close(ds[1]) == -1) exit(255);
1956         if (dup2(ds[0], 0) == -1) exit(255);
1958         execv(shellPath, shellArgv);
1960         // Never reached unless execv fails
1961         exit(255);
1962     } else {
1963         // Parent process
1964         if (close(ds[0]) == -1) return -1;
1966         // Send input to execute to the child process
1967         [input appendString:@"\n"];
1968         int bytes = [input lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
1970         if (write(ds[1], [input UTF8String], bytes) != bytes) return -1;
1971         if (close(ds[1]) == -1) return -1;
1972     }
1974     return pid;