Revert "Modifier key sends Esc" related commits
[MacVim.git] / src / MacVim / MMAppController.m
blob3bea328aed422dcf97090712b967693b826abe15
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         nil];
190     [[NSUserDefaults standardUserDefaults] registerDefaults:dict];
192     NSArray *types = [NSArray arrayWithObject:NSStringPboardType];
193     [NSApp registerServicesMenuSendTypes:types returnTypes:types];
195     // NOTE: Set the current directory to user's home directory, otherwise it
196     // will default to the root directory.  (This matters since new Vim
197     // processes inherit MacVim's environment variables.)
198     [[NSFileManager defaultManager] changeCurrentDirectoryPath:
199             NSHomeDirectory()];
202 - (id)init
204     if (!(self = [super init])) return nil;
206     fontContainerRef = loadFonts();
208     vimControllers = [NSMutableArray new];
209     cachedVimControllers = [NSMutableArray new];
210     preloadPid = -1;
211     pidArguments = [NSMutableDictionary new];
213 #ifdef MM_ENABLE_PLUGINS
214     NSString *plugInTitle = NSLocalizedString(@"Plug-In",
215                                               @"Plug-In menu title");
216     plugInMenuItem = [[NSMenuItem alloc] initWithTitle:plugInTitle
217                                                 action:NULL
218                                          keyEquivalent:@""];
219     NSMenu *submenu = [[NSMenu alloc] initWithTitle:plugInTitle];
220     [plugInMenuItem setSubmenu:submenu];
221     [submenu release];
222 #endif
224     // NOTE: Do not use the default connection since the Logitech Control
225     // Center (LCC) input manager steals and this would cause MacVim to
226     // never open any windows.  (This is a bug in LCC but since they are
227     // unlikely to fix it, we graciously give them the default connection.)
228     connection = [[NSConnection alloc] initWithReceivePort:[NSPort port]
229                                                   sendPort:nil];
230     [connection setRootObject:self];
231     [connection setRequestTimeout:MMRequestTimeout];
232     [connection setReplyTimeout:MMReplyTimeout];
234     // NOTE: When the user is resizing the window the AppKit puts the run
235     // loop in event tracking mode.  Unless the connection listens to
236     // request in this mode, live resizing won't work.
237     [connection addRequestMode:NSEventTrackingRunLoopMode];
239     // NOTE!  If the name of the connection changes here it must also be
240     // updated in MMBackend.m.
241     NSString *name = [NSString stringWithFormat:@"%@-connection",
242              [[NSBundle mainBundle] bundlePath]];
243     //NSLog(@"Registering connection with name '%@'", name);
244     if (![connection registerName:name]) {
245         NSLog(@"FATAL ERROR: Failed to register connection with name '%@'",
246                 name);
247         [connection release];  connection = nil;
248     }
250     return self;
253 - (void)dealloc
255     //NSLog(@"MMAppController dealloc");
257     [connection release];  connection = nil;
258     [pidArguments release];  pidArguments = nil;
259     [vimControllers release];  vimControllers = nil;
260     [cachedVimControllers release];  cachedVimControllers = nil;
261     [openSelectionString release];  openSelectionString = nil;
262     [recentFilesMenuItem release];  recentFilesMenuItem = nil;
263     [defaultMainMenu release];  defaultMainMenu = nil;
264 #ifdef MM_ENABLE_PLUGINS
265     [plugInMenuItem release];  plugInMenuItem = nil;
266 #endif
267     [appMenuItemTemplate release];  appMenuItemTemplate = nil;
269     [super dealloc];
272 - (void)applicationWillFinishLaunching:(NSNotification *)notification
274     // Remember the default menu so that it can be restored if the user closes
275     // all editor windows.
276     defaultMainMenu = [[NSApp mainMenu] retain];
278     // Store a copy of the default app menu so we can use this as a template
279     // for all other menus.  We make a copy here because the "Services" menu
280     // will not yet have been populated at this time.  If we don't we get
281     // problems trying to set key equivalents later on because they might clash
282     // with items on the "Services" menu.
283     appMenuItemTemplate = [defaultMainMenu itemAtIndex:0];
284     appMenuItemTemplate = [appMenuItemTemplate copy];
286     // Set up the "Open Recent" menu. See
287     //   http://lapcatsoftware.com/blog/2007/07/10/
288     //     working-without-a-nib-part-5-open-recent-menu/
289     // and
290     //   http://www.cocoabuilder.com/archive/message/cocoa/2007/8/15/187793
291     // for more information.
292     //
293     // The menu itself is created in MainMenu.nib but we still seem to have to
294     // hack around a bit to get it to work.  (This has to be done in
295     // applicationWillFinishLaunching at the latest, otherwise it doesn't
296     // work.)
297     NSMenu *fileMenu = [defaultMainMenu findFileMenu];
298     if (fileMenu) {
299         int idx = [fileMenu indexOfItemWithAction:@selector(fileOpen:)];
300         if (idx >= 0 && idx+1 < [fileMenu numberOfItems])
302         recentFilesMenuItem = [fileMenu itemWithTitle:@"Open Recent"];
303         [[recentFilesMenuItem submenu] performSelector:@selector(_setMenuName:)
304                                         withObject:@"NSRecentDocumentsMenu"];
306         // Note: The "Recent Files" menu must be moved around since there is no
307         // -[NSApp setRecentFilesMenu:] method.  We keep a reference to it to
308         // facilitate this move (see setMainMenu: below).
309         [recentFilesMenuItem retain];
310     }
312 #if MM_HANDLE_XCODE_MOD_EVENT
313     [[NSAppleEventManager sharedAppleEventManager]
314             setEventHandler:self
315                 andSelector:@selector(handleXcodeModEvent:replyEvent:)
316               forEventClass:'KAHL'
317                  andEventID:'MOD '];
318 #endif
320     // Register 'mvim://' URL handler
321     [[NSAppleEventManager sharedAppleEventManager]
322             setEventHandler:self
323                 andSelector:@selector(handleGetURLEvent:replyEvent:)
324               forEventClass:kInternetEventClass
325                  andEventID:kAEGetURL];
328 - (void)applicationDidFinishLaunching:(NSNotification *)notification
330     [NSApp setServicesProvider:self];
331 #ifdef MM_ENABLE_PLUGINS
332     [[MMPlugInManager sharedManager] loadAllPlugIns];
333 #endif
335     if ([self maxPreloadCacheSize] > 0) {
336         [self scheduleVimControllerPreloadAfterDelay:2];
337         [self startWatchingVimDir];
338     }
341 - (BOOL)applicationShouldOpenUntitledFile:(NSApplication *)sender
343     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
344     NSAppleEventManager *aem = [NSAppleEventManager sharedAppleEventManager];
345     NSAppleEventDescriptor *desc = [aem currentAppleEvent];
347     // The user default MMUntitledWindow can be set to control whether an
348     // untitled window should open on 'Open' and 'Reopen' events.
349     int untitledWindowFlag = [ud integerForKey:MMUntitledWindowKey];
350     if ([desc eventID] == kAEOpenApplication
351             && (untitledWindowFlag & MMUntitledWindowOnOpen) == 0)
352         return NO;
353     else if ([desc eventID] == kAEReopenApplication
354             && (untitledWindowFlag & MMUntitledWindowOnReopen) == 0)
355         return NO;
357     // When a process is started from the command line, the 'Open' event will
358     // contain a parameter to surpress the opening of an untitled window.
359     desc = [desc paramDescriptorForKeyword:keyAEPropData];
360     desc = [desc paramDescriptorForKeyword:keyMMUntitledWindow];
361     if (desc && ![desc booleanValue])
362         return NO;
364     // Never open an untitled window if there is at least one open window or if
365     // there are processes that are currently launching.
366     if ([vimControllers count] > 0 || [pidArguments count] > 0)
367         return NO;
369     // NOTE!  This way it possible to start the app with the command-line
370     // argument '-nowindow yes' and no window will be opened by default.
371     return ![ud boolForKey:MMNoWindowKey];
374 - (BOOL)applicationOpenUntitledFile:(NSApplication *)sender
376     [self newWindow:self];
377     return YES;
380 - (void)application:(NSApplication *)sender openFiles:(NSArray *)filenames
382     // Extract ODB/Xcode/Spotlight parameters from the current Apple event,
383     // sort the filenames, and then let openFiles:withArguments: do the heavy
384     // lifting.
386     if (!(filenames && [filenames count] > 0))
387         return;
389     // Sort filenames since the Finder doesn't take care in preserving the
390     // order in which files are selected anyway (and "sorted" is more
391     // predictable than "random").
392     if ([filenames count] > 1)
393         filenames = [filenames sortedArrayUsingSelector:
394                 @selector(localizedCompare:)];
396     // Extract ODB/Xcode/Spotlight parameters from the current Apple event
397     NSMutableDictionary *arguments = [self extractArgumentsFromOdocEvent:
398             [[NSAppleEventManager sharedAppleEventManager] currentAppleEvent]];
400     if ([self openFiles:filenames withArguments:arguments]) {
401         [NSApp replyToOpenOrPrint:NSApplicationDelegateReplySuccess];
402     } else {
403         // TODO: Notify user of failure?
404         [NSApp replyToOpenOrPrint:NSApplicationDelegateReplyFailure];
405     }
408 - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender
410     return (MMTerminateWhenLastWindowClosed ==
411             [[NSUserDefaults standardUserDefaults]
412                 integerForKey:MMLastWindowClosedBehaviorKey]);
415 - (NSApplicationTerminateReply)applicationShouldTerminate:
416     (NSApplication *)sender
418     // TODO: Follow Apple's guidelines for 'Graceful Application Termination'
419     // (in particular, allow user to review changes and save).
420     int reply = NSTerminateNow;
421     BOOL modifiedBuffers = NO;
423     // Go through windows, checking for modified buffers.  (Each Vim process
424     // tells MacVim when any buffer has been modified and MacVim sets the
425     // 'documentEdited' flag of the window correspondingly.)
426     NSEnumerator *e = [[NSApp windows] objectEnumerator];
427     id window;
428     while ((window = [e nextObject])) {
429         if ([window isDocumentEdited]) {
430             modifiedBuffers = YES;
431             break;
432         }
433     }
435     if (modifiedBuffers) {
436         NSAlert *alert = [[NSAlert alloc] init];
437         [alert setAlertStyle:NSWarningAlertStyle];
438         [alert addButtonWithTitle:NSLocalizedString(@"Quit",
439                 @"Dialog button")];
440         [alert addButtonWithTitle:NSLocalizedString(@"Cancel",
441                 @"Dialog button")];
442         [alert setMessageText:NSLocalizedString(@"Quit without saving?",
443                 @"Quit dialog with changed buffers, title")];
444         [alert setInformativeText:NSLocalizedString(
445                 @"There are modified buffers, "
446                 "if you quit now all changes will be lost.  Quit anyway?",
447                 @"Quit dialog with changed buffers, text")];
449         if ([alert runModal] != NSAlertFirstButtonReturn)
450             reply = NSTerminateCancel;
452         [alert release];
453     } else {
454         // No unmodified buffers, but give a warning if there are multiple
455         // windows and/or tabs open.
456         int numWindows = [vimControllers count];
457         int numTabs = 0;
459         // Count the number of open tabs
460         e = [vimControllers objectEnumerator];
461         id vc;
462         while ((vc = [e nextObject])) {
463             NSString *eval = [vc evaluateVimExpression:@"tabpagenr('$')"];
464             if (eval) {
465                 int count = [eval intValue];
466                 if (count > 0 && count < INT_MAX)
467                     numTabs += count;
468             }
469         }
471         if (numWindows > 1 || numTabs > 1) {
472             NSAlert *alert = [[NSAlert alloc] init];
473             [alert setAlertStyle:NSWarningAlertStyle];
474             [alert addButtonWithTitle:NSLocalizedString(@"Quit",
475                     @"Dialog button")];
476             [alert addButtonWithTitle:NSLocalizedString(@"Cancel",
477                     @"Dialog button")];
478             [alert setMessageText:NSLocalizedString(
479                     @"Are you sure you want to quit MacVim?",
480                     @"Quit dialog with no changed buffers, title")];
482             NSString *info = nil;
483             if (numWindows > 1) {
484                 if (numTabs > numWindows)
485                     info = [NSString stringWithFormat:NSLocalizedString(
486                             @"There are %d windows open in MacVim, with a "
487                             "total of %d tabs. Do you want to quit anyway?",
488                             @"Quit dialog with no changed buffers, text"),
489                          numWindows, numTabs];
490                 else
491                     info = [NSString stringWithFormat:NSLocalizedString(
492                             @"There are %d windows open in MacVim. "
493                             "Do you want to quit anyway?",
494                             @"Quit dialog with no changed buffers, text"),
495                         numWindows];
497             } else {
498                 info = [NSString stringWithFormat:NSLocalizedString(
499                         @"There are %d tabs open in MacVim. "
500                         "Do you want to quit anyway?",
501                         @"Quit dialog with no changed buffers, text"), 
502                      numTabs];
503             }
505             [alert setInformativeText:info];
507             if ([alert runModal] != NSAlertFirstButtonReturn)
508                 reply = NSTerminateCancel;
510             [alert release];
511         }
512     }
515     // Tell all Vim processes to terminate now (otherwise they'll leave swap
516     // files behind).
517     if (NSTerminateNow == reply) {
518         e = [vimControllers objectEnumerator];
519         id vc;
520         while ((vc = [e nextObject]))
521             [vc sendMessage:TerminateNowMsgID data:nil];
523         e = [cachedVimControllers objectEnumerator];
524         while ((vc = [e nextObject]))
525             [vc sendMessage:TerminateNowMsgID data:nil];
527         // Give Vim processes a chance to terminate before MacVim.  If they
528         // haven't terminated by the time applicationWillTerminate: is sent,
529         // they may be forced to quit (see below).
530         usleep(MMTerminationSleepPeriod);
531     }
533     return reply;
536 - (void)applicationWillTerminate:(NSNotification *)notification
538     [self stopWatchingVimDir];
540 #ifdef MM_ENABLE_PLUGINS
541     [[MMPlugInManager sharedManager] unloadAllPlugIns];
542 #endif
544 #if MM_HANDLE_XCODE_MOD_EVENT
545     [[NSAppleEventManager sharedAppleEventManager]
546             removeEventHandlerForEventClass:'KAHL'
547                                  andEventID:'MOD '];
548 #endif
550     // This will invalidate all connections (since they were spawned from this
551     // connection).
552     [connection invalidate];
554     // Send a SIGINT to all running Vim processes, so that they are sure to
555     // receive the connectionDidDie: notification (a process has to be checking
556     // the run-loop for this to happen).
557     unsigned i, count = [vimControllers count];
558     for (i = 0; i < count; ++i) {
559         MMVimController *controller = [vimControllers objectAtIndex:i];
560         int pid = [controller pid];
561         if (-1 != pid)
562             kill(pid, SIGINT);
563     }
565     if (fontContainerRef) {
566         ATSFontDeactivate(fontContainerRef, NULL, kATSOptionFlagsDefault);
567         fontContainerRef = 0;
568     }
570     [NSApp setDelegate:nil];
573 + (MMAppController *)sharedInstance
575     // Note: The app controller is a singleton which is instantiated in
576     // MainMenu.nib where it is also connected as the delegate of NSApp.
577     id delegate = [NSApp delegate];
578     return [delegate isKindOfClass:self] ? (MMAppController*)delegate : nil;
581 - (NSMenu *)defaultMainMenu
583     return defaultMainMenu;
586 - (NSMenuItem *)appMenuItemTemplate
588     return appMenuItemTemplate;
591 - (void)removeVimController:(id)controller
593     int idx = [vimControllers indexOfObject:controller];
594     if (NSNotFound == idx)
595         return;
597     [controller cleanup];
599     [vimControllers removeObjectAtIndex:idx];
601     if (![vimControllers count]) {
602         // The last editor window just closed so restore the main menu back to
603         // its default state (which is defined in MainMenu.nib).
604         [self setMainMenu:defaultMainMenu];
606         BOOL hide = (MMHideWhenLastWindowClosed ==
607                     [[NSUserDefaults standardUserDefaults]
608                         integerForKey:MMLastWindowClosedBehaviorKey]);
609         if (hide)
610             [NSApp hide:self];
611     }
614 - (void)windowControllerWillOpen:(MMWindowController *)windowController
616     NSPoint topLeft = NSZeroPoint;
617     NSWindow *topWin = [[[self topmostVimController] windowController] window];
618     NSWindow *win = [windowController window];
620     if (!win) return;
622     // If there is a window belonging to a Vim process, cascade from it,
623     // otherwise use the autosaved window position (if any).
624     if (topWin) {
625         NSRect frame = [topWin frame];
626         topLeft = NSMakePoint(frame.origin.x, NSMaxY(frame));
627     } else {
628         NSString *topLeftString = [[NSUserDefaults standardUserDefaults]
629             stringForKey:MMTopLeftPointKey];
630         if (topLeftString)
631             topLeft = NSPointFromString(topLeftString);
632     }
634     if (!NSEqualPoints(topLeft, NSZeroPoint)) {
635         NSPoint oldTopLeft = topLeft;
636         if (topWin)
637             topLeft = [win cascadeTopLeftFromPoint:topLeft];
639         [win setFrameTopLeftPoint:topLeft];
641         NSPoint screenOrigin = [[win screen] frame].origin;
642         if ([win frame].origin.y < screenOrigin.y) {
643             // Try to avoid shifting the new window downwards if it means that
644             // the bottom of the window will be off the screen.  E.g. if the
645             // user has set windows to open maximized in the vertical direction
646             // then the new window will cascade horizontally only.
647             topLeft.y = oldTopLeft.y;
648             [win setFrameTopLeftPoint:topLeft];
649         }
651         if ([win frame].origin.y < screenOrigin.y) {
652             // Move the window to the top of the screen if the bottom of the
653             // window is still obscured.
654             topLeft.y = NSMaxY([[win screen] frame]);
655             [win setFrameTopLeftPoint:topLeft];
656         }
657     }
659     if (1 == [vimControllers count]) {
660         // The first window autosaves its position.  (The autosaving
661         // features of Cocoa are not used because we need more control over
662         // what is autosaved and when it is restored.)
663         [windowController setWindowAutosaveKey:MMTopLeftPointKey];
664     }
666     if (openSelectionString) {
667         // TODO: Pass this as a parameter instead!  Get rid of
668         // 'openSelectionString' etc.
669         //
670         // There is some text to paste into this window as a result of the
671         // services menu "Open selection ..." being used.
672         [[windowController vimController] dropString:openSelectionString];
673         [openSelectionString release];
674         openSelectionString = nil;
675     }
677     if (shouldActivateWhenNextWindowOpens) {
678         [NSApp activateIgnoringOtherApps:YES];
679         shouldActivateWhenNextWindowOpens = NO;
680     }
683 - (void)setMainMenu:(NSMenu *)mainMenu
685     if ([NSApp mainMenu] == mainMenu) return;
687     // If the new menu has a "Recent Files" dummy item, then swap the real item
688     // for the dummy.  We are forced to do this since Cocoa initializes the
689     // "Recent Files" menu and there is no way to simply point Cocoa to a new
690     // item each time the menus are swapped.
691     NSMenu *fileMenu = [mainMenu findFileMenu];
692     if (recentFilesMenuItem && fileMenu) {
693         int dummyIdx =
694                 [fileMenu indexOfItemWithAction:@selector(recentFilesDummy:)];
695         if (dummyIdx >= 0) {
696             NSMenuItem *dummyItem = [[fileMenu itemAtIndex:dummyIdx] retain];
697             [fileMenu removeItemAtIndex:dummyIdx];
699             NSMenu *recentFilesParentMenu = [recentFilesMenuItem menu];
700             int idx = [recentFilesParentMenu indexOfItem:recentFilesMenuItem];
701             if (idx >= 0) {
702                 [[recentFilesMenuItem retain] autorelease];
703                 [recentFilesParentMenu removeItemAtIndex:idx];
704                 [recentFilesParentMenu insertItem:dummyItem atIndex:idx];
705             }
707             [fileMenu insertItem:recentFilesMenuItem atIndex:dummyIdx];
708             [dummyItem release];
709         }
710     }
712     // Now set the new menu.  Notice that we keep one menu for each editor
713     // window since each editor can have its own set of menus.  When swapping
714     // menus we have to tell Cocoa where the new "MacVim", "Windows", and
715     // "Services" menu are.
716     [NSApp setMainMenu:mainMenu];
718     // Setting the "MacVim" (or "Application") menu ensures that it is typeset
719     // in boldface.  (The setAppleMenu: method used to be public but is now
720     // private so this will have to be considered a bit of a hack!)
721     NSMenu *appMenu = [mainMenu findApplicationMenu];
722     [NSApp performSelector:@selector(setAppleMenu:) withObject:appMenu];
724     NSMenu *servicesMenu = [mainMenu findServicesMenu];
725     [NSApp setServicesMenu:servicesMenu];
727     NSMenu *windowsMenu = [mainMenu findWindowsMenu];
728     if (windowsMenu) {
729         // Cocoa isn't clever enough to get rid of items it has added to the
730         // "Windows" menu so we have to do it ourselves otherwise there will be
731         // multiple menu items for each window in the "Windows" menu.
732         //   This code assumes that the only items Cocoa add are ones which
733         // send off the action makeKeyAndOrderFront:.  (Cocoa will not add
734         // another separator item if the last item on the "Windows" menu
735         // already is a separator, so we needen't worry about separators.)
736         int i, count = [windowsMenu numberOfItems];
737         for (i = count-1; i >= 0; --i) {
738             NSMenuItem *item = [windowsMenu itemAtIndex:i];
739             if ([item action] == @selector(makeKeyAndOrderFront:))
740                 [windowsMenu removeItem:item];
741         }
742     }
743     [NSApp setWindowsMenu:windowsMenu];
745 #ifdef MM_ENABLE_PLUGINS
746     // Move plugin menu from old to new main menu.
747     [self removePlugInMenu];
748     [self addPlugInMenuToMenu:mainMenu];
749 #endif
752 - (NSArray *)filterOpenFiles:(NSArray *)filenames
754     return [self filterOpenFiles:filenames openFilesDict:nil];
757 - (BOOL)openFiles:(NSArray *)filenames withArguments:(NSDictionary *)args
759     // Opening files works like this:
760     //  a) filter out any already open files
761     //  b) open any remaining files
762     //
763     // A file is opened in an untitled window if there is one (it may be
764     // currently launching, or it may already be visible), otherwise a new
765     // window is opened.
766     //
767     // Each launching Vim process has a dictionary of arguments that are passed
768     // to the process when in checks in (via connectBackend:pid:).  The
769     // arguments for each launching process can be looked up by its PID (in the
770     // pidArguments dictionary).
772     NSMutableDictionary *arguments = (args ? [[args mutableCopy] autorelease]
773                                            : [NSMutableDictionary dictionary]);
775     //
776     // a) Filter out any already open files
777     //
778     NSString *firstFile = [filenames objectAtIndex:0];
779     MMVimController *firstController = nil;
780     NSDictionary *openFilesDict = nil;
781     filenames = [self filterOpenFiles:filenames openFilesDict:&openFilesDict];
783     // Pass arguments to vim controllers that had files open.
784     id key;
785     NSEnumerator *e = [openFilesDict keyEnumerator];
787     // (Indicate that we do not wish to open any files at the moment.)
788     [arguments setObject:[NSNumber numberWithBool:YES] forKey:@"dontOpen"];
790     while ((key = [e nextObject])) {
791         NSArray *files = [openFilesDict objectForKey:key];
792         [arguments setObject:files forKey:@"filenames"];
794         MMVimController *vc = [key pointerValue];
795         [vc passArguments:arguments];
797         // If this controller holds the first file, then remember it for later.
798         if ([files containsObject:firstFile])
799             firstController = vc;
800     }
802     if ([filenames count] == 0) {
803         // Raise the window containing the first file that was already open,
804         // and make sure that the tab containing that file is selected.  Only
805         // do this when there are no more files to open, otherwise sometimes
806         // the window with 'firstFile' will be raised, other times it might be
807         // the window that will open with the files in the 'filenames' array.
808         firstFile = [firstFile stringByEscapingSpecialFilenameCharacters];
809         NSString *input = [NSString stringWithFormat:@"<C-\\><C-N>"
810                 ":let oldswb=&swb|let &swb=\"useopen,usetab\"|"
811                 "tab sb %@|let &swb=oldswb|unl oldswb|"
812                 "cal foreground()|redr|f<CR>", firstFile];
814         [firstController addVimInput:input];
816         return YES;
817     }
819     // Add filenames to "Recent Files" menu, unless they are being edited
820     // remotely (using ODB).
821     if ([arguments objectForKey:@"remoteID"] == nil) {
822         [[NSDocumentController sharedDocumentController]
823                 noteNewRecentFilePaths:filenames];
824     }
826     //
827     // b) Open any remaining files
828     //
829     MMVimController *vc;
830     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
831     BOOL openInCurrentWindow = [ud boolForKey:MMOpenInCurrentWindowKey];
833     // The meaning of "layout" is defined by the WIN_* defines in main.c.
834     int layout = [ud integerForKey:MMOpenLayoutKey];
835     BOOL splitVert = [ud boolForKey:MMVerticalSplitKey];
836     if (splitVert && MMLayoutHorizontalSplit == layout)
837         layout = MMLayoutVerticalSplit;
838     if (layout < 0 || (layout > MMLayoutTabs && openInCurrentWindow))
839         layout = MMLayoutTabs;
841     [arguments setObject:[NSNumber numberWithInt:layout] forKey:@"layout"];
842     [arguments setObject:filenames forKey:@"filenames"];
843     // (Indicate that files should be opened from now on.)
844     [arguments setObject:[NSNumber numberWithBool:NO] forKey:@"dontOpen"];
846     if (openInCurrentWindow && (vc = [self topmostVimController])) {
847         // Open files in an already open window.
848         [[[vc windowController] window] makeKeyAndOrderFront:self];
849         [vc passArguments:arguments];
850         return YES;
851     }
853     BOOL openOk = YES;
854     int numFiles = [filenames count];
855     if (MMLayoutWindows == layout && numFiles > 1) {
856         // Open one file at a time in a new window, but don't open too many at
857         // once (at most cap+1 windows will open).  If the user has increased
858         // the preload cache size we'll take that as a hint that more windows
859         // should be able to open at once.
860         int cap = [self maxPreloadCacheSize] - 1;
861         if (cap < 4) cap = 4;
862         if (cap > numFiles) cap = numFiles;
864         int i;
865         for (i = 0; i < cap; ++i) {
866             NSArray *a = [NSArray arrayWithObject:[filenames objectAtIndex:i]];
867             [arguments setObject:a forKey:@"filenames"];
869             // NOTE: We have to copy the args since we'll mutate them in the
870             // next loop and the below call may retain the arguments while
871             // waiting for a process to start.
872             NSDictionary *args = [[arguments copy] autorelease];
874             openOk = [self openVimControllerWithArguments:args];
875             if (!openOk) break;
876         }
878         // Open remaining files in tabs in a new window.
879         if (openOk && numFiles > cap) {
880             NSRange range = { i, numFiles-cap };
881             NSArray *a = [filenames subarrayWithRange:range];
882             [arguments setObject:a forKey:@"filenames"];
883             [arguments setObject:[NSNumber numberWithInt:MMLayoutTabs]
884                           forKey:@"layout"];
886             openOk = [self openVimControllerWithArguments:arguments];
887         }
888     } else {
889         // Open all files at once.
890         openOk = [self openVimControllerWithArguments:arguments];
891     }
893     return openOk;
896 #ifdef MM_ENABLE_PLUGINS
897 - (void)addItemToPlugInMenu:(NSMenuItem *)item
899     NSMenu *menu = [plugInMenuItem submenu];
900     [menu addItem:item];
901     if ([menu numberOfItems] == 1)
902         [self addPlugInMenuToMenu:[NSApp mainMenu]];
905 - (void)removeItemFromPlugInMenu:(NSMenuItem *)item
907     NSMenu *menu = [plugInMenuItem submenu];
908     [menu removeItem:item];
909     if ([menu numberOfItems] == 0)
910         [self removePlugInMenu];
912 #endif
914 - (IBAction)newWindow:(id)sender
916     // A cached controller requires no loading times and results in the new
917     // window popping up instantaneously.  If the cache is empty it may take
918     // 1-2 seconds to start a new Vim process.
919     MMVimController *vc = [self takeVimControllerFromCache];
920     if (vc) {
921         [[vc backendProxy] acknowledgeConnection];
922     } else {
923         [self launchVimProcessWithArguments:nil];
924     }
927 - (IBAction)newWindowAndActivate:(id)sender
929     [self activateWhenNextWindowOpens];
930     [self newWindow:sender];
933 - (IBAction)fileOpen:(id)sender
935     NSString *dir = nil;
936     BOOL trackPwd = [[NSUserDefaults standardUserDefaults]
937             boolForKey:MMDialogsTrackPwdKey];
938     if (trackPwd) {
939         MMVimController *vc = [self keyVimController];
940         if (vc) dir = [[vc vimState] objectForKey:@"pwd"];
941     }
943     NSOpenPanel *panel = [NSOpenPanel openPanel];
944     [panel setAllowsMultipleSelection:YES];
945     [panel setAccessoryView:openPanelAccessoryView()];
947     int result = [panel runModalForDirectory:dir file:nil types:nil];
948     if (NSOKButton == result)
949         [self application:NSApp openFiles:[panel filenames]];
952 - (IBAction)selectNextWindow:(id)sender
954     unsigned i, count = [vimControllers count];
955     if (!count) return;
957     NSWindow *keyWindow = [NSApp keyWindow];
958     for (i = 0; i < count; ++i) {
959         MMVimController *vc = [vimControllers objectAtIndex:i];
960         if ([[[vc windowController] window] isEqual:keyWindow])
961             break;
962     }
964     if (i < count) {
965         if (++i >= count)
966             i = 0;
967         MMVimController *vc = [vimControllers objectAtIndex:i];
968         [[vc windowController] showWindow:self];
969     }
972 - (IBAction)selectPreviousWindow:(id)sender
974     unsigned i, count = [vimControllers count];
975     if (!count) return;
977     NSWindow *keyWindow = [NSApp keyWindow];
978     for (i = 0; i < count; ++i) {
979         MMVimController *vc = [vimControllers objectAtIndex:i];
980         if ([[[vc windowController] window] isEqual:keyWindow])
981             break;
982     }
984     if (i < count) {
985         if (i > 0) {
986             --i;
987         } else {
988             i = count - 1;
989         }
990         MMVimController *vc = [vimControllers objectAtIndex:i];
991         [[vc windowController] showWindow:self];
992     }
995 - (IBAction)orderFrontPreferencePanel:(id)sender
997     [[MMPreferenceController sharedPrefsWindowController] showWindow:self];
1000 - (IBAction)openWebsite:(id)sender
1002     [[NSWorkspace sharedWorkspace] openURL:
1003             [NSURL URLWithString:MMWebsiteString]];
1006 - (IBAction)showVimHelp:(id)sender
1008     // Open a new window with the help window maximized.
1009     [self launchVimProcessWithArguments:[NSArray arrayWithObjects:
1010             @"-c", @":h gui_mac", @"-c", @":res", nil]];
1013 - (IBAction)zoomAll:(id)sender
1015     [NSApp makeWindowsPerform:@selector(performZoom:) inOrder:YES];
1018 - (IBAction)atsuiButtonClicked:(id)sender
1020     // This action is called when the user clicks the "use ATSUI renderer"
1021     // button in the advanced preferences pane.
1022     [self rebuildPreloadCache];
1025 - (IBAction)loginShellButtonClicked:(id)sender
1027     // This action is called when the user clicks the "use login shell" button
1028     // in the advanced preferences pane.
1029     [self rebuildPreloadCache];
1032 - (IBAction)quickstartButtonClicked:(id)sender
1034     if ([self maxPreloadCacheSize] > 0) {
1035         [self scheduleVimControllerPreloadAfterDelay:1.0];
1036         [self startWatchingVimDir];
1037     } else {
1038         [self cancelVimControllerPreloadRequests];
1039         [self clearPreloadCacheWithCount:-1];
1040         [self stopWatchingVimDir];
1041     }
1044 - (byref id <MMFrontendProtocol>)
1045     connectBackend:(byref in id <MMBackendProtocol>)backend
1046                pid:(int)pid
1048     //NSLog(@"Connect backend (pid=%d)", pid);
1049     NSNumber *pidKey = [NSNumber numberWithInt:pid];
1050     MMVimController *vc = nil;
1052     @try {
1053         [(NSDistantObject*)backend
1054                 setProtocolForProxy:@protocol(MMBackendProtocol)];
1056         vc = [[[MMVimController alloc] initWithBackend:backend pid:pid]
1057                 autorelease];
1059         if (preloadPid == pid) {
1060             // This backend was preloaded, so add it to the cache and schedule
1061             // another vim process to be preloaded.
1062             preloadPid = -1;
1063             [vc setIsPreloading:YES];
1064             [cachedVimControllers addObject:vc];
1065             [self scheduleVimControllerPreloadAfterDelay:1];
1067             return vc;
1068         }
1070         [vimControllers addObject:vc];
1072         id args = [pidArguments objectForKey:pidKey];
1073         if (args && [NSNull null] != args)
1074             [vc passArguments:args];
1076         // HACK!  MacVim does not get activated if it is launched from the
1077         // terminal, so we forcibly activate here unless it is an untitled
1078         // window opening.  Untitled windows are treated differently, else
1079         // MacVim would steal the focus if another app was activated while the
1080         // untitled window was loading.
1081         if (!args || args != [NSNull null])
1082             [NSApp activateIgnoringOtherApps:YES];
1084         if (args)
1085             [pidArguments removeObjectForKey:pidKey];
1087         return vc;
1088     }
1090     @catch (NSException *e) {
1091         NSLog(@"Exception caught in %s: \"%@\"", _cmd, e);
1093         if (vc)
1094             [vimControllers removeObject:vc];
1096         [pidArguments removeObjectForKey:pidKey];
1097     }
1099     return nil;
1102 - (NSArray *)serverList
1104     NSMutableArray *array = [NSMutableArray array];
1106     unsigned i, count = [vimControllers count];
1107     for (i = 0; i < count; ++i) {
1108         MMVimController *controller = [vimControllers objectAtIndex:i];
1109         if ([controller serverName])
1110             [array addObject:[controller serverName]];
1111     }
1113     return array;
1116 - (MMVimController *)keyVimController
1118     NSWindow *keyWindow = [NSApp keyWindow];
1119     if (keyWindow) {
1120         unsigned i, count = [vimControllers count];
1121         for (i = 0; i < count; ++i) {
1122             MMVimController *vc = [vimControllers objectAtIndex:i];
1123             if ([[[vc windowController] window] isEqual:keyWindow])
1124                 return vc;
1125         }
1126     }
1128     return nil;
1131 @end // MMAppController
1136 @implementation MMAppController (MMServices)
1138 - (void)openSelection:(NSPasteboard *)pboard userData:(NSString *)userData
1139                 error:(NSString **)error
1141     if (![[pboard types] containsObject:NSStringPboardType]) {
1142         NSLog(@"WARNING: Pasteboard contains no object of type "
1143                 "NSStringPboardType");
1144         return;
1145     }
1147     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
1148     BOOL openInCurrentWindow = [ud boolForKey:MMOpenInCurrentWindowKey];
1149     MMVimController *vc;
1151     if (openInCurrentWindow && (vc = [self topmostVimController])) {
1152         [vc sendMessage:AddNewTabMsgID data:nil];
1153         [vc dropString:[pboard stringForType:NSStringPboardType]];
1154     } else {
1155         // Save the text, open a new window, and paste the text when the next
1156         // window opens.  (If this is called several times in a row, then all
1157         // but the last call may be ignored.)
1158         if (openSelectionString) [openSelectionString release];
1159         openSelectionString = [[pboard stringForType:NSStringPboardType] copy];
1161         [self newWindow:self];
1162     }
1165 - (void)openFile:(NSPasteboard *)pboard userData:(NSString *)userData
1166            error:(NSString **)error
1168     if (![[pboard types] containsObject:NSStringPboardType]) {
1169         NSLog(@"WARNING: Pasteboard contains no object of type "
1170                 "NSStringPboardType");
1171         return;
1172     }
1174     // TODO: Parse multiple filenames and create array with names.
1175     NSString *string = [pboard stringForType:NSStringPboardType];
1176     string = [string stringByTrimmingCharactersInSet:
1177             [NSCharacterSet whitespaceAndNewlineCharacterSet]];
1178     string = [string stringByStandardizingPath];
1180     NSArray *filenames = [self filterFilesAndNotify:
1181             [NSArray arrayWithObject:string]];
1182     if ([filenames count] == 0)
1183         return;
1185     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
1186     BOOL openInCurrentWindow = [ud boolForKey:MMOpenInCurrentWindowKey];
1187     MMVimController *vc;
1189     if (openInCurrentWindow && (vc = [self topmostVimController])) {
1190         [vc dropFiles:filenames forceOpen:YES];
1191     } else {
1192         [self openFiles:filenames withArguments:nil];
1193     }
1196 - (void)newFileHere:(NSPasteboard *)pboard userData:(NSString *)userData
1197               error:(NSString **)error
1199     if (![[pboard types] containsObject:NSStringPboardType]) {
1200         NSLog(@"WARNING: Pasteboard contains no object of type "
1201               "NSStringPboardType");
1202         return;
1203     }
1205     NSString *path = [pboard stringForType:NSStringPboardType];
1207     BOOL dirIndicator;
1208     if (![[NSFileManager defaultManager] fileExistsAtPath:path
1209                                               isDirectory:&dirIndicator]) {
1210         NSLog(@"Invalid path. Cannot open new document at: %@", path);
1211         return;
1212     }
1214     if (!dirIndicator)
1215         path = [path stringByDeletingLastPathComponent];
1217     path = [path stringByReplacingOccurrencesOfString:@" " withString:@"\\ "];
1219     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
1220     BOOL openInCurrentWindow = [ud boolForKey:MMOpenInCurrentWindowKey];
1221     MMVimController *vc;
1223     if (openInCurrentWindow && (vc = [self topmostVimController])) {
1224         NSString *input = [NSString stringWithFormat:@"<C-\\><C-N>"
1225                 ":tabe|cd %@<CR>", path];
1226         [vc addVimInput:input];
1227     } else {
1228         NSString *input = [NSString stringWithFormat:@":cd %@", path];
1229         [self launchVimProcessWithArguments:[NSArray arrayWithObjects:
1230                                              @"-c", input, nil]];
1231     }
1234 @end // MMAppController (MMServices)
1239 @implementation MMAppController (Private)
1241 - (MMVimController *)topmostVimController
1243     // Find the topmost visible window which has an associated vim controller.
1244     NSEnumerator *e = [[NSApp orderedWindows] objectEnumerator];
1245     id window;
1246     while ((window = [e nextObject]) && [window isVisible]) {
1247         unsigned i, count = [vimControllers count];
1248         for (i = 0; i < count; ++i) {
1249             MMVimController *vc = [vimControllers objectAtIndex:i];
1250             if ([[[vc windowController] window] isEqual:window])
1251                 return vc;
1252         }
1253     }
1255     return nil;
1258 - (int)launchVimProcessWithArguments:(NSArray *)args
1260     int pid = -1;
1261     NSString *path = [[NSBundle mainBundle] pathForAuxiliaryExecutable:@"Vim"];
1263     if (!path) {
1264         NSLog(@"ERROR: Vim executable could not be found inside app bundle!");
1265         return -1;
1266     }
1268     NSArray *taskArgs = [NSArray arrayWithObjects:@"-g", @"-f", nil];
1269     if (args)
1270         taskArgs = [taskArgs arrayByAddingObjectsFromArray:args];
1272     BOOL useLoginShell = [[NSUserDefaults standardUserDefaults]
1273             boolForKey:MMLoginShellKey];
1274     if (useLoginShell) {
1275         // Run process with a login shell, roughly:
1276         //   echo "exec Vim -g -f args" | ARGV0=-`basename $SHELL` $SHELL [-l]
1277         pid = executeInLoginShell(path, taskArgs);
1278     } else {
1279         // Run process directly:
1280         //   Vim -g -f args
1281         NSTask *task = [NSTask launchedTaskWithLaunchPath:path
1282                                                 arguments:taskArgs];
1283         pid = task ? [task processIdentifier] : -1;
1284     }
1286     if (-1 != pid) {
1287         // NOTE: If the process has no arguments, then add a null argument to
1288         // the pidArguments dictionary.  This is later used to detect that a
1289         // process without arguments is being launched.
1290         if (!args)
1291             [pidArguments setObject:[NSNull null]
1292                              forKey:[NSNumber numberWithInt:pid]];
1293     } else {
1294         NSLog(@"WARNING: %s%@ failed (useLoginShell=%d)", _cmd, args,
1295                 useLoginShell);
1296     }
1298     return pid;
1301 - (NSArray *)filterFilesAndNotify:(NSArray *)filenames
1303     // Go trough 'filenames' array and make sure each file exists.  Present
1304     // warning dialog if some file was missing.
1306     NSString *firstMissingFile = nil;
1307     NSMutableArray *files = [NSMutableArray array];
1308     unsigned i, count = [filenames count];
1310     for (i = 0; i < count; ++i) {
1311         NSString *name = [filenames objectAtIndex:i];
1312         if ([[NSFileManager defaultManager] fileExistsAtPath:name]) {
1313             [files addObject:name];
1314         } else if (!firstMissingFile) {
1315             firstMissingFile = name;
1316         }
1317     }
1319     if (firstMissingFile) {
1320         NSAlert *alert = [[NSAlert alloc] init];
1321         [alert addButtonWithTitle:NSLocalizedString(@"OK",
1322                 @"Dialog button")];
1324         NSString *text;
1325         if ([files count] >= count-1) {
1326             [alert setMessageText:NSLocalizedString(@"File not found",
1327                     @"File not found dialog, title")];
1328             text = [NSString stringWithFormat:NSLocalizedString(
1329                     @"Could not open file with name %@.",
1330                     @"File not found dialog, text"), firstMissingFile];
1331         } else {
1332             [alert setMessageText:NSLocalizedString(@"Multiple files not found",
1333                     @"File not found dialog, title")];
1334             text = [NSString stringWithFormat:NSLocalizedString(
1335                     @"Could not open file with name %@, and %d other files.",
1336                     @"File not found dialog, text"),
1337                 firstMissingFile, count-[files count]-1];
1338         }
1340         [alert setInformativeText:text];
1341         [alert setAlertStyle:NSWarningAlertStyle];
1343         [alert runModal];
1344         [alert release];
1346         [NSApp replyToOpenOrPrint:NSApplicationDelegateReplyFailure];
1347     }
1349     return files;
1352 - (NSArray *)filterOpenFiles:(NSArray *)filenames
1353                openFilesDict:(NSDictionary **)openFiles
1355     // Filter out any files in the 'filenames' array that are open and return
1356     // all files that are not already open.  On return, the 'openFiles'
1357     // parameter (if non-nil) will point to a dictionary of open files, indexed
1358     // by Vim controller.
1360     NSMutableDictionary *dict = [NSMutableDictionary dictionary];
1361     NSMutableArray *files = [filenames mutableCopy];
1363     // TODO: Escape special characters in 'files'?
1364     NSString *expr = [NSString stringWithFormat:
1365             @"map([\"%@\"],\"bufloaded(v:val)\")",
1366             [files componentsJoinedByString:@"\",\""]];
1368     unsigned i, count = [vimControllers count];
1369     for (i = 0; i < count && [files count] > 0; ++i) {
1370         MMVimController *vc = [vimControllers objectAtIndex:i];
1372         // Query Vim for which files in the 'files' array are open.
1373         NSString *eval = [vc evaluateVimExpression:expr];
1374         if (!eval) continue;
1376         NSIndexSet *idxSet = [NSIndexSet indexSetWithVimList:eval];
1377         if ([idxSet count] > 0) {
1378             [dict setObject:[files objectsAtIndexes:idxSet]
1379                      forKey:[NSValue valueWithPointer:vc]];
1381             // Remove all the files that were open in this Vim process and
1382             // create a new expression to evaluate.
1383             [files removeObjectsAtIndexes:idxSet];
1384             expr = [NSString stringWithFormat:
1385                     @"map([\"%@\"],\"bufloaded(v:val)\")",
1386                     [files componentsJoinedByString:@"\",\""]];
1387         }
1388     }
1390     if (openFiles != nil)
1391         *openFiles = dict;
1393     return files;
1396 #if MM_HANDLE_XCODE_MOD_EVENT
1397 - (void)handleXcodeModEvent:(NSAppleEventDescriptor *)event
1398                  replyEvent:(NSAppleEventDescriptor *)reply
1400 #if 0
1401     // Xcode sends this event to query MacVim which open files have been
1402     // modified.
1403     NSLog(@"reply:%@", reply);
1404     NSLog(@"event:%@", event);
1406     NSEnumerator *e = [vimControllers objectEnumerator];
1407     id vc;
1408     while ((vc = [e nextObject])) {
1409         DescType type = [reply descriptorType];
1410         unsigned len = [[type data] length];
1411         NSMutableData *data = [NSMutableData data];
1413         [data appendBytes:&type length:sizeof(DescType)];
1414         [data appendBytes:&len length:sizeof(unsigned)];
1415         [data appendBytes:[reply data] length:len];
1417         [vc sendMessage:XcodeModMsgID data:data];
1418     }
1419 #endif
1421 #endif
1423 - (void)handleGetURLEvent:(NSAppleEventDescriptor *)event
1424                replyEvent:(NSAppleEventDescriptor *)reply
1426     NSString *urlString = [[event paramDescriptorForKeyword:keyDirectObject]
1427         stringValue];
1428     NSURL *url = [NSURL URLWithString:urlString];
1430     // We try to be compatible with TextMate's URL scheme here, as documented
1431     // at http://blog.macromates.com/2007/the-textmate-url-scheme/ . Currently,
1432     // this means that:
1433     //
1434     // The format is: mvim://open?<arguments> where arguments can be:
1435     //
1436     // * url â€” the actual file to open (i.e. a file://… URL), if you leave
1437     //         out this argument, the frontmost document is implied.
1438     // * line â€” line number to go to (one based).
1439     // * column â€” column number to go to (one based).
1440     //
1441     // Example: mvim://open?url=file:///etc/profile&line=20
1443     if ([[url host] isEqualToString:@"open"]) {
1444         NSMutableDictionary *dict = [NSMutableDictionary dictionary];
1446         // Parse query ("url=file://...&line=14") into a dictionary
1447         NSArray *queries = [[url query] componentsSeparatedByString:@"&"];
1448         NSEnumerator *enumerator = [queries objectEnumerator];
1449         NSString *param;
1450         while( param = [enumerator nextObject] ) {
1451             NSArray *arr = [param componentsSeparatedByString:@"="];
1452             if ([arr count] == 2) {
1453                 [dict setValue:[[arr lastObject]
1454                             stringByReplacingPercentEscapesUsingEncoding:
1455                                 NSUTF8StringEncoding]
1456                         forKey:[[arr objectAtIndex:0]
1457                             stringByReplacingPercentEscapesUsingEncoding:
1458                                 NSUTF8StringEncoding]];
1459             }
1460         }
1462         // Actually open the file.
1463         NSString *file = [dict objectForKey:@"url"];
1464         if (file != nil) {
1465             NSURL *fileUrl= [NSURL URLWithString:file];
1466             // TextMate only opens files that already exist.
1467             if ([fileUrl isFileURL]
1468                     && [[NSFileManager defaultManager] fileExistsAtPath:
1469                            [fileUrl path]]) {
1470                 // Strip 'file://' path, else application:openFiles: might think
1471                 // the file is not yet open.
1472                 NSArray *filenames = [NSArray arrayWithObject:[fileUrl path]];
1474                 // Look for the line and column options.
1475                 NSDictionary *args = nil;
1476                 NSString *line = [dict objectForKey:@"line"];
1477                 if (line) {
1478                     NSString *column = [dict objectForKey:@"column"];
1479                     if (column)
1480                         args = [NSDictionary dictionaryWithObjectsAndKeys:
1481                                 line, @"cursorLine",
1482                                 column, @"cursorColumn",
1483                                 nil];
1484                     else
1485                         args = [NSDictionary dictionaryWithObject:line
1486                                 forKey:@"cursorLine"];
1487                 }
1489                 [self openFiles:filenames withArguments:args];
1490             }
1491         }
1492     } else {
1493         NSAlert *alert = [[NSAlert alloc] init];
1494         [alert addButtonWithTitle:NSLocalizedString(@"OK",
1495             @"Dialog button")];
1497         [alert setMessageText:NSLocalizedString(@"Unknown URL Scheme",
1498             @"Unknown URL Scheme dialog, title")];
1499         [alert setInformativeText:[NSString stringWithFormat:NSLocalizedString(
1500             @"This version of MacVim does not support \"%@\""
1501             @" in its URL scheme.",
1502             @"Unknown URL Scheme dialog, text"),
1503             [url host]]];
1505         [alert setAlertStyle:NSWarningAlertStyle];
1506         [alert runModal];
1507         [alert release];
1508     }
1512 - (int)findLaunchingProcessWithoutArguments
1514     NSArray *keys = [pidArguments allKeysForObject:[NSNull null]];
1515     if ([keys count] > 0) {
1516         //NSLog(@"found launching process without arguments");
1517         return [[keys objectAtIndex:0] intValue];
1518     }
1520     return -1;
1523 - (MMVimController *)findUnusedEditor
1525     NSEnumerator *e = [vimControllers objectEnumerator];
1526     id vc;
1527     while ((vc = [e nextObject])) {
1528         if ([[[vc vimState] objectForKey:@"unusedEditor"] boolValue])
1529             return vc;
1530     }
1532     return nil;
1535 - (NSMutableDictionary *)extractArgumentsFromOdocEvent:
1536     (NSAppleEventDescriptor *)desc
1538     NSMutableDictionary *dict = [NSMutableDictionary dictionary];
1540     // 1. Extract ODB parameters (if any)
1541     NSAppleEventDescriptor *odbdesc = desc;
1542     if (![odbdesc paramDescriptorForKeyword:keyFileSender]) {
1543         // The ODB paramaters may hide inside the 'keyAEPropData' descriptor.
1544         odbdesc = [odbdesc paramDescriptorForKeyword:keyAEPropData];
1545         if (![odbdesc paramDescriptorForKeyword:keyFileSender])
1546             odbdesc = nil;
1547     }
1549     if (odbdesc) {
1550         NSAppleEventDescriptor *p =
1551                 [odbdesc paramDescriptorForKeyword:keyFileSender];
1552         if (p)
1553             [dict setObject:[NSNumber numberWithUnsignedInt:[p typeCodeValue]]
1554                      forKey:@"remoteID"];
1556         p = [odbdesc paramDescriptorForKeyword:keyFileCustomPath];
1557         if (p)
1558             [dict setObject:[p stringValue] forKey:@"remotePath"];
1560         p = [odbdesc paramDescriptorForKeyword:keyFileSenderToken];
1561         if (p) {
1562             [dict setObject:[NSNumber numberWithUnsignedLong:[p descriptorType]]
1563                      forKey:@"remoteTokenDescType"];
1564             [dict setObject:[p data] forKey:@"remoteTokenData"];
1565         }
1566     }
1568     // 2. Extract Xcode parameters (if any)
1569     NSAppleEventDescriptor *xcodedesc =
1570             [desc paramDescriptorForKeyword:keyAEPosition];
1571     if (xcodedesc) {
1572         NSRange range;
1573         MMSelectionRange *sr = (MMSelectionRange*)[[xcodedesc data] bytes];
1575         if (sr->lineNum < 0) {
1576             // Should select a range of lines.
1577             range.location = sr->startRange + 1;
1578             range.length = sr->endRange - sr->startRange + 1;
1579         } else {
1580             // Should only move cursor to a line.
1581             range.location = sr->lineNum + 1;
1582             range.length = 0;
1583         }
1585         [dict setObject:NSStringFromRange(range) forKey:@"selectionRange"];
1586     }
1588     // 3. Extract Spotlight search text (if any)
1589     NSAppleEventDescriptor *spotlightdesc = 
1590             [desc paramDescriptorForKeyword:keyAESearchText];
1591     if (spotlightdesc)
1592         [dict setObject:[spotlightdesc stringValue] forKey:@"searchText"];
1594     return dict;
1597 #ifdef MM_ENABLE_PLUGINS
1598 - (void)removePlugInMenu
1600     if ([plugInMenuItem menu])
1601         [[plugInMenuItem menu] removeItem:plugInMenuItem];
1604 - (void)addPlugInMenuToMenu:(NSMenu *)mainMenu
1606     NSMenu *windowsMenu = [mainMenu findWindowsMenu];
1608     if ([[plugInMenuItem submenu] numberOfItems] > 0) {
1609         int idx = windowsMenu ? [mainMenu indexOfItemWithSubmenu:windowsMenu]
1610                               : -1;
1611         if (idx > 0) {
1612             [mainMenu insertItem:plugInMenuItem atIndex:idx];
1613         } else {
1614             [mainMenu addItem:plugInMenuItem];
1615         }
1616     }
1618 #endif
1620 - (void)scheduleVimControllerPreloadAfterDelay:(NSTimeInterval)delay
1622     [self performSelector:@selector(preloadVimController:)
1623                withObject:nil
1624                afterDelay:delay];
1627 - (void)cancelVimControllerPreloadRequests
1629     [NSObject cancelPreviousPerformRequestsWithTarget:self
1630             selector:@selector(preloadVimController:)
1631               object:nil];
1634 - (void)preloadVimController:(id)sender
1636     // We only allow preloading of one Vim process at a time (to avoid hogging
1637     // CPU), so schedule another preload in a little while if necessary.
1638     if (-1 != preloadPid) {
1639         [self scheduleVimControllerPreloadAfterDelay:2];
1640         return;
1641     }
1643     if ([cachedVimControllers count] >= [self maxPreloadCacheSize])
1644         return;
1646     preloadPid = [self launchVimProcessWithArguments:
1647             [NSArray arrayWithObject:@"--mmwaitforack"]];
1650 - (int)maxPreloadCacheSize
1652     // The maximum number of Vim processes to keep in the cache can be
1653     // controlled via the user default "MMPreloadCacheSize".
1654     int maxCacheSize = [[NSUserDefaults standardUserDefaults]
1655             integerForKey:MMPreloadCacheSizeKey];
1656     if (maxCacheSize < 0) maxCacheSize = 0;
1657     else if (maxCacheSize > 10) maxCacheSize = 10;
1659     return maxCacheSize;
1662 - (MMVimController *)takeVimControllerFromCache
1664     // NOTE: After calling this message the backend corresponding to the
1665     // returned vim controller must be sent an acknowledgeConnection message,
1666     // else the vim process will be stuck.
1667     //
1668     // This method may return nil even though the cache might be non-empty; the
1669     // caller should handle this by starting a new Vim process.
1671     int i, count = [cachedVimControllers count];
1672     if (0 == count) return nil;
1674     // Locate the first Vim controller with up-to-date rc-files sourced.
1675     NSDate *rcDate = [self rcFilesModificationDate];
1676     for (i = 0; i < count; ++i) {
1677         MMVimController *vc = [cachedVimControllers objectAtIndex:i];
1678         NSDate *date = [vc creationDate];
1679         if ([date compare:rcDate] != NSOrderedAscending)
1680             break;
1681     }
1683     if (i > 0) {
1684         // Clear out cache entries whose vimrc/gvimrc files were sourced before
1685         // the latest modification date for those files.  This ensures that the
1686         // latest rc-files are always sourced for new windows.
1687         [self clearPreloadCacheWithCount:i];
1688     }
1690     if ([cachedVimControllers count] == 0) {
1691         [self scheduleVimControllerPreloadAfterDelay:2.0];
1692         return nil;
1693     }
1695     MMVimController *vc = [cachedVimControllers objectAtIndex:0];
1696     [vimControllers addObject:vc];
1697     [cachedVimControllers removeObjectAtIndex:0];
1698     [vc setIsPreloading:NO];
1700     // If the Vim process has finished loading then the window will displayed
1701     // now, otherwise it will be displayed when the OpenWindowMsgID message is
1702     // received.
1703     [[vc windowController] showWindow];
1705     // Since we've taken one controller from the cache we take the opportunity
1706     // to preload another.
1707     [self scheduleVimControllerPreloadAfterDelay:1];
1709     return vc;
1712 - (void)clearPreloadCacheWithCount:(int)count
1714     // Remove the 'count' first entries in the preload cache.  It is assumed
1715     // that objects are added/removed from the cache in a FIFO manner so that
1716     // this effectively clears the 'count' oldest entries.
1717     // If 'count' is negative, then the entire cache is cleared.
1719     if ([cachedVimControllers count] == 0 || count == 0)
1720         return;
1722     if (count < 0)
1723         count = [cachedVimControllers count];
1725     // Make sure the preloaded Vim processes get killed or they'll just hang
1726     // around being useless until MacVim is terminated.
1727     NSEnumerator *e = [cachedVimControllers objectEnumerator];
1728     MMVimController *vc;
1729     int n = count;
1730     while ((vc = [e nextObject]) && n-- > 0) {
1731         [[NSNotificationCenter defaultCenter] removeObserver:vc];
1732         [vc sendMessage:TerminateNowMsgID data:nil];
1734         // Since the preloaded processes were killed "prematurely" we have to
1735         // manually tell them to cleanup (it is not enough to simply release
1736         // them since deallocation and cleanup are separated).
1737         [vc cleanup];
1738     }
1740     n = count;
1741     while (n-- > 0 && [cachedVimControllers count] > 0)
1742         [cachedVimControllers removeObjectAtIndex:0];
1745 - (void)rebuildPreloadCache
1747     if ([self maxPreloadCacheSize] > 0) {
1748         [self clearPreloadCacheWithCount:-1];
1749         [self cancelVimControllerPreloadRequests];
1750         [self scheduleVimControllerPreloadAfterDelay:1.0];
1751     }
1754 - (NSDate *)rcFilesModificationDate
1756     // Check modification dates for ~/.vimrc and ~/.gvimrc and return the
1757     // latest modification date.  If ~/.vimrc does not exist, check ~/_vimrc
1758     // and similarly for gvimrc.
1759     // Returns distantPath if no rc files were found.
1761     NSDate *date = [NSDate distantPast];
1762     NSFileManager *fm = [NSFileManager defaultManager];
1764     NSString *path = [@"~/.vimrc" stringByExpandingTildeInPath];
1765     NSDictionary *attr = [fm fileAttributesAtPath:path traverseLink:YES];
1766     if (!attr) {
1767         path = [@"~/_vimrc" stringByExpandingTildeInPath];
1768         attr = [fm fileAttributesAtPath:path traverseLink:YES];
1769     }
1770     NSDate *modDate = [attr objectForKey:NSFileModificationDate];
1771     if (modDate)
1772         date = modDate;
1774     path = [@"~/.gvimrc" stringByExpandingTildeInPath];
1775     attr = [fm fileAttributesAtPath:path traverseLink:YES];
1776     if (!attr) {
1777         path = [@"~/_gvimrc" stringByExpandingTildeInPath];
1778         attr = [fm fileAttributesAtPath:path traverseLink:YES];
1779     }
1780     modDate = [attr objectForKey:NSFileModificationDate];
1781     if (modDate)
1782         date = [date laterDate:modDate];
1784     return date;
1787 - (BOOL)openVimControllerWithArguments:(NSDictionary *)arguments
1789     MMVimController *vc = [self findUnusedEditor];
1790     if (vc) {
1791         // Open files in an already open window.
1792         [[[vc windowController] window] makeKeyAndOrderFront:self];
1793         [vc passArguments:arguments];
1794     } else if ((vc = [self takeVimControllerFromCache])) {
1795         // Open files in a new window using a cached vim controller.  This
1796         // requires virtually no loading time so the new window will pop up
1797         // instantaneously.
1798         [vc passArguments:arguments];
1799         [[vc backendProxy] acknowledgeConnection];
1800     } else {
1801         // Open files in a launching Vim process or start a new process.  This
1802         // may take 1-2 seconds so there will be a visible delay before the
1803         // window appears on screen.
1804         int pid = [self findLaunchingProcessWithoutArguments];
1805         if (-1 == pid) {
1806             pid = [self launchVimProcessWithArguments:nil];
1807             if (-1 == pid)
1808                 return NO;
1809         }
1811         // TODO: If the Vim process fails to start, or if it changes PID,
1812         // then the memory allocated for these parameters will leak.
1813         // Ensure that this cannot happen or somehow detect it.
1815         if ([arguments count] > 0)
1816             [pidArguments setObject:arguments
1817                              forKey:[NSNumber numberWithInt:pid]];
1818     }
1820     return YES;
1823 - (void)activateWhenNextWindowOpens
1825     shouldActivateWhenNextWindowOpens = YES;
1828 - (void)startWatchingVimDir
1830     //NSLog(@"%s", _cmd);
1831 #if (MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_4)
1832     if (fsEventStream)
1833         return;
1834     if (NULL == FSEventStreamStart)
1835         return; // FSEvent functions are weakly linked
1837     NSString *path = [@"~/.vim" stringByExpandingTildeInPath];
1838     NSArray *pathsToWatch = [NSArray arrayWithObject:path];
1840     fsEventStream = FSEventStreamCreate(NULL, &fsEventCallback, NULL,
1841             (CFArrayRef)pathsToWatch, kFSEventStreamEventIdSinceNow,
1842             MMEventStreamLatency, kFSEventStreamCreateFlagNone);
1844     FSEventStreamScheduleWithRunLoop(fsEventStream,
1845             [[NSRunLoop currentRunLoop] getCFRunLoop],
1846             kCFRunLoopDefaultMode);
1848     FSEventStreamStart(fsEventStream);
1849     //NSLog(@"Started FS event stream");
1850 #endif
1853 - (void)stopWatchingVimDir
1855     //NSLog(@"%s", _cmd);
1856 #if (MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_4)
1857     if (NULL == FSEventStreamStop)
1858         return; // FSEvent functions are weakly linked
1860     if (fsEventStream) {
1861         FSEventStreamStop(fsEventStream);
1862         FSEventStreamInvalidate(fsEventStream);
1863         FSEventStreamRelease(fsEventStream);
1864         fsEventStream = NULL;
1865         //NSLog(@"Stopped FS event stream");
1866     }
1867 #endif
1871 - (void)handleFSEvent
1873     //NSLog(@"%s", _cmd);
1874     [self clearPreloadCacheWithCount:-1];
1876     // Several FS events may arrive in quick succession so make sure to cancel
1877     // any previous preload requests before making a new one.
1878     [self cancelVimControllerPreloadRequests];
1879     [self scheduleVimControllerPreloadAfterDelay:0.5];
1882 @end // MMAppController (Private)
1887     static int
1888 executeInLoginShell(NSString *path, NSArray *args)
1890     // Start a login shell and execute the command 'path' with arguments 'args'
1891     // in the shell.  This ensures that user environment variables are set even
1892     // when MacVim was started from the Finder.
1894     int pid = -1;
1895     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
1897     // Determine which shell to use to execute the command.  The user
1898     // may decide which shell to use by setting a user default or the
1899     // $SHELL environment variable.
1900     NSString *shell = [ud stringForKey:MMLoginShellCommandKey];
1901     if (!shell || [shell length] == 0)
1902         shell = [[[NSProcessInfo processInfo] environment]
1903             objectForKey:@"SHELL"];
1904     if (!shell)
1905         shell = @"/bin/bash";
1907     //NSLog(@"shell = %@", shell);
1909     // Bash needs the '-l' flag to launch a login shell.  The user may add
1910     // flags by setting a user default.
1911     NSString *shellArgument = [ud stringForKey:MMLoginShellArgumentKey];
1912     if (!shellArgument || [shellArgument length] == 0) {
1913         if ([[shell lastPathComponent] isEqual:@"bash"])
1914             shellArgument = @"-l";
1915         else
1916             shellArgument = nil;
1917     }
1919     //NSLog(@"shellArgument = %@", shellArgument);
1921     // Build input string to pipe to the login shell.
1922     NSMutableString *input = [NSMutableString stringWithFormat:
1923             @"exec \"%@\"", path];
1924     if (args) {
1925         // Append all arguments, making sure they are properly quoted, even
1926         // when they contain single quotes.
1927         NSEnumerator *e = [args objectEnumerator];
1928         id obj;
1930         while ((obj = [e nextObject])) {
1931             NSMutableString *arg = [NSMutableString stringWithString:obj];
1932             [arg replaceOccurrencesOfString:@"'" withString:@"'\"'\"'"
1933                                     options:NSLiteralSearch
1934                                       range:NSMakeRange(0, [arg length])];
1935             [input appendFormat:@" '%@'", arg];
1936         }
1937     }
1939     // Build the argument vector used to start the login shell.
1940     NSString *shellArg0 = [NSString stringWithFormat:@"-%@",
1941              [shell lastPathComponent]];
1942     char *shellArgv[3] = { (char *)[shellArg0 UTF8String], NULL, NULL };
1943     if (shellArgument)
1944         shellArgv[1] = (char *)[shellArgument UTF8String];
1946     // Get the C string representation of the shell path before the fork since
1947     // we must not call Foundation functions after a fork.
1948     const char *shellPath = [shell fileSystemRepresentation];
1950     // Fork and execute the process.
1951     int ds[2];
1952     if (pipe(ds)) return -1;
1954     pid = fork();
1955     if (pid == -1) {
1956         return -1;
1957     } else if (pid == 0) {
1958         // Child process
1959         if (close(ds[1]) == -1) exit(255);
1960         if (dup2(ds[0], 0) == -1) exit(255);
1962         execv(shellPath, shellArgv);
1964         // Never reached unless execv fails
1965         exit(255);
1966     } else {
1967         // Parent process
1968         if (close(ds[0]) == -1) return -1;
1970         // Send input to execute to the child process
1971         [input appendString:@"\n"];
1972         int bytes = [input lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
1974         if (write(ds[1], [input UTF8String], bytes) != bytes) return -1;
1975         if (close(ds[1]) == -1) return -1;
1976     }
1978     return pid;