Change a few toolbar icons
[MacVim.git] / src / MacVim / MMAppController.m
blob0c8a29fd77496ecb16f19103e6615f648ddf5fb4
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>
53 #define MM_HANDLE_XCODE_MOD_EVENT 0
57 // Default timeout intervals on all connections.
58 static NSTimeInterval MMRequestTimeout = 5;
59 static NSTimeInterval MMReplyTimeout = 5;
61 static NSString *MMWebsiteString = @"http://code.google.com/p/macvim/";
63 // When terminating, notify Vim processes then sleep for these many
64 // microseconds.
65 static useconds_t MMTerminationSleepPeriod = 10000;
68 #pragma options align=mac68k
69 typedef struct
71     short unused1;      // 0 (not used)
72     short lineNum;      // line to select (< 0 to specify range)
73     long  startRange;   // start of selection range (if line < 0)
74     long  endRange;     // end of selection range (if line < 0)
75     long  unused2;      // 0 (not used)
76     long  theDate;      // modification date/time
77 } MMSelectionRange;
78 #pragma options align=reset
81 static int executeInLoginShell(NSString *path, NSArray *args);
84 @interface MMAppController (MMServices)
85 - (void)openSelection:(NSPasteboard *)pboard userData:(NSString *)userData
86                 error:(NSString **)error;
87 - (void)openFile:(NSPasteboard *)pboard userData:(NSString *)userData
88            error:(NSString **)error;
89 @end
92 @interface MMAppController (Private)
93 - (MMVimController *)topmostVimController;
94 - (int)launchVimProcessWithArguments:(NSArray *)args;
95 - (NSArray *)filterFilesAndNotify:(NSArray *)files;
96 - (NSArray *)filterOpenFiles:(NSArray *)filenames
97                    arguments:(NSDictionary *)args;
98 #if MM_HANDLE_XCODE_MOD_EVENT
99 - (void)handleXcodeModEvent:(NSAppleEventDescriptor *)event
100                  replyEvent:(NSAppleEventDescriptor *)reply;
101 #endif
102 - (int)findLaunchingProcessWithoutArguments;
103 - (MMVimController *)findUntitledWindow;
104 - (NSMutableDictionary *)extractArgumentsFromOdocEvent:
105     (NSAppleEventDescriptor *)desc;
106 - (void)passArguments:(NSDictionary *)args toVimController:(MMVimController*)vc;
108 #ifdef MM_ENABLE_PLUGINS
109 - (void)removePlugInMenu;
110 - (void)addPlugInMenuToMenu:(NSMenu *)mainMenu;
111 #endif
112 @end
116 @implementation MMAppController
118 + (void)initialize
120     NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:
121         [NSNumber numberWithBool:NO],   MMNoWindowKey,
122         [NSNumber numberWithInt:64],    MMTabMinWidthKey,
123         [NSNumber numberWithInt:6*64],  MMTabMaxWidthKey,
124         [NSNumber numberWithInt:132],   MMTabOptimumWidthKey,
125         [NSNumber numberWithInt:2],     MMTextInsetLeftKey,
126         [NSNumber numberWithInt:1],     MMTextInsetRightKey,
127         [NSNumber numberWithInt:1],     MMTextInsetTopKey,
128         [NSNumber numberWithInt:1],     MMTextInsetBottomKey,
129         [NSNumber numberWithBool:NO],   MMTerminateAfterLastWindowClosedKey,
130         @"MMTypesetter",                MMTypesetterKey,
131         [NSNumber numberWithFloat:1],   MMCellWidthMultiplierKey,
132         [NSNumber numberWithFloat:-1],  MMBaselineOffsetKey,
133         [NSNumber numberWithBool:YES],  MMTranslateCtrlClickKey,
134         [NSNumber numberWithBool:NO],   MMOpenFilesInTabsKey,
135         [NSNumber numberWithBool:NO],   MMNoFontSubstitutionKey,
136         [NSNumber numberWithBool:NO],   MMLoginShellKey,
137         [NSNumber numberWithBool:NO],   MMAtsuiRendererKey,
138         [NSNumber numberWithInt:MMUntitledWindowAlways],
139                                         MMUntitledWindowKey,
140         [NSNumber numberWithBool:NO],   MMTexturedWindowKey,
141         [NSNumber numberWithBool:NO],   MMZoomBothKey,
142         @"",                            MMLoginShellCommandKey,
143         @"",                            MMLoginShellArgumentKey,
144         [NSNumber numberWithBool:YES],  MMDialogsTrackPwdKey,
145         nil];
147     [[NSUserDefaults standardUserDefaults] registerDefaults:dict];
149     NSArray *types = [NSArray arrayWithObject:NSStringPboardType];
150     [NSApp registerServicesMenuSendTypes:types returnTypes:types];
153 - (id)init
155     if ((self = [super init])) {
156         fontContainerRef = loadFonts();
158         vimControllers = [NSMutableArray new];
159         pidArguments = [NSMutableDictionary new];
161 #ifdef MM_ENABLE_PLUGINS
162         NSString *plugInTitle = NSLocalizedString(@"Plug-In",
163                                                   @"Plug-In menu title");
164         plugInMenuItem = [[NSMenuItem alloc] initWithTitle:plugInTitle
165                                                     action:NULL
166                                              keyEquivalent:@""];
167         NSMenu *submenu = [[NSMenu alloc] initWithTitle:plugInTitle];
168         [plugInMenuItem setSubmenu:submenu];
169         [submenu release];
170 #endif
172         // NOTE: Do not use the default connection since the Logitech Control
173         // Center (LCC) input manager steals and this would cause MacVim to
174         // never open any windows.  (This is a bug in LCC but since they are
175         // unlikely to fix it, we graciously give them the default connection.)
176         connection = [[NSConnection alloc] initWithReceivePort:[NSPort port]
177                                                       sendPort:nil];
178         [connection setRootObject:self];
179         [connection setRequestTimeout:MMRequestTimeout];
180         [connection setReplyTimeout:MMReplyTimeout];
182         // NOTE: When the user is resizing the window the AppKit puts the run
183         // loop in event tracking mode.  Unless the connection listens to
184         // request in this mode, live resizing won't work.
185         [connection addRequestMode:NSEventTrackingRunLoopMode];
187         // NOTE!  If the name of the connection changes here it must also be
188         // updated in MMBackend.m.
189         NSString *name = [NSString stringWithFormat:@"%@-connection",
190                  [[NSBundle mainBundle] bundleIdentifier]];
191         //NSLog(@"Registering connection with name '%@'", name);
192         if (![connection registerName:name]) {
193             NSLog(@"FATAL ERROR: Failed to register connection with name '%@'",
194                     name);
195             [connection release];  connection = nil;
196         }
197     }
199     return self;
202 - (void)dealloc
204     //NSLog(@"MMAppController dealloc");
206     [connection release];  connection = nil;
207     [pidArguments release];  pidArguments = nil;
208     [vimControllers release];  vimControllers = nil;
209     [openSelectionString release];  openSelectionString = nil;
210     [recentFilesMenuItem release];  recentFilesMenuItem = nil;
211     [defaultMainMenu release];  defaultMainMenu = nil;
212 #ifdef MM_ENABLE_PLUGINS
213     [plugInMenuItem release];  plugInMenuItem = nil;
214 #endif
215     [appMenuItemTemplate release];  appMenuItemTemplate = nil;
217     [super dealloc];
220 - (void)applicationWillFinishLaunching:(NSNotification *)notification
222     // Remember the default menu so that it can be restored if the user closes
223     // all editor windows.
224     defaultMainMenu = [[NSApp mainMenu] retain];
226     // Store a copy of the default app menu so we can use this as a template
227     // for all other menus.  We make a copy here because the "Services" menu
228     // will not yet have been populated at this time.  If we don't we get
229     // problems trying to set key equivalents later on because they might clash
230     // with items on the "Services" menu.
231     appMenuItemTemplate = [defaultMainMenu itemAtIndex:0];
232     appMenuItemTemplate = [appMenuItemTemplate copy];
234     // Set up the "Open Recent" menu. See
235     //   http://lapcatsoftware.com/blog/2007/07/10/
236     //     working-without-a-nib-part-5-open-recent-menu/
237     // and
238     //   http://www.cocoabuilder.com/archive/message/cocoa/2007/8/15/187793
239     // for more information.
240     //
241     // The menu itself is created in MainMenu.nib but we still seem to have to
242     // hack around a bit to get it to work.  (This has to be done in
243     // applicationWillFinishLaunching at the latest, otherwise it doesn't
244     // work.)
245     NSMenu *fileMenu = [defaultMainMenu findFileMenu];
246     if (fileMenu) {
247         int idx = [fileMenu indexOfItemWithAction:@selector(fileOpen:)];
248         if (idx >= 0 && idx+1 < [fileMenu numberOfItems])
250         recentFilesMenuItem = [fileMenu itemWithTitle:@"Open Recent"];
251         [[recentFilesMenuItem submenu] performSelector:@selector(_setMenuName:)
252                                         withObject:@"NSRecentDocumentsMenu"];
254         // Note: The "Recent Files" menu must be moved around since there is no
255         // -[NSApp setRecentFilesMenu:] method.  We keep a reference to it to
256         // facilitate this move (see setMainMenu: below).
257         [recentFilesMenuItem retain];
258     }
260 #if MM_HANDLE_XCODE_MOD_EVENT
261     [[NSAppleEventManager sharedAppleEventManager]
262             setEventHandler:self
263                 andSelector:@selector(handleXcodeModEvent:replyEvent:)
264               forEventClass:'KAHL'
265                  andEventID:'MOD '];
266 #endif
269 - (void)applicationDidFinishLaunching:(NSNotification *)notification
271     [NSApp setServicesProvider:self];
272 #ifdef MM_ENABLE_PLUGINS
273     [[MMPlugInManager sharedManager] loadAllPlugIns];
274 #endif
277 - (BOOL)applicationShouldOpenUntitledFile:(NSApplication *)sender
279     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
280     NSAppleEventManager *aem = [NSAppleEventManager sharedAppleEventManager];
281     NSAppleEventDescriptor *desc = [aem currentAppleEvent];
283     // The user default MMUntitledWindow can be set to control whether an
284     // untitled window should open on 'Open' and 'Reopen' events.
285     int untitledWindowFlag = [ud integerForKey:MMUntitledWindowKey];
286     if ([desc eventID] == kAEOpenApplication
287             && (untitledWindowFlag & MMUntitledWindowOnOpen) == 0)
288         return NO;
289     else if ([desc eventID] == kAEReopenApplication
290             && (untitledWindowFlag & MMUntitledWindowOnReopen) == 0)
291         return NO;
293     // When a process is started from the command line, the 'Open' event will
294     // contain a parameter to surpress the opening of an untitled window.
295     desc = [desc paramDescriptorForKeyword:keyAEPropData];
296     desc = [desc paramDescriptorForKeyword:keyMMUntitledWindow];
297     if (desc && ![desc booleanValue])
298         return NO;
300     // Never open an untitled window if there is at least one open window or if
301     // there are processes that are currently launching.
302     if ([vimControllers count] > 0 || [pidArguments count] > 0)
303         return NO;
305     // NOTE!  This way it possible to start the app with the command-line
306     // argument '-nowindow yes' and no window will be opened by default.
307     return ![ud boolForKey:MMNoWindowKey];
310 - (BOOL)applicationOpenUntitledFile:(NSApplication *)sender
312     [self newWindow:self];
313     return YES;
316 - (void)application:(NSApplication *)sender openFiles:(NSArray *)filenames
318     // Opening files works like this:
319     //  a) extract ODB/Xcode/Spotlight parameters from the current Apple event
320     //  b) filter out any already open files (see filterOpenFiles::)
321     //  c) open any remaining files
322     //
323     // A file is opened in an untitled window if there is one (it may be
324     // currently launching, or it may already be visible), otherwise a new
325     // window is opened.
326     //
327     // Each launching Vim process has a dictionary of arguments that are passed
328     // to the process when in checks in (via connectBackend:pid:).  The
329     // arguments for each launching process can be looked up by its PID (in the
330     // pidArguments dictionary).
332     NSMutableDictionary *arguments = [self extractArgumentsFromOdocEvent:
333             [[NSAppleEventManager sharedAppleEventManager] currentAppleEvent]];
335     // Filter out files that are already open
336     filenames = [self filterOpenFiles:filenames arguments:arguments];
338     // Open any files that remain
339     if ([filenames count]) {
340         MMVimController *vc;
341         BOOL openInTabs = [[NSUserDefaults standardUserDefaults]
342             boolForKey:MMOpenFilesInTabsKey];
344         [arguments setObject:filenames forKey:@"filenames"];
345         [arguments setObject:[NSNumber numberWithBool:YES] forKey:@"openFiles"];
347         // Add file names to "Recent Files" menu.
348         int i, count = [filenames count];
349         for (i = 0; i < count; ++i) {
350             // Don't add files that are being edited remotely (using ODB).
351             if ([arguments objectForKey:@"remoteID"]) continue;
353             [[NSDocumentController sharedDocumentController]
354                     noteNewRecentFilePath:[filenames objectAtIndex:i]];
355         }
357         if ((openInTabs && (vc = [self topmostVimController]))
358                || (vc = [self findUntitledWindow])) {
359             // Open files in an already open window.
360             [[[vc windowController] window] makeKeyAndOrderFront:self];
361             [self passArguments:arguments toVimController:vc];
362         } else {
363             // Open files in a launching Vim process or start a new process.
364             int pid = [self findLaunchingProcessWithoutArguments];
365             if (!pid) {
366                 // Pass the filenames to the process straight away.
367                 //
368                 // TODO: It would be nicer if all arguments were passed to the
369                 // Vim process in connectBackend::, but if we don't pass the
370                 // filename arguments here, the window 'flashes' once when it
371                 // opens.  This is due to the 'welcome' screen first being
372                 // displayed, then quickly thereafter the files are opened.
373                 NSArray *fileArgs = [NSArray arrayWithObject:@"-p"];
374                 fileArgs = [fileArgs arrayByAddingObjectsFromArray:filenames];
376                 pid = [self launchVimProcessWithArguments:fileArgs];
378                 if (-1 == pid) {
379                     // TODO: Notify user of failure?
380                     [NSApp replyToOpenOrPrint:
381                         NSApplicationDelegateReplyFailure];
382                     return;
383                 }
385                 // Make sure these files aren't opened again when
386                 // connectBackend:pid: is called.
387                 [arguments setObject:[NSNumber numberWithBool:NO]
388                               forKey:@"openFiles"];
389             }
391             // TODO: If the Vim process fails to start, or if it changes PID,
392             // then the memory allocated for these parameters will leak.
393             // Ensure that this cannot happen or somehow detect it.
395             if ([arguments count] > 0)
396                 [pidArguments setObject:arguments
397                                  forKey:[NSNumber numberWithInt:pid]];
398         }
399     }
401     [NSApp replyToOpenOrPrint:NSApplicationDelegateReplySuccess];
402     // NSApplicationDelegateReplySuccess = 0,
403     // NSApplicationDelegateReplyCancel = 1,
404     // NSApplicationDelegateReplyFailure = 2
407 - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender
409     return [[NSUserDefaults standardUserDefaults]
410             boolForKey:MMTerminateAfterLastWindowClosedKey];
413 - (NSApplicationTerminateReply)applicationShouldTerminate:
414     (NSApplication *)sender
416     // TODO: Follow Apple's guidelines for 'Graceful Application Termination'
417     // (in particular, allow user to review changes and save).
418     int reply = NSTerminateNow;
419     BOOL modifiedBuffers = NO;
421     // Go through windows, checking for modified buffers.  (Each Vim process
422     // tells MacVim when any buffer has been modified and MacVim sets the
423     // 'documentEdited' flag of the window correspondingly.)
424     NSEnumerator *e = [[NSApp windows] objectEnumerator];
425     id window;
426     while ((window = [e nextObject])) {
427         if ([window isDocumentEdited]) {
428             modifiedBuffers = YES;
429             break;
430         }
431     }
433     if (modifiedBuffers) {
434         NSAlert *alert = [[NSAlert alloc] init];
435         [alert setAlertStyle:NSWarningAlertStyle];
436         [alert addButtonWithTitle:NSLocalizedString(@"Quit",
437                 @"Dialog button")];
438         [alert addButtonWithTitle:NSLocalizedString(@"Cancel",
439                 @"Dialog button")];
440         [alert setMessageText:NSLocalizedString(@"Quit without saving?",
441                 @"Quit dialog with changed buffers, title")];
442         [alert setInformativeText:NSLocalizedString(
443                 @"There are modified buffers, "
444                 "if you quit now all changes will be lost.  Quit anyway?",
445                 @"Quit dialog with changed buffers, text")];
447         if ([alert runModal] != NSAlertFirstButtonReturn)
448             reply = NSTerminateCancel;
450         [alert release];
451     } else {
452         // No unmodified buffers, but give a warning if there are multiple
453         // windows and/or tabs open.
454         int numWindows = [vimControllers count];
455         int numTabs = 0;
457         // Count the number of open tabs
458         e = [vimControllers objectEnumerator];
459         id vc;
460         while ((vc = [e nextObject])) {
461             NSString *eval = [vc evaluateVimExpression:@"tabpagenr('$')"];
462             if (eval) {
463                 int count = [eval intValue];
464                 if (count > 0 && count < INT_MAX)
465                     numTabs += count;
466             }
467         }
469         if (numWindows > 1 || numTabs > 1) {
470             NSAlert *alert = [[NSAlert alloc] init];
471             [alert setAlertStyle:NSWarningAlertStyle];
472             [alert addButtonWithTitle:NSLocalizedString(@"Quit",
473                     @"Dialog button")];
474             [alert addButtonWithTitle:NSLocalizedString(@"Cancel",
475                     @"Dialog button")];
476             [alert setMessageText:NSLocalizedString(
477                     @"Are you sure you want to quit MacVim?",
478                     @"Quit dialog with no changed buffers, title")];
480             NSString *info = nil;
481             if (numWindows > 1) {
482                 if (numTabs > numWindows)
483                     info = [NSString stringWithFormat:NSLocalizedString(
484                             @"There are %d windows open in MacVim, with a "
485                             "total of %d tabs. Do you want to quit anyway?",
486                             @"Quit dialog with no changed buffers, text"),
487                          numWindows, numTabs];
488                 else
489                     info = [NSString stringWithFormat:NSLocalizedString(
490                             @"There are %d windows open in MacVim. "
491                             "Do you want to quit anyway?",
492                             @"Quit dialog with no changed buffers, text"),
493                         numWindows];
495             } else {
496                 info = [NSString stringWithFormat:NSLocalizedString(
497                         @"There are %d tabs open in MacVim. "
498                         "Do you want to quit anyway?",
499                         @"Quit dialog with no changed buffers, text"), 
500                      numTabs];
501             }
503             [alert setInformativeText:info];
505             if ([alert runModal] != NSAlertFirstButtonReturn)
506                 reply = NSTerminateCancel;
508             [alert release];
509         }
510     }
513     // Tell all Vim processes to terminate now (otherwise they'll leave swap
514     // files behind).
515     if (NSTerminateNow == reply) {
516         e = [vimControllers objectEnumerator];
517         id vc;
518         while ((vc = [e nextObject]))
519             [vc sendMessage:TerminateNowMsgID data:nil];
521         // Give Vim processes a chance to terminate before MacVim.  If they
522         // haven't terminated by the time applicationWillTerminate: is sent,
523         // they may be forced to quit (see below).
524         usleep(MMTerminationSleepPeriod);
525     }
527     return reply;
530 - (void)applicationWillTerminate:(NSNotification *)notification
532 #ifdef MM_ENABLE_PLUGINS
533     [[MMPlugInManager sharedManager] unloadAllPlugIns];
534 #endif
536 #if MM_HANDLE_XCODE_MOD_EVENT
537     [[NSAppleEventManager sharedAppleEventManager]
538             removeEventHandlerForEventClass:'KAHL'
539                                  andEventID:'MOD '];
540 #endif
542     // This will invalidate all connections (since they were spawned from this
543     // connection).
544     [connection invalidate];
546     // Send a SIGINT to all running Vim processes, so that they are sure to
547     // receive the connectionDidDie: notification (a process has to be checking
548     // the run-loop for this to happen).
549     unsigned i, count = [vimControllers count];
550     for (i = 0; i < count; ++i) {
551         MMVimController *controller = [vimControllers objectAtIndex:i];
552         int pid = [controller pid];
553         if (pid > 0)
554             kill(pid, SIGINT);
555     }
557     if (fontContainerRef) {
558         ATSFontDeactivate(fontContainerRef, NULL, kATSOptionFlagsDefault);
559         fontContainerRef = 0;
560     }
562     [NSApp setDelegate:nil];
565 + (MMAppController *)sharedInstance
567     // Note: The app controller is a singleton which is instantiated in
568     // MainMenu.nib where it is also connected as the delegate of NSApp.
569     id delegate = [NSApp delegate];
570     return [delegate isKindOfClass:self] ? (MMAppController*)delegate : nil;
573 - (NSMenu *)defaultMainMenu
575     return defaultMainMenu;
578 - (NSMenuItem *)appMenuItemTemplate
580     return appMenuItemTemplate;
583 - (void)removeVimController:(id)controller
585     //NSLog(@"%s%@", _cmd, controller);
587     [controller cleanup];
588     [[controller windowController] close];
590     [vimControllers removeObject:controller];
592     if (![vimControllers count]) {
593         // The last editor window just closed so restore the main menu back to
594         // its default state (which is defined in MainMenu.nib).
595         [self setMainMenu:defaultMainMenu];
596     }
599 - (void)windowControllerWillOpen:(MMWindowController *)windowController
601     NSPoint topLeft = NSZeroPoint;
602     NSWindow *topWin = [[[self topmostVimController] windowController] window];
603     NSWindow *win = [windowController window];
605     if (!win) return;
607     // If there is a window belonging to a Vim process, cascade from it,
608     // otherwise use the autosaved window position (if any).
609     if (topWin) {
610         NSRect frame = [topWin frame];
611         topLeft = NSMakePoint(frame.origin.x, NSMaxY(frame));
612     } else {
613         NSString *topLeftString = [[NSUserDefaults standardUserDefaults]
614             stringForKey:MMTopLeftPointKey];
615         if (topLeftString)
616             topLeft = NSPointFromString(topLeftString);
617     }
619     if (!NSEqualPoints(topLeft, NSZeroPoint)) {
620         if (topWin)
621             topLeft = [win cascadeTopLeftFromPoint:topLeft];
623         [win setFrameTopLeftPoint:topLeft];
624     }
626     if (openSelectionString) {
627         // TODO: Pass this as a parameter instead!  Get rid of
628         // 'openSelectionString' etc.
629         //
630         // There is some text to paste into this window as a result of the
631         // services menu "Open selection ..." being used.
632         [[windowController vimController] dropString:openSelectionString];
633         [openSelectionString release];
634         openSelectionString = nil;
635     }
638 - (void)setMainMenu:(NSMenu *)mainMenu
640     if ([NSApp mainMenu] == mainMenu) return;
642     // If the new menu has a "Recent Files" dummy item, then swap the real item
643     // for the dummy.  We are forced to do this since Cocoa initializes the
644     // "Recent Files" menu and there is no way to simply point Cocoa to a new
645     // item each time the menus are swapped.
646     NSMenu *fileMenu = [mainMenu findFileMenu];
647     if (recentFilesMenuItem && fileMenu) {
648         int dummyIdx =
649                 [fileMenu indexOfItemWithAction:@selector(recentFilesDummy:)];
650         if (dummyIdx >= 0) {
651             NSMenuItem *dummyItem = [[fileMenu itemAtIndex:dummyIdx] retain];
652             [fileMenu removeItemAtIndex:dummyIdx];
654             NSMenu *recentFilesParentMenu = [recentFilesMenuItem menu];
655             int idx = [recentFilesParentMenu indexOfItem:recentFilesMenuItem];
656             if (idx >= 0) {
657                 [[recentFilesMenuItem retain] autorelease];
658                 [recentFilesParentMenu removeItemAtIndex:idx];
659                 [recentFilesParentMenu insertItem:dummyItem atIndex:idx];
660             }
662             [fileMenu insertItem:recentFilesMenuItem atIndex:dummyIdx];
663             [dummyItem release];
664         }
665     }
667     // Now set the new menu.  Notice that we keep one menu for each editor
668     // window since each editor can have its own set of menus.  When swapping
669     // menus we have to tell Cocoa where the new "MacVim", "Windows", and
670     // "Services" menu are.
671     [NSApp setMainMenu:mainMenu];
673     // Setting the "MacVim" (or "Application") menu ensures that it is typeset
674     // in boldface.  (The setAppleMenu: method used to be public but is now
675     // private so this will have to be considered a bit of a hack!)
676     NSMenu *appMenu = [mainMenu findApplicationMenu];
677     [NSApp performSelector:@selector(setAppleMenu:) withObject:appMenu];
679     NSMenu *servicesMenu = [mainMenu findServicesMenu];
680     [NSApp setServicesMenu:servicesMenu];
682     NSMenu *windowsMenu = [mainMenu findWindowsMenu];
683     if (windowsMenu) {
684         // Cocoa isn't clever enough to get rid of items it has added to the
685         // "Windows" menu so we have to do it ourselves otherwise there will be
686         // multiple menu items for each window in the "Windows" menu.
687         //   This code assumes that the only items Cocoa add are ones which
688         // send off the action makeKeyAndOrderFront:.  (Cocoa will not add
689         // another separator item if the last item on the "Windows" menu
690         // already is a separator, so we needen't worry about separators.)
691         int i, count = [windowsMenu numberOfItems];
692         for (i = count-1; i >= 0; --i) {
693             NSMenuItem *item = [windowsMenu itemAtIndex:i];
694             if ([item action] == @selector(makeKeyAndOrderFront:))
695                 [windowsMenu removeItem:item];
696         }
697     }
698     [NSApp setWindowsMenu:windowsMenu];
700 #ifdef MM_ENABLE_PLUGINS
701     // Move plugin menu from old to new main menu.
702     [self removePlugInMenu];
703     [self addPlugInMenuToMenu:mainMenu];
704 #endif
707 #ifdef MM_ENABLE_PLUGINS
708 - (void)addItemToPlugInMenu:(NSMenuItem *)item
710     NSMenu *menu = [plugInMenuItem submenu];
711     [menu addItem:item];
712     if ([menu numberOfItems] == 1)
713         [self addPlugInMenuToMenu:[NSApp mainMenu]];
716 - (void)removeItemFromPlugInMenu:(NSMenuItem *)item
718     NSMenu *menu = [plugInMenuItem submenu];
719     [menu removeItem:item];
720     if ([menu numberOfItems] == 0)
721         [self removePlugInMenu];
723 #endif
725 - (IBAction)newWindow:(id)sender
727     [self launchVimProcessWithArguments:nil];
730 - (IBAction)fileOpen:(id)sender
732     NSString *dir = nil;
733     BOOL trackPwd = [[NSUserDefaults standardUserDefaults]
734             boolForKey:MMDialogsTrackPwdKey];
735     if (trackPwd) {
736         MMVimController *vc = [self keyVimController];
737         if (vc) dir = [[vc vimState] objectForKey:@"pwd"];
738     }
740     NSOpenPanel *panel = [NSOpenPanel openPanel];
741     [panel setAllowsMultipleSelection:YES];
742     [panel setAccessoryView:openPanelAccessoryView()];
744     int result = [panel runModalForDirectory:dir file:nil types:nil];
745     if (NSOKButton == result)
746         [self application:NSApp openFiles:[panel filenames]];
749 - (IBAction)selectNextWindow:(id)sender
751     unsigned i, count = [vimControllers count];
752     if (!count) return;
754     NSWindow *keyWindow = [NSApp keyWindow];
755     for (i = 0; i < count; ++i) {
756         MMVimController *vc = [vimControllers objectAtIndex:i];
757         if ([[[vc windowController] window] isEqual:keyWindow])
758             break;
759     }
761     if (i < count) {
762         if (++i >= count)
763             i = 0;
764         MMVimController *vc = [vimControllers objectAtIndex:i];
765         [[vc windowController] showWindow:self];
766     }
769 - (IBAction)selectPreviousWindow:(id)sender
771     unsigned i, count = [vimControllers count];
772     if (!count) return;
774     NSWindow *keyWindow = [NSApp keyWindow];
775     for (i = 0; i < count; ++i) {
776         MMVimController *vc = [vimControllers objectAtIndex:i];
777         if ([[[vc windowController] window] isEqual:keyWindow])
778             break;
779     }
781     if (i < count) {
782         if (i > 0) {
783             --i;
784         } else {
785             i = count - 1;
786         }
787         MMVimController *vc = [vimControllers objectAtIndex:i];
788         [[vc windowController] showWindow:self];
789     }
792 - (IBAction)orderFrontPreferencePanel:(id)sender
794     [[MMPreferenceController sharedPrefsWindowController] showWindow:self];
797 - (IBAction)openWebsite:(id)sender
799     [[NSWorkspace sharedWorkspace] openURL:
800             [NSURL URLWithString:MMWebsiteString]];
803 - (IBAction)showVimHelp:(id)sender
805     // Open a new window with the help window maximized.
806     [self launchVimProcessWithArguments:[NSArray arrayWithObjects:
807             @"-c", @":h gui_mac", @"-c", @":res", nil]];
810 - (IBAction)zoomAll:(id)sender
812     [NSApp makeWindowsPerform:@selector(performZoom:) inOrder:YES];
815 - (byref id <MMFrontendProtocol>)
816     connectBackend:(byref in id <MMBackendProtocol>)backend
817                pid:(int)pid
819     //NSLog(@"Connect backend (pid=%d)", pid);
820     NSNumber *pidKey = [NSNumber numberWithInt:pid];
821     MMVimController *vc = nil;
823     @try {
824         [(NSDistantObject*)backend
825                 setProtocolForProxy:@protocol(MMBackendProtocol)];
827         vc = [[[MMVimController alloc]
828             initWithBackend:backend pid:pid]
829             autorelease];
831         if (![vimControllers count]) {
832             // The first window autosaves its position.  (The autosaving
833             // features of Cocoa are not used because we need more control over
834             // what is autosaved and when it is restored.)
835             [[vc windowController] setWindowAutosaveKey:MMTopLeftPointKey];
836         }
838         [vimControllers addObject:vc];
840         id args = [pidArguments objectForKey:pidKey];
841         if (args && [NSNull null] != args)
842             [self passArguments:args toVimController:vc];
844         // HACK!  MacVim does not get activated if it is launched from the
845         // terminal, so we forcibly activate here unless it is an untitled
846         // window opening.  Untitled windows are treated differently, else
847         // MacVim would steal the focus if another app was activated while the
848         // untitled window was loading.
849         if (!args || args != [NSNull null])
850             [NSApp activateIgnoringOtherApps:YES];
852         if (args)
853             [pidArguments removeObjectForKey:pidKey];
855         return vc;
856     }
858     @catch (NSException *e) {
859         NSLog(@"Exception caught in %s: \"%@\"", _cmd, e);
861         if (vc)
862             [vimControllers removeObject:vc];
864         [pidArguments removeObjectForKey:pidKey];
865     }
867     return nil;
870 - (NSArray *)serverList
872     NSMutableArray *array = [NSMutableArray array];
874     unsigned i, count = [vimControllers count];
875     for (i = 0; i < count; ++i) {
876         MMVimController *controller = [vimControllers objectAtIndex:i];
877         if ([controller serverName])
878             [array addObject:[controller serverName]];
879     }
881     return array;
884 - (MMVimController *)keyVimController
886     NSWindow *keyWindow = [NSApp keyWindow];
887     if (keyWindow) {
888         unsigned i, count = [vimControllers count];
889         for (i = 0; i < count; ++i) {
890             MMVimController *vc = [vimControllers objectAtIndex:i];
891             if ([[[vc windowController] window] isEqual:keyWindow])
892                 return vc;
893         }
894     }
896     return nil;
899 @end // MMAppController
904 @implementation MMAppController (MMServices)
906 - (void)openSelection:(NSPasteboard *)pboard userData:(NSString *)userData
907                 error:(NSString **)error
909     if (![[pboard types] containsObject:NSStringPboardType]) {
910         NSLog(@"WARNING: Pasteboard contains no object of type "
911                 "NSStringPboardType");
912         return;
913     }
915     MMVimController *vc = [self topmostVimController];
916     if (vc) {
917         // Open a new tab first, since dropString: does not do this.
918         [vc sendMessage:AddNewTabMsgID data:nil];
919         [vc dropString:[pboard stringForType:NSStringPboardType]];
920     } else {
921         // NOTE: There is no window to paste the selection into, so save the
922         // text, open a new window, and paste the text when the next window
923         // opens.  (If this is called several times in a row, then all but the
924         // last call might be ignored.)
925         if (openSelectionString) [openSelectionString release];
926         openSelectionString = [[pboard stringForType:NSStringPboardType] copy];
928         [self newWindow:self];
929     }
932 - (void)openFile:(NSPasteboard *)pboard userData:(NSString *)userData
933            error:(NSString **)error
935     if (![[pboard types] containsObject:NSStringPboardType]) {
936         NSLog(@"WARNING: Pasteboard contains no object of type "
937                 "NSStringPboardType");
938         return;
939     }
941     // TODO: Parse multiple filenames and create array with names.
942     NSString *string = [pboard stringForType:NSStringPboardType];
943     string = [string stringByTrimmingCharactersInSet:
944             [NSCharacterSet whitespaceAndNewlineCharacterSet]];
945     string = [string stringByStandardizingPath];
947     NSArray *filenames = [self filterFilesAndNotify:
948             [NSArray arrayWithObject:string]];
949     if ([filenames count] > 0) {
950         MMVimController *vc = nil;
951         if (userData && [userData isEqual:@"Tab"])
952             vc = [self topmostVimController];
954         if (vc) {
955             [vc dropFiles:filenames forceOpen:YES];
956         } else {
957             [self application:NSApp openFiles:filenames];
958         }
959     }
962 @end // MMAppController (MMServices)
967 @implementation MMAppController (Private)
969 - (MMVimController *)topmostVimController
971     // Find the topmost visible window which has an associated vim controller.
972     NSEnumerator *e = [[NSApp orderedWindows] objectEnumerator];
973     id window;
974     while ((window = [e nextObject]) && [window isVisible]) {
975         unsigned i, count = [vimControllers count];
976         for (i = 0; i < count; ++i) {
977             MMVimController *vc = [vimControllers objectAtIndex:i];
978             if ([[[vc windowController] window] isEqual:window])
979                 return vc;
980         }
981     }
983     return nil;
986 - (int)launchVimProcessWithArguments:(NSArray *)args
988     int pid = -1;
989     NSString *path = [[NSBundle mainBundle] pathForAuxiliaryExecutable:@"Vim"];
991     if (!path) {
992         NSLog(@"ERROR: Vim executable could not be found inside app bundle!");
993         return -1;
994     }
996     NSArray *taskArgs = [NSArray arrayWithObjects:@"-g", @"-f", nil];
997     if (args)
998         taskArgs = [taskArgs arrayByAddingObjectsFromArray:args];
1000     BOOL useLoginShell = [[NSUserDefaults standardUserDefaults]
1001             boolForKey:MMLoginShellKey];
1002     if (useLoginShell) {
1003         // Run process with a login shell, roughly:
1004         //   echo "exec Vim -g -f args" | ARGV0=-`basename $SHELL` $SHELL [-l]
1005         pid = executeInLoginShell(path, taskArgs);
1006     } else {
1007         // Run process directly:
1008         //   Vim -g -f args
1009         NSTask *task = [NSTask launchedTaskWithLaunchPath:path
1010                                                 arguments:taskArgs];
1011         pid = task ? [task processIdentifier] : -1;
1012     }
1014     if (-1 != pid) {
1015         // NOTE: If the process has no arguments, then add a null argument to
1016         // the pidArguments dictionary.  This is later used to detect that a
1017         // process without arguments is being launched.
1018         if (!args)
1019             [pidArguments setObject:[NSNull null]
1020                              forKey:[NSNumber numberWithInt:pid]];
1021     } else {
1022         NSLog(@"WARNING: %s%@ failed (useLoginShell=%d)", _cmd, args,
1023                 useLoginShell);
1024     }
1026     return pid;
1029 - (NSArray *)filterFilesAndNotify:(NSArray *)filenames
1031     // Go trough 'filenames' array and make sure each file exists.  Present
1032     // warning dialog if some file was missing.
1034     NSString *firstMissingFile = nil;
1035     NSMutableArray *files = [NSMutableArray array];
1036     unsigned i, count = [filenames count];
1038     for (i = 0; i < count; ++i) {
1039         NSString *name = [filenames objectAtIndex:i];
1040         if ([[NSFileManager defaultManager] fileExistsAtPath:name]) {
1041             [files addObject:name];
1042         } else if (!firstMissingFile) {
1043             firstMissingFile = name;
1044         }
1045     }
1047     if (firstMissingFile) {
1048         NSAlert *alert = [[NSAlert alloc] init];
1049         [alert addButtonWithTitle:NSLocalizedString(@"OK",
1050                 @"Dialog button")];
1052         NSString *text;
1053         if ([files count] >= count-1) {
1054             [alert setMessageText:NSLocalizedString(@"File not found",
1055                     @"File not found dialog, title")];
1056             text = [NSString stringWithFormat:NSLocalizedString(
1057                     @"Could not open file with name %@.",
1058                     @"File not found dialog, text"), firstMissingFile];
1059         } else {
1060             [alert setMessageText:NSLocalizedString(@"Multiple files not found",
1061                     @"File not found dialog, title")];
1062             text = [NSString stringWithFormat:NSLocalizedString(
1063                     @"Could not open file with name %@, and %d other files.",
1064                     @"File not found dialog, text"),
1065                 firstMissingFile, count-[files count]-1];
1066         }
1068         [alert setInformativeText:text];
1069         [alert setAlertStyle:NSWarningAlertStyle];
1071         [alert runModal];
1072         [alert release];
1074         [NSApp replyToOpenOrPrint:NSApplicationDelegateReplyFailure];
1075     }
1077     return files;
1080 - (NSArray *)filterOpenFiles:(NSArray *)filenames
1081                    arguments:(NSDictionary *)args
1083     // Check if any of the files in the 'filenames' array are open in any Vim
1084     // process.  Remove the files that are open from the 'filenames' array and
1085     // return it.  If all files were filtered out, then raise the first file in
1086     // the Vim process it is open.  Files that are filtered are sent an odb
1087     // open event in case theID is not zero.
1089     NSMutableDictionary *localArgs =
1090             [NSMutableDictionary dictionaryWithDictionary:args];
1091     MMVimController *raiseController = nil;
1092     NSString *raiseFile = nil;
1093     NSMutableArray *files = [filenames mutableCopy];
1094     NSString *expr = [NSString stringWithFormat:
1095             @"map([\"%@\"],\"bufloaded(v:val)\")",
1096             [files componentsJoinedByString:@"\",\""]];
1097     unsigned i, count = [vimControllers count];
1099     // Ensure that the files aren't opened when passing arguments.
1100     [localArgs setObject:[NSNumber numberWithBool:NO] forKey:@"openFiles"];
1102     for (i = 0; i < count && [files count]; ++i) {
1103         MMVimController *controller = [vimControllers objectAtIndex:i];
1105         // Query Vim for which files in the 'files' array are open.
1106         NSString *eval = [controller evaluateVimExpression:expr];
1107         if (!eval) continue;
1109         NSIndexSet *idxSet = [NSIndexSet indexSetWithVimList:eval];
1110         if ([idxSet count]) {
1111             if (!raiseFile) {
1112                 // Remember the file and which Vim that has it open so that
1113                 // we can raise it later on.
1114                 raiseController = controller;
1115                 raiseFile = [files objectAtIndex:[idxSet firstIndex]];
1116                 [[raiseFile retain] autorelease];
1117             }
1119             // Pass (ODB/Xcode/Spotlight) arguments to this process.
1120             [localArgs setObject:[files objectsAtIndexes:idxSet]
1121                           forKey:@"filenames"];
1122             [self passArguments:localArgs toVimController:controller];
1124             // Remove all the files that were open in this Vim process and
1125             // create a new expression to evaluate.
1126             [files removeObjectsAtIndexes:idxSet];
1127             expr = [NSString stringWithFormat:
1128                     @"map([\"%@\"],\"bufloaded(v:val)\")",
1129                     [files componentsJoinedByString:@"\",\""]];
1130         }
1131     }
1133     if (![files count] && raiseFile) {
1134         // Raise the window containing the first file that was already open,
1135         // and make sure that the tab containing that file is selected.  Only
1136         // do this if there are no more files to open, otherwise sometimes the
1137         // window with 'raiseFile' will be raised, other times it might be the
1138         // window that will open with the files in the 'files' array.
1139         raiseFile = [raiseFile stringByEscapingSpecialFilenameCharacters];
1140         NSString *input = [NSString stringWithFormat:@"<C-\\><C-N>"
1141             ":let oldswb=&swb|let &swb=\"useopen,usetab\"|"
1142             "tab sb %@|let &swb=oldswb|unl oldswb|"
1143             "cal foreground()|redr|f<CR>", raiseFile];
1145         [raiseController addVimInput:input];
1146     }
1148     return files;
1151 #if MM_HANDLE_XCODE_MOD_EVENT
1152 - (void)handleXcodeModEvent:(NSAppleEventDescriptor *)event
1153                  replyEvent:(NSAppleEventDescriptor *)reply
1155 #if 0
1156     // Xcode sends this event to query MacVim which open files have been
1157     // modified.
1158     NSLog(@"reply:%@", reply);
1159     NSLog(@"event:%@", event);
1161     NSEnumerator *e = [vimControllers objectEnumerator];
1162     id vc;
1163     while ((vc = [e nextObject])) {
1164         DescType type = [reply descriptorType];
1165         unsigned len = [[type data] length];
1166         NSMutableData *data = [NSMutableData data];
1168         [data appendBytes:&type length:sizeof(DescType)];
1169         [data appendBytes:&len length:sizeof(unsigned)];
1170         [data appendBytes:[reply data] length:len];
1172         [vc sendMessage:XcodeModMsgID data:data];
1173     }
1174 #endif
1176 #endif
1178 - (int)findLaunchingProcessWithoutArguments
1180     NSArray *keys = [pidArguments allKeysForObject:[NSNull null]];
1181     if ([keys count] > 0) {
1182         //NSLog(@"found launching process without arguments");
1183         return [[keys objectAtIndex:0] intValue];
1184     }
1186     return 0;
1189 - (MMVimController *)findUntitledWindow
1191     NSEnumerator *e = [vimControllers objectEnumerator];
1192     id vc;
1193     while ((vc = [e nextObject])) {
1194         // TODO: This is a moronic test...should query the Vim process if there
1195         // are any open buffers or something like that instead.
1196         NSString *title = [[[vc windowController] window] title];
1198         // TODO: this will not work in a localized MacVim
1199         if ([title hasPrefix:@"[No Name] - VIM"]) {
1200             //NSLog(@"found untitled window");
1201             return vc;
1202         }
1203     }
1205     return nil;
1208 - (NSMutableDictionary *)extractArgumentsFromOdocEvent:
1209     (NSAppleEventDescriptor *)desc
1211     NSMutableDictionary *dict = [NSMutableDictionary dictionary];
1213     // 1. Extract ODB parameters (if any)
1214     NSAppleEventDescriptor *odbdesc = desc;
1215     if (![odbdesc paramDescriptorForKeyword:keyFileSender]) {
1216         // The ODB paramaters may hide inside the 'keyAEPropData' descriptor.
1217         odbdesc = [odbdesc paramDescriptorForKeyword:keyAEPropData];
1218         if (![odbdesc paramDescriptorForKeyword:keyFileSender])
1219             odbdesc = nil;
1220     }
1222     if (odbdesc) {
1223         NSAppleEventDescriptor *p =
1224                 [odbdesc paramDescriptorForKeyword:keyFileSender];
1225         if (p)
1226             [dict setObject:[NSNumber numberWithUnsignedInt:[p typeCodeValue]]
1227                      forKey:@"remoteID"];
1229         p = [odbdesc paramDescriptorForKeyword:keyFileCustomPath];
1230         if (p)
1231             [dict setObject:[p stringValue] forKey:@"remotePath"];
1233         p = [odbdesc paramDescriptorForKeyword:keyFileSenderToken];
1234         if (p)
1235             [dict setObject:p forKey:@"remotePath"];
1236     }
1238     // 2. Extract Xcode parameters (if any)
1239     NSAppleEventDescriptor *xcodedesc =
1240             [desc paramDescriptorForKeyword:keyAEPosition];
1241     if (xcodedesc) {
1242         NSRange range;
1243         MMSelectionRange *sr = (MMSelectionRange*)[[xcodedesc data] bytes];
1245         if (sr->lineNum < 0) {
1246             // Should select a range of lines.
1247             range.location = sr->startRange + 1;
1248             range.length = sr->endRange - sr->startRange + 1;
1249         } else {
1250             // Should only move cursor to a line.
1251             range.location = sr->lineNum + 1;
1252             range.length = 0;
1253         }
1255         [dict setObject:NSStringFromRange(range) forKey:@"selectionRange"];
1256     }
1258     // 3. Extract Spotlight search text (if any)
1259     NSAppleEventDescriptor *spotlightdesc = 
1260             [desc paramDescriptorForKeyword:keyAESearchText];
1261     if (spotlightdesc)
1262         [dict setObject:[spotlightdesc stringValue] forKey:@"searchText"];
1264     return dict;
1267 - (void)passArguments:(NSDictionary *)args toVimController:(MMVimController*)vc
1269     if (!args) return;
1271     // Pass filenames to open if required (the 'openFiles' argument can be used
1272     // to disallow opening of the files).
1273     NSArray *filenames = [args objectForKey:@"filenames"];
1274     if (filenames && [[args objectForKey:@"openFiles"] boolValue]) {
1275         NSString *tabDrop = buildTabDropCommand(filenames);
1276         [vc addVimInput:tabDrop];
1277     }
1279     // Pass ODB data
1280     if (filenames && [args objectForKey:@"remoteID"]) {
1281         [vc odbEdit:filenames
1282              server:[[args objectForKey:@"remoteID"] unsignedIntValue]
1283                path:[args objectForKey:@"remotePath"]
1284               token:[args objectForKey:@"remoteToken"]];
1285     }
1287     // Pass range of lines to select
1288     if ([args objectForKey:@"selectionRange"]) {
1289         NSRange selectionRange = NSRangeFromString(
1290                 [args objectForKey:@"selectionRange"]);
1291         [vc addVimInput:buildSelectRangeCommand(selectionRange)];
1292     }
1294     // Pass search text
1295     NSString *searchText = [args objectForKey:@"searchText"];
1296     if (searchText)
1297         [vc addVimInput:buildSearchTextCommand(searchText)];
1300 #ifdef MM_ENABLE_PLUGINS
1301 - (void)removePlugInMenu
1303     if ([plugInMenuItem menu])
1304         [[plugInMenuItem menu] removeItem:plugInMenuItem];
1307 - (void)addPlugInMenuToMenu:(NSMenu *)mainMenu
1309     NSMenu *windowsMenu = [mainMenu findWindowsMenu];
1311     if ([[plugInMenuItem submenu] numberOfItems] > 0) {
1312         int idx = windowsMenu ? [mainMenu indexOfItemWithSubmenu:windowsMenu]
1313                               : -1;
1314         if (idx > 0) {
1315             [mainMenu insertItem:plugInMenuItem atIndex:idx];
1316         } else {
1317             [mainMenu addItem:plugInMenuItem];
1318         }
1319     }
1321 #endif
1323 @end // MMAppController (Private)
1328     static int
1329 executeInLoginShell(NSString *path, NSArray *args)
1331     // Start a login shell and execute the command 'path' with arguments 'args'
1332     // in the shell.  This ensures that user environment variables are set even
1333     // when MacVim was started from the Finder.
1335     int pid = -1;
1336     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
1338     // Determine which shell to use to execute the command.  The user
1339     // may decide which shell to use by setting a user default or the
1340     // $SHELL environment variable.
1341     NSString *shell = [ud stringForKey:MMLoginShellCommandKey];
1342     if (!shell || [shell length] == 0)
1343         shell = [[[NSProcessInfo processInfo] environment]
1344             objectForKey:@"SHELL"];
1345     if (!shell)
1346         shell = @"/bin/bash";
1348     //NSLog(@"shell = %@", shell);
1350     // Bash needs the '-l' flag to launch a login shell.  The user may add
1351     // flags by setting a user default.
1352     NSString *shellArgument = [ud stringForKey:MMLoginShellArgumentKey];
1353     if (!shellArgument || [shellArgument length] == 0) {
1354         if ([[shell lastPathComponent] isEqual:@"bash"])
1355             shellArgument = @"-l";
1356         else
1357             shellArgument = nil;
1358     }
1360     //NSLog(@"shellArgument = %@", shellArgument);
1362     // Build input string to pipe to the login shell.
1363     NSMutableString *input = [NSMutableString stringWithFormat:
1364             @"exec \"%@\"", path];
1365     if (args) {
1366         // Append all arguments, making sure they are properly quoted, even
1367         // when they contain single quotes.
1368         NSEnumerator *e = [args objectEnumerator];
1369         id obj;
1371         while ((obj = [e nextObject])) {
1372             NSMutableString *arg = [NSMutableString stringWithString:obj];
1373             [arg replaceOccurrencesOfString:@"'" withString:@"'\"'\"'"
1374                                     options:NSLiteralSearch
1375                                       range:NSMakeRange(0, [arg length])];
1376             [input appendFormat:@" '%@'", arg];
1377         }
1378     }
1380     // Build the argument vector used to start the login shell.
1381     NSString *shellArg0 = [NSString stringWithFormat:@"-%@",
1382              [shell lastPathComponent]];
1383     char *shellArgv[3] = { (char *)[shellArg0 UTF8String], NULL, NULL };
1384     if (shellArgument)
1385         shellArgv[1] = (char *)[shellArgument UTF8String];
1387     // Get the C string representation of the shell path before the fork since
1388     // we must not call Foundation functions after a fork.
1389     const char *shellPath = [shell fileSystemRepresentation];
1391     // Fork and execute the process.
1392     int ds[2];
1393     if (pipe(ds)) return -1;
1395     pid = fork();
1396     if (pid == -1) {
1397         return -1;
1398     } else if (pid == 0) {
1399         // Child process
1400         if (close(ds[1]) == -1) exit(255);
1401         if (dup2(ds[0], 0) == -1) exit(255);
1403         execv(shellPath, shellArgv);
1405         // Never reached unless execv fails
1406         exit(255);
1407     } else {
1408         // Parent process
1409         if (close(ds[0]) == -1) return -1;
1411         // Send input to execute to the child process
1412         [input appendString:@"\n"];
1413         int bytes = [input lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
1415         if (write(ds[1], [input UTF8String], bytes) != bytes) return -1;
1416         if (close(ds[1]) == -1) return -1;
1417     }
1419     return pid;