Reorder preferences panel
[MacVim.git] / src / MacVim / MMAppController.m
blobd6fdaf8dabc30c139bb8501e79a12f3e05cd47ad
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 @end
100 @interface MMAppController (Private)
101 - (MMVimController *)topmostVimController;
102 - (int)launchVimProcessWithArguments:(NSArray *)args;
103 - (NSArray *)filterFilesAndNotify:(NSArray *)files;
104 - (NSArray *)filterOpenFiles:(NSArray *)filenames
105                openFilesDict:(NSDictionary **)openFiles;
106 #if MM_HANDLE_XCODE_MOD_EVENT
107 - (void)handleXcodeModEvent:(NSAppleEventDescriptor *)event
108                  replyEvent:(NSAppleEventDescriptor *)reply;
109 #endif
110 - (int)findLaunchingProcessWithoutArguments;
111 - (MMVimController *)findUnusedEditor;
112 - (NSMutableDictionary *)extractArgumentsFromOdocEvent:
113     (NSAppleEventDescriptor *)desc;
114 - (void)scheduleVimControllerPreloadAfterDelay:(NSTimeInterval)delay;
115 - (void)cancelVimControllerPreloadRequests;
116 - (void)preloadVimController:(id)sender;
117 - (int)maxPreloadCacheSize;
118 - (MMVimController *)takeVimControllerFromCache;
119 - (void)clearPreloadCacheWithCount:(int)count;
120 - (void)rebuildPreloadCache;
121 - (NSDate *)rcFilesModificationDate;
122 - (BOOL)openVimControllerWithArguments:(NSDictionary *)arguments;
123 - (void)activateWhenNextWindowOpens;
124 - (void)startWatchingVimDir;
125 - (void)stopWatchingVimDir;
126 - (void)handleFSEvent;
128 #ifdef MM_ENABLE_PLUGINS
129 - (void)removePlugInMenu;
130 - (void)addPlugInMenuToMenu:(NSMenu *)mainMenu;
131 #endif
132 @end
136 #if (MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_4)
137     static void
138 fsEventCallback(ConstFSEventStreamRef streamRef,
139                 void *clientCallBackInfo,
140                 size_t numEvents,
141                 void *eventPaths,
142                 const FSEventStreamEventFlags eventFlags[],
143                 const FSEventStreamEventId eventIds[])
145     [[MMAppController sharedInstance] handleFSEvent];
147 #endif
149 @implementation MMAppController
151 + (void)initialize
153     NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:
154         [NSNumber numberWithBool:NO],   MMNoWindowKey,
155         [NSNumber numberWithInt:64],    MMTabMinWidthKey,
156         [NSNumber numberWithInt:6*64],  MMTabMaxWidthKey,
157         [NSNumber numberWithInt:132],   MMTabOptimumWidthKey,
158         [NSNumber numberWithInt:2],     MMTextInsetLeftKey,
159         [NSNumber numberWithInt:1],     MMTextInsetRightKey,
160         [NSNumber numberWithInt:1],     MMTextInsetTopKey,
161         [NSNumber numberWithInt:1],     MMTextInsetBottomKey,
162         [NSNumber numberWithBool:NO],   MMTerminateAfterLastWindowClosedKey,
163         @"MMTypesetter",                MMTypesetterKey,
164         [NSNumber numberWithFloat:1],   MMCellWidthMultiplierKey,
165         [NSNumber numberWithFloat:-1],  MMBaselineOffsetKey,
166         [NSNumber numberWithBool:YES],  MMTranslateCtrlClickKey,
167         [NSNumber numberWithInt:0],     MMOpenInCurrentWindowKey,
168         [NSNumber numberWithBool:NO],   MMNoFontSubstitutionKey,
169         [NSNumber numberWithBool:YES],  MMLoginShellKey,
170         [NSNumber numberWithBool:NO],   MMAtsuiRendererKey,
171         [NSNumber numberWithInt:MMUntitledWindowAlways],
172                                         MMUntitledWindowKey,
173         [NSNumber numberWithBool:NO],   MMTexturedWindowKey,
174         [NSNumber numberWithBool:NO],   MMZoomBothKey,
175         @"",                            MMLoginShellCommandKey,
176         @"",                            MMLoginShellArgumentKey,
177         [NSNumber numberWithBool:YES],  MMDialogsTrackPwdKey,
178 #ifdef MM_ENABLE_PLUGINS
179         [NSNumber numberWithBool:YES],  MMShowLeftPlugInContainerKey,
180 #endif
181         [NSNumber numberWithInt:3],     MMOpenLayoutKey,
182         [NSNumber numberWithBool:NO],   MMVerticalSplitKey,
183         [NSNumber numberWithInt:0],     MMPreloadCacheSizeKey,
184         nil];
186     [[NSUserDefaults standardUserDefaults] registerDefaults:dict];
188     NSArray *types = [NSArray arrayWithObject:NSStringPboardType];
189     [NSApp registerServicesMenuSendTypes:types returnTypes:types];
191     // NOTE: Set the current directory to user's home directory, otherwise it
192     // will default to the root directory.  (This matters since new Vim
193     // processes inherit MacVim's environment variables.)
194     [[NSFileManager defaultManager] changeCurrentDirectoryPath:
195             NSHomeDirectory()];
198 - (id)init
200     if (!(self = [super init])) return nil;
202     fontContainerRef = loadFonts();
204     vimControllers = [NSMutableArray new];
205     cachedVimControllers = [NSMutableArray new];
206     preloadPid = -1;
207     pidArguments = [NSMutableDictionary new];
209 #ifdef MM_ENABLE_PLUGINS
210     NSString *plugInTitle = NSLocalizedString(@"Plug-In",
211                                               @"Plug-In menu title");
212     plugInMenuItem = [[NSMenuItem alloc] initWithTitle:plugInTitle
213                                                 action:NULL
214                                          keyEquivalent:@""];
215     NSMenu *submenu = [[NSMenu alloc] initWithTitle:plugInTitle];
216     [plugInMenuItem setSubmenu:submenu];
217     [submenu release];
218 #endif
220     // NOTE: Do not use the default connection since the Logitech Control
221     // Center (LCC) input manager steals and this would cause MacVim to
222     // never open any windows.  (This is a bug in LCC but since they are
223     // unlikely to fix it, we graciously give them the default connection.)
224     connection = [[NSConnection alloc] initWithReceivePort:[NSPort port]
225                                                   sendPort:nil];
226     [connection setRootObject:self];
227     [connection setRequestTimeout:MMRequestTimeout];
228     [connection setReplyTimeout:MMReplyTimeout];
230     // NOTE: When the user is resizing the window the AppKit puts the run
231     // loop in event tracking mode.  Unless the connection listens to
232     // request in this mode, live resizing won't work.
233     [connection addRequestMode:NSEventTrackingRunLoopMode];
235     // NOTE!  If the name of the connection changes here it must also be
236     // updated in MMBackend.m.
237     NSString *name = [NSString stringWithFormat:@"%@-connection",
238              [[NSBundle mainBundle] bundlePath]];
239     //NSLog(@"Registering connection with name '%@'", name);
240     if (![connection registerName:name]) {
241         NSLog(@"FATAL ERROR: Failed to register connection with name '%@'",
242                 name);
243         [connection release];  connection = nil;
244     }
246     return self;
249 - (void)dealloc
251     //NSLog(@"MMAppController dealloc");
253     [connection release];  connection = nil;
254     [pidArguments release];  pidArguments = nil;
255     [vimControllers release];  vimControllers = nil;
256     [cachedVimControllers release];  cachedVimControllers = nil;
257     [openSelectionString release];  openSelectionString = nil;
258     [recentFilesMenuItem release];  recentFilesMenuItem = nil;
259     [defaultMainMenu release];  defaultMainMenu = nil;
260 #ifdef MM_ENABLE_PLUGINS
261     [plugInMenuItem release];  plugInMenuItem = nil;
262 #endif
263     [appMenuItemTemplate release];  appMenuItemTemplate = nil;
265     [super dealloc];
268 - (void)applicationWillFinishLaunching:(NSNotification *)notification
270     // Remember the default menu so that it can be restored if the user closes
271     // all editor windows.
272     defaultMainMenu = [[NSApp mainMenu] retain];
274     // Store a copy of the default app menu so we can use this as a template
275     // for all other menus.  We make a copy here because the "Services" menu
276     // will not yet have been populated at this time.  If we don't we get
277     // problems trying to set key equivalents later on because they might clash
278     // with items on the "Services" menu.
279     appMenuItemTemplate = [defaultMainMenu itemAtIndex:0];
280     appMenuItemTemplate = [appMenuItemTemplate copy];
282     // Set up the "Open Recent" menu. See
283     //   http://lapcatsoftware.com/blog/2007/07/10/
284     //     working-without-a-nib-part-5-open-recent-menu/
285     // and
286     //   http://www.cocoabuilder.com/archive/message/cocoa/2007/8/15/187793
287     // for more information.
288     //
289     // The menu itself is created in MainMenu.nib but we still seem to have to
290     // hack around a bit to get it to work.  (This has to be done in
291     // applicationWillFinishLaunching at the latest, otherwise it doesn't
292     // work.)
293     NSMenu *fileMenu = [defaultMainMenu findFileMenu];
294     if (fileMenu) {
295         int idx = [fileMenu indexOfItemWithAction:@selector(fileOpen:)];
296         if (idx >= 0 && idx+1 < [fileMenu numberOfItems])
298         recentFilesMenuItem = [fileMenu itemWithTitle:@"Open Recent"];
299         [[recentFilesMenuItem submenu] performSelector:@selector(_setMenuName:)
300                                         withObject:@"NSRecentDocumentsMenu"];
302         // Note: The "Recent Files" menu must be moved around since there is no
303         // -[NSApp setRecentFilesMenu:] method.  We keep a reference to it to
304         // facilitate this move (see setMainMenu: below).
305         [recentFilesMenuItem retain];
306     }
308 #if MM_HANDLE_XCODE_MOD_EVENT
309     [[NSAppleEventManager sharedAppleEventManager]
310             setEventHandler:self
311                 andSelector:@selector(handleXcodeModEvent:replyEvent:)
312               forEventClass:'KAHL'
313                  andEventID:'MOD '];
314 #endif
317 - (void)applicationDidFinishLaunching:(NSNotification *)notification
319     [NSApp setServicesProvider:self];
320 #ifdef MM_ENABLE_PLUGINS
321     [[MMPlugInManager sharedManager] loadAllPlugIns];
322 #endif
324     if ([self maxPreloadCacheSize] > 0) {
325         [self scheduleVimControllerPreloadAfterDelay:2];
326         [self startWatchingVimDir];
327     }
330 - (BOOL)applicationShouldOpenUntitledFile:(NSApplication *)sender
332     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
333     NSAppleEventManager *aem = [NSAppleEventManager sharedAppleEventManager];
334     NSAppleEventDescriptor *desc = [aem currentAppleEvent];
336     // The user default MMUntitledWindow can be set to control whether an
337     // untitled window should open on 'Open' and 'Reopen' events.
338     int untitledWindowFlag = [ud integerForKey:MMUntitledWindowKey];
339     if ([desc eventID] == kAEOpenApplication
340             && (untitledWindowFlag & MMUntitledWindowOnOpen) == 0)
341         return NO;
342     else if ([desc eventID] == kAEReopenApplication
343             && (untitledWindowFlag & MMUntitledWindowOnReopen) == 0)
344         return NO;
346     // When a process is started from the command line, the 'Open' event will
347     // contain a parameter to surpress the opening of an untitled window.
348     desc = [desc paramDescriptorForKeyword:keyAEPropData];
349     desc = [desc paramDescriptorForKeyword:keyMMUntitledWindow];
350     if (desc && ![desc booleanValue])
351         return NO;
353     // Never open an untitled window if there is at least one open window or if
354     // there are processes that are currently launching.
355     if ([vimControllers count] > 0 || [pidArguments count] > 0)
356         return NO;
358     // NOTE!  This way it possible to start the app with the command-line
359     // argument '-nowindow yes' and no window will be opened by default.
360     return ![ud boolForKey:MMNoWindowKey];
363 - (BOOL)applicationOpenUntitledFile:(NSApplication *)sender
365     [self newWindow:self];
366     return YES;
369 - (void)application:(NSApplication *)sender openFiles:(NSArray *)filenames
371     // Opening files works like this:
372     //  a) extract ODB/Xcode/Spotlight parameters from the current Apple event
373     //  b) filter out any already open files
374     //  c) open any remaining files
375     //
376     // A file is opened in an untitled window if there is one (it may be
377     // currently launching, or it may already be visible), otherwise a new
378     // window is opened.
379     //
380     // Each launching Vim process has a dictionary of arguments that are passed
381     // to the process when in checks in (via connectBackend:pid:).  The
382     // arguments for each launching process can be looked up by its PID (in the
383     // pidArguments dictionary).
385     if (!(filenames && [filenames count] > 0))
386         return;
388     //
389     // a) Extract ODB/Xcode/Spotlight parameters from the current Apple event
390     //
391     NSMutableDictionary *arguments = [self extractArgumentsFromOdocEvent:
392             [[NSAppleEventManager sharedAppleEventManager] currentAppleEvent]];
394     //
395     // b) Filter out any already open files
396     //
397     NSString *firstFile = [filenames objectAtIndex:0];
398     MMVimController *firstController = nil;
399     NSDictionary *openFilesDict = nil;
400     filenames = [self filterOpenFiles:filenames openFilesDict:&openFilesDict];
402     // Pass arguments to vim controllers that had files open.
403     id key;
404     NSEnumerator *e = [openFilesDict keyEnumerator];
406     // (Indicate that we do not wish to open any files at the moment.)
407     [arguments setObject:[NSNumber numberWithBool:YES] forKey:@"dontOpen"];
409     while ((key = [e nextObject])) {
410         NSArray *files = [openFilesDict objectForKey:key];
411         [arguments setObject:files forKey:@"filenames"];
413         MMVimController *vc = [key pointerValue];
414         [vc passArguments:arguments];
416         // If this controller holds the first file, then remember it for later.
417         if ([files containsObject:firstFile])
418             firstController = vc;
419     }
421     if ([filenames count] == 0) {
422         // Raise the window containing the first file that was already open,
423         // and make sure that the tab containing that file is selected.  Only
424         // do this when there are no more files to open, otherwise sometimes
425         // the window with 'firstFile' will be raised, other times it might be
426         // the window that will open with the files in the 'filenames' array.
427         firstFile = [firstFile stringByEscapingSpecialFilenameCharacters];
428         NSString *input = [NSString stringWithFormat:@"<C-\\><C-N>"
429                 ":let oldswb=&swb|let &swb=\"useopen,usetab\"|"
430                 "tab sb %@|let &swb=oldswb|unl oldswb|"
431                 "cal foreground()|redr|f<CR>", firstFile];
433         [firstController addVimInput:input];
435         [NSApp replyToOpenOrPrint:NSApplicationDelegateReplySuccess];
436         return;
437     }
439     // Add filenames to "Recent Files" menu, unless they are being edited
440     // remotely (using ODB).
441     if ([arguments objectForKey:@"remoteID"] == nil) {
442         [[NSDocumentController sharedDocumentController]
443                 noteNewRecentFilePaths:filenames];
444     }
446     //
447     // c) Open any remaining files
448     //
449     MMVimController *vc;
450     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
451     BOOL openInCurrentWindow = [ud boolForKey:MMOpenInCurrentWindowKey];
453     // The meaning of "layout" is defined by the WIN_* defines in main.c.
454     int layout = [ud integerForKey:MMOpenLayoutKey];
455     BOOL splitVert = [ud boolForKey:MMVerticalSplitKey];
456     if (splitVert && MMLayoutHorizontalSplit == layout)
457         layout = MMLayoutVerticalSplit;
458     if (layout < 0 || (layout > MMLayoutTabs && openInCurrentWindow))
459         layout = MMLayoutTabs;
461     [arguments setObject:[NSNumber numberWithInt:layout] forKey:@"layout"];
462     [arguments setObject:filenames forKey:@"filenames"];
463     // (Indicate that files should be opened from now on.)
464     [arguments setObject:[NSNumber numberWithBool:NO] forKey:@"dontOpen"];
466     if (openInCurrentWindow && (vc = [self topmostVimController])) {
467         // Open files in an already open window.
468         [[[vc windowController] window] makeKeyAndOrderFront:self];
469         [vc passArguments:arguments];
470         [NSApp replyToOpenOrPrint:NSApplicationDelegateReplySuccess];
471         return;
472     }
474     BOOL openOk = YES;
475     int numFiles = [filenames count];
476     if (MMLayoutWindows == layout && numFiles > 1) {
477         // Open one file at a time in a new window, but don't open too many at
478         // once (at most cap+1 windows will open).  If the user has increased
479         // the preload cache size we'll take that as a hint that more windows
480         // should be able to open at once.
481         int cap = [self maxPreloadCacheSize] - 1;
482         if (cap < 4) cap = 4;
483         if (cap > numFiles) cap = numFiles;
485         int i;
486         for (i = 0; i < cap; ++i) {
487             NSArray *a = [NSArray arrayWithObject:[filenames objectAtIndex:i]];
488             [arguments setObject:a forKey:@"filenames"];
490             // NOTE: We have to copy the args since we'll mutate them in the
491             // next loop and the below call may retain the arguments while
492             // waiting for a process to start.
493             NSDictionary *args = [[arguments copy] autorelease];
495             openOk = [self openVimControllerWithArguments:args];
496             if (!openOk) break;
497         }
499         // Open remaining files in tabs in a new window.
500         if (openOk && numFiles > cap) {
501             NSRange range = { i, numFiles-cap };
502             NSArray *a = [filenames subarrayWithRange:range];
503             [arguments setObject:a forKey:@"filenames"];
504             [arguments setObject:[NSNumber numberWithInt:MMLayoutTabs]
505                           forKey:@"layout"];
507             openOk = [self openVimControllerWithArguments:arguments];
508         }
509     } else {
510         // Open all files at once.
511         openOk = [self openVimControllerWithArguments:arguments];
512     }
514     if (openOk) {
515         [NSApp replyToOpenOrPrint:NSApplicationDelegateReplySuccess];
516     } else {
517         // TODO: Notify user of failure?
518         [NSApp replyToOpenOrPrint:NSApplicationDelegateReplyFailure];
519     }
522 - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender
524     return [[NSUserDefaults standardUserDefaults]
525             boolForKey:MMTerminateAfterLastWindowClosedKey];
528 - (NSApplicationTerminateReply)applicationShouldTerminate:
529     (NSApplication *)sender
531     // TODO: Follow Apple's guidelines for 'Graceful Application Termination'
532     // (in particular, allow user to review changes and save).
533     int reply = NSTerminateNow;
534     BOOL modifiedBuffers = NO;
536     // Go through windows, checking for modified buffers.  (Each Vim process
537     // tells MacVim when any buffer has been modified and MacVim sets the
538     // 'documentEdited' flag of the window correspondingly.)
539     NSEnumerator *e = [[NSApp windows] objectEnumerator];
540     id window;
541     while ((window = [e nextObject])) {
542         if ([window isDocumentEdited]) {
543             modifiedBuffers = YES;
544             break;
545         }
546     }
548     if (modifiedBuffers) {
549         NSAlert *alert = [[NSAlert alloc] init];
550         [alert setAlertStyle:NSWarningAlertStyle];
551         [alert addButtonWithTitle:NSLocalizedString(@"Quit",
552                 @"Dialog button")];
553         [alert addButtonWithTitle:NSLocalizedString(@"Cancel",
554                 @"Dialog button")];
555         [alert setMessageText:NSLocalizedString(@"Quit without saving?",
556                 @"Quit dialog with changed buffers, title")];
557         [alert setInformativeText:NSLocalizedString(
558                 @"There are modified buffers, "
559                 "if you quit now all changes will be lost.  Quit anyway?",
560                 @"Quit dialog with changed buffers, text")];
562         if ([alert runModal] != NSAlertFirstButtonReturn)
563             reply = NSTerminateCancel;
565         [alert release];
566     } else {
567         // No unmodified buffers, but give a warning if there are multiple
568         // windows and/or tabs open.
569         int numWindows = [vimControllers count];
570         int numTabs = 0;
572         // Count the number of open tabs
573         e = [vimControllers objectEnumerator];
574         id vc;
575         while ((vc = [e nextObject])) {
576             NSString *eval = [vc evaluateVimExpression:@"tabpagenr('$')"];
577             if (eval) {
578                 int count = [eval intValue];
579                 if (count > 0 && count < INT_MAX)
580                     numTabs += count;
581             }
582         }
584         if (numWindows > 1 || numTabs > 1) {
585             NSAlert *alert = [[NSAlert alloc] init];
586             [alert setAlertStyle:NSWarningAlertStyle];
587             [alert addButtonWithTitle:NSLocalizedString(@"Quit",
588                     @"Dialog button")];
589             [alert addButtonWithTitle:NSLocalizedString(@"Cancel",
590                     @"Dialog button")];
591             [alert setMessageText:NSLocalizedString(
592                     @"Are you sure you want to quit MacVim?",
593                     @"Quit dialog with no changed buffers, title")];
595             NSString *info = nil;
596             if (numWindows > 1) {
597                 if (numTabs > numWindows)
598                     info = [NSString stringWithFormat:NSLocalizedString(
599                             @"There are %d windows open in MacVim, with a "
600                             "total of %d tabs. Do you want to quit anyway?",
601                             @"Quit dialog with no changed buffers, text"),
602                          numWindows, numTabs];
603                 else
604                     info = [NSString stringWithFormat:NSLocalizedString(
605                             @"There are %d windows open in MacVim. "
606                             "Do you want to quit anyway?",
607                             @"Quit dialog with no changed buffers, text"),
608                         numWindows];
610             } else {
611                 info = [NSString stringWithFormat:NSLocalizedString(
612                         @"There are %d tabs open in MacVim. "
613                         "Do you want to quit anyway?",
614                         @"Quit dialog with no changed buffers, text"), 
615                      numTabs];
616             }
618             [alert setInformativeText:info];
620             if ([alert runModal] != NSAlertFirstButtonReturn)
621                 reply = NSTerminateCancel;
623             [alert release];
624         }
625     }
628     // Tell all Vim processes to terminate now (otherwise they'll leave swap
629     // files behind).
630     if (NSTerminateNow == reply) {
631         e = [vimControllers objectEnumerator];
632         id vc;
633         while ((vc = [e nextObject]))
634             [vc sendMessage:TerminateNowMsgID data:nil];
636         e = [cachedVimControllers objectEnumerator];
637         while ((vc = [e nextObject]))
638             [vc sendMessage:TerminateNowMsgID data:nil];
640         // Give Vim processes a chance to terminate before MacVim.  If they
641         // haven't terminated by the time applicationWillTerminate: is sent,
642         // they may be forced to quit (see below).
643         usleep(MMTerminationSleepPeriod);
644     }
646     return reply;
649 - (void)applicationWillTerminate:(NSNotification *)notification
651     [self stopWatchingVimDir];
653 #ifdef MM_ENABLE_PLUGINS
654     [[MMPlugInManager sharedManager] unloadAllPlugIns];
655 #endif
657 #if MM_HANDLE_XCODE_MOD_EVENT
658     [[NSAppleEventManager sharedAppleEventManager]
659             removeEventHandlerForEventClass:'KAHL'
660                                  andEventID:'MOD '];
661 #endif
663     // This will invalidate all connections (since they were spawned from this
664     // connection).
665     [connection invalidate];
667     // Send a SIGINT to all running Vim processes, so that they are sure to
668     // receive the connectionDidDie: notification (a process has to be checking
669     // the run-loop for this to happen).
670     unsigned i, count = [vimControllers count];
671     for (i = 0; i < count; ++i) {
672         MMVimController *controller = [vimControllers objectAtIndex:i];
673         int pid = [controller pid];
674         if (-1 != pid)
675             kill(pid, SIGINT);
676     }
678     if (fontContainerRef) {
679         ATSFontDeactivate(fontContainerRef, NULL, kATSOptionFlagsDefault);
680         fontContainerRef = 0;
681     }
683     [NSApp setDelegate:nil];
686 + (MMAppController *)sharedInstance
688     // Note: The app controller is a singleton which is instantiated in
689     // MainMenu.nib where it is also connected as the delegate of NSApp.
690     id delegate = [NSApp delegate];
691     return [delegate isKindOfClass:self] ? (MMAppController*)delegate : nil;
694 - (NSMenu *)defaultMainMenu
696     return defaultMainMenu;
699 - (NSMenuItem *)appMenuItemTemplate
701     return appMenuItemTemplate;
704 - (void)removeVimController:(id)controller
706     int idx = [vimControllers indexOfObject:controller];
707     if (NSNotFound == idx)
708         return;
710     [controller cleanup];
712     [vimControllers removeObjectAtIndex:idx];
714     if (![vimControllers count]) {
715         // The last editor window just closed so restore the main menu back to
716         // its default state (which is defined in MainMenu.nib).
717         [self setMainMenu:defaultMainMenu];
718     }
721 - (void)windowControllerWillOpen:(MMWindowController *)windowController
723     NSPoint topLeft = NSZeroPoint;
724     NSWindow *topWin = [[[self topmostVimController] windowController] window];
725     NSWindow *win = [windowController window];
727     if (!win) return;
729     // If there is a window belonging to a Vim process, cascade from it,
730     // otherwise use the autosaved window position (if any).
731     if (topWin) {
732         NSRect frame = [topWin frame];
733         topLeft = NSMakePoint(frame.origin.x, NSMaxY(frame));
734     } else {
735         NSString *topLeftString = [[NSUserDefaults standardUserDefaults]
736             stringForKey:MMTopLeftPointKey];
737         if (topLeftString)
738             topLeft = NSPointFromString(topLeftString);
739     }
741     if (!NSEqualPoints(topLeft, NSZeroPoint)) {
742         if (topWin)
743             topLeft = [win cascadeTopLeftFromPoint:topLeft];
745         [win setFrameTopLeftPoint:topLeft];
746     }
748     if (1 == [vimControllers count]) {
749         // The first window autosaves its position.  (The autosaving
750         // features of Cocoa are not used because we need more control over
751         // what is autosaved and when it is restored.)
752         [windowController setWindowAutosaveKey:MMTopLeftPointKey];
753     }
755     if (openSelectionString) {
756         // TODO: Pass this as a parameter instead!  Get rid of
757         // 'openSelectionString' etc.
758         //
759         // There is some text to paste into this window as a result of the
760         // services menu "Open selection ..." being used.
761         [[windowController vimController] dropString:openSelectionString];
762         [openSelectionString release];
763         openSelectionString = nil;
764     }
766     if (shouldActivateWhenNextWindowOpens) {
767         [NSApp activateIgnoringOtherApps:YES];
768         shouldActivateWhenNextWindowOpens = NO;
769     }
772 - (void)setMainMenu:(NSMenu *)mainMenu
774     if ([NSApp mainMenu] == mainMenu) return;
776     // If the new menu has a "Recent Files" dummy item, then swap the real item
777     // for the dummy.  We are forced to do this since Cocoa initializes the
778     // "Recent Files" menu and there is no way to simply point Cocoa to a new
779     // item each time the menus are swapped.
780     NSMenu *fileMenu = [mainMenu findFileMenu];
781     if (recentFilesMenuItem && fileMenu) {
782         int dummyIdx =
783                 [fileMenu indexOfItemWithAction:@selector(recentFilesDummy:)];
784         if (dummyIdx >= 0) {
785             NSMenuItem *dummyItem = [[fileMenu itemAtIndex:dummyIdx] retain];
786             [fileMenu removeItemAtIndex:dummyIdx];
788             NSMenu *recentFilesParentMenu = [recentFilesMenuItem menu];
789             int idx = [recentFilesParentMenu indexOfItem:recentFilesMenuItem];
790             if (idx >= 0) {
791                 [[recentFilesMenuItem retain] autorelease];
792                 [recentFilesParentMenu removeItemAtIndex:idx];
793                 [recentFilesParentMenu insertItem:dummyItem atIndex:idx];
794             }
796             [fileMenu insertItem:recentFilesMenuItem atIndex:dummyIdx];
797             [dummyItem release];
798         }
799     }
801     // Now set the new menu.  Notice that we keep one menu for each editor
802     // window since each editor can have its own set of menus.  When swapping
803     // menus we have to tell Cocoa where the new "MacVim", "Windows", and
804     // "Services" menu are.
805     [NSApp setMainMenu:mainMenu];
807     // Setting the "MacVim" (or "Application") menu ensures that it is typeset
808     // in boldface.  (The setAppleMenu: method used to be public but is now
809     // private so this will have to be considered a bit of a hack!)
810     NSMenu *appMenu = [mainMenu findApplicationMenu];
811     [NSApp performSelector:@selector(setAppleMenu:) withObject:appMenu];
813     NSMenu *servicesMenu = [mainMenu findServicesMenu];
814     [NSApp setServicesMenu:servicesMenu];
816     NSMenu *windowsMenu = [mainMenu findWindowsMenu];
817     if (windowsMenu) {
818         // Cocoa isn't clever enough to get rid of items it has added to the
819         // "Windows" menu so we have to do it ourselves otherwise there will be
820         // multiple menu items for each window in the "Windows" menu.
821         //   This code assumes that the only items Cocoa add are ones which
822         // send off the action makeKeyAndOrderFront:.  (Cocoa will not add
823         // another separator item if the last item on the "Windows" menu
824         // already is a separator, so we needen't worry about separators.)
825         int i, count = [windowsMenu numberOfItems];
826         for (i = count-1; i >= 0; --i) {
827             NSMenuItem *item = [windowsMenu itemAtIndex:i];
828             if ([item action] == @selector(makeKeyAndOrderFront:))
829                 [windowsMenu removeItem:item];
830         }
831     }
832     [NSApp setWindowsMenu:windowsMenu];
834 #ifdef MM_ENABLE_PLUGINS
835     // Move plugin menu from old to new main menu.
836     [self removePlugInMenu];
837     [self addPlugInMenuToMenu:mainMenu];
838 #endif
841 - (NSArray *)filterOpenFiles:(NSArray *)filenames
843     return [self filterOpenFiles:filenames openFilesDict:nil];
846 #ifdef MM_ENABLE_PLUGINS
847 - (void)addItemToPlugInMenu:(NSMenuItem *)item
849     NSMenu *menu = [plugInMenuItem submenu];
850     [menu addItem:item];
851     if ([menu numberOfItems] == 1)
852         [self addPlugInMenuToMenu:[NSApp mainMenu]];
855 - (void)removeItemFromPlugInMenu:(NSMenuItem *)item
857     NSMenu *menu = [plugInMenuItem submenu];
858     [menu removeItem:item];
859     if ([menu numberOfItems] == 0)
860         [self removePlugInMenu];
862 #endif
864 - (IBAction)newWindow:(id)sender
866     // A cached controller requires no loading times and results in the new
867     // window popping up instantaneously.  If the cache is empty it may take
868     // 1-2 seconds to start a new Vim process.
869     MMVimController *vc = [self takeVimControllerFromCache];
870     if (vc) {
871         [[vc backendProxy] acknowledgeConnection];
872     } else {
873         [self launchVimProcessWithArguments:nil];
874     }
877 - (IBAction)newWindowAndActivate:(id)sender
879     [self activateWhenNextWindowOpens];
880     [self newWindow:sender];
883 - (IBAction)fileOpen:(id)sender
885     NSString *dir = nil;
886     BOOL trackPwd = [[NSUserDefaults standardUserDefaults]
887             boolForKey:MMDialogsTrackPwdKey];
888     if (trackPwd) {
889         MMVimController *vc = [self keyVimController];
890         if (vc) dir = [[vc vimState] objectForKey:@"pwd"];
891     }
893     NSOpenPanel *panel = [NSOpenPanel openPanel];
894     [panel setAllowsMultipleSelection:YES];
895     [panel setAccessoryView:openPanelAccessoryView()];
897     int result = [panel runModalForDirectory:dir file:nil types:nil];
898     if (NSOKButton == result)
899         [self application:NSApp openFiles:[panel filenames]];
902 - (IBAction)selectNextWindow:(id)sender
904     unsigned i, count = [vimControllers count];
905     if (!count) return;
907     NSWindow *keyWindow = [NSApp keyWindow];
908     for (i = 0; i < count; ++i) {
909         MMVimController *vc = [vimControllers objectAtIndex:i];
910         if ([[[vc windowController] window] isEqual:keyWindow])
911             break;
912     }
914     if (i < count) {
915         if (++i >= count)
916             i = 0;
917         MMVimController *vc = [vimControllers objectAtIndex:i];
918         [[vc windowController] showWindow:self];
919     }
922 - (IBAction)selectPreviousWindow:(id)sender
924     unsigned i, count = [vimControllers count];
925     if (!count) return;
927     NSWindow *keyWindow = [NSApp keyWindow];
928     for (i = 0; i < count; ++i) {
929         MMVimController *vc = [vimControllers objectAtIndex:i];
930         if ([[[vc windowController] window] isEqual:keyWindow])
931             break;
932     }
934     if (i < count) {
935         if (i > 0) {
936             --i;
937         } else {
938             i = count - 1;
939         }
940         MMVimController *vc = [vimControllers objectAtIndex:i];
941         [[vc windowController] showWindow:self];
942     }
945 - (IBAction)orderFrontPreferencePanel:(id)sender
947     [[MMPreferenceController sharedPrefsWindowController] showWindow:self];
950 - (IBAction)openWebsite:(id)sender
952     [[NSWorkspace sharedWorkspace] openURL:
953             [NSURL URLWithString:MMWebsiteString]];
956 - (IBAction)showVimHelp:(id)sender
958     // Open a new window with the help window maximized.
959     [self launchVimProcessWithArguments:[NSArray arrayWithObjects:
960             @"-c", @":h gui_mac", @"-c", @":res", nil]];
963 - (IBAction)zoomAll:(id)sender
965     [NSApp makeWindowsPerform:@selector(performZoom:) inOrder:YES];
968 - (IBAction)atsuiButtonClicked:(id)sender
970     // This action is called when the user clicks the "use ATSUI renderer"
971     // button in the advanced preferences pane.
972     [self rebuildPreloadCache];
975 - (IBAction)loginShellButtonClicked:(id)sender
977     // This action is called when the user clicks the "use login shell" button
978     // in the advanced preferences pane.
979     [self rebuildPreloadCache];
982 - (IBAction)quickstartButtonClicked:(id)sender
984     if ([self maxPreloadCacheSize] > 0) {
985         [self scheduleVimControllerPreloadAfterDelay:1.0];
986         [self startWatchingVimDir];
987     } else {
988         [self cancelVimControllerPreloadRequests];
989         [self clearPreloadCacheWithCount:-1];
990         [self stopWatchingVimDir];
991     }
994 - (byref id <MMFrontendProtocol>)
995     connectBackend:(byref in id <MMBackendProtocol>)backend
996                pid:(int)pid
998     //NSLog(@"Connect backend (pid=%d)", pid);
999     NSNumber *pidKey = [NSNumber numberWithInt:pid];
1000     MMVimController *vc = nil;
1002     @try {
1003         [(NSDistantObject*)backend
1004                 setProtocolForProxy:@protocol(MMBackendProtocol)];
1006         vc = [[[MMVimController alloc] initWithBackend:backend pid:pid]
1007                 autorelease];
1009         if (preloadPid == pid) {
1010             // This backend was preloaded, so add it to the cache and schedule
1011             // another vim process to be preloaded.
1012             preloadPid = -1;
1013             [vc setIsPreloading:YES];
1014             [cachedVimControllers addObject:vc];
1015             [self scheduleVimControllerPreloadAfterDelay:1];
1017             return vc;
1018         }
1020         [vimControllers addObject:vc];
1022         id args = [pidArguments objectForKey:pidKey];
1023         if (args && [NSNull null] != args)
1024             [vc passArguments:args];
1026         // HACK!  MacVim does not get activated if it is launched from the
1027         // terminal, so we forcibly activate here unless it is an untitled
1028         // window opening.  Untitled windows are treated differently, else
1029         // MacVim would steal the focus if another app was activated while the
1030         // untitled window was loading.
1031         if (!args || args != [NSNull null])
1032             [NSApp activateIgnoringOtherApps:YES];
1034         if (args)
1035             [pidArguments removeObjectForKey:pidKey];
1037         return vc;
1038     }
1040     @catch (NSException *e) {
1041         NSLog(@"Exception caught in %s: \"%@\"", _cmd, e);
1043         if (vc)
1044             [vimControllers removeObject:vc];
1046         [pidArguments removeObjectForKey:pidKey];
1047     }
1049     return nil;
1052 - (NSArray *)serverList
1054     NSMutableArray *array = [NSMutableArray array];
1056     unsigned i, count = [vimControllers count];
1057     for (i = 0; i < count; ++i) {
1058         MMVimController *controller = [vimControllers objectAtIndex:i];
1059         if ([controller serverName])
1060             [array addObject:[controller serverName]];
1061     }
1063     return array;
1066 - (MMVimController *)keyVimController
1068     NSWindow *keyWindow = [NSApp keyWindow];
1069     if (keyWindow) {
1070         unsigned i, count = [vimControllers count];
1071         for (i = 0; i < count; ++i) {
1072             MMVimController *vc = [vimControllers objectAtIndex:i];
1073             if ([[[vc windowController] window] isEqual:keyWindow])
1074                 return vc;
1075         }
1076     }
1078     return nil;
1081 @end // MMAppController
1086 @implementation MMAppController (MMServices)
1088 - (void)openSelection:(NSPasteboard *)pboard userData:(NSString *)userData
1089                 error:(NSString **)error
1091     if (![[pboard types] containsObject:NSStringPboardType]) {
1092         NSLog(@"WARNING: Pasteboard contains no object of type "
1093                 "NSStringPboardType");
1094         return;
1095     }
1097     MMVimController *vc = [self topmostVimController];
1098     if (vc) {
1099         // Open a new tab first, since dropString: does not do this.
1100         [vc sendMessage:AddNewTabMsgID data:nil];
1101         [vc dropString:[pboard stringForType:NSStringPboardType]];
1102     } else {
1103         // NOTE: There is no window to paste the selection into, so save the
1104         // text, open a new window, and paste the text when the next window
1105         // opens.  (If this is called several times in a row, then all but the
1106         // last call might be ignored.)
1107         if (openSelectionString) [openSelectionString release];
1108         openSelectionString = [[pboard stringForType:NSStringPboardType] copy];
1110         [self newWindow:self];
1111     }
1114 - (void)openFile:(NSPasteboard *)pboard userData:(NSString *)userData
1115            error:(NSString **)error
1117     if (![[pboard types] containsObject:NSStringPboardType]) {
1118         NSLog(@"WARNING: Pasteboard contains no object of type "
1119                 "NSStringPboardType");
1120         return;
1121     }
1123     // TODO: Parse multiple filenames and create array with names.
1124     NSString *string = [pboard stringForType:NSStringPboardType];
1125     string = [string stringByTrimmingCharactersInSet:
1126             [NSCharacterSet whitespaceAndNewlineCharacterSet]];
1127     string = [string stringByStandardizingPath];
1129     NSArray *filenames = [self filterFilesAndNotify:
1130             [NSArray arrayWithObject:string]];
1131     if ([filenames count] > 0) {
1132         MMVimController *vc = nil;
1133         if (userData && [userData isEqual:@"Tab"])
1134             vc = [self topmostVimController];
1136         if (vc) {
1137             [vc dropFiles:filenames forceOpen:YES];
1138         } else {
1139             [self application:NSApp openFiles:filenames];
1140         }
1141     }
1144 @end // MMAppController (MMServices)
1149 @implementation MMAppController (Private)
1151 - (MMVimController *)topmostVimController
1153     // Find the topmost visible window which has an associated vim controller.
1154     NSEnumerator *e = [[NSApp orderedWindows] objectEnumerator];
1155     id window;
1156     while ((window = [e nextObject]) && [window isVisible]) {
1157         unsigned i, count = [vimControllers count];
1158         for (i = 0; i < count; ++i) {
1159             MMVimController *vc = [vimControllers objectAtIndex:i];
1160             if ([[[vc windowController] window] isEqual:window])
1161                 return vc;
1162         }
1163     }
1165     return nil;
1168 - (int)launchVimProcessWithArguments:(NSArray *)args
1170     int pid = -1;
1171     NSString *path = [[NSBundle mainBundle] pathForAuxiliaryExecutable:@"Vim"];
1173     if (!path) {
1174         NSLog(@"ERROR: Vim executable could not be found inside app bundle!");
1175         return -1;
1176     }
1178     NSArray *taskArgs = [NSArray arrayWithObjects:@"-g", @"-f", nil];
1179     if (args)
1180         taskArgs = [taskArgs arrayByAddingObjectsFromArray:args];
1182     BOOL useLoginShell = [[NSUserDefaults standardUserDefaults]
1183             boolForKey:MMLoginShellKey];
1184     if (useLoginShell) {
1185         // Run process with a login shell, roughly:
1186         //   echo "exec Vim -g -f args" | ARGV0=-`basename $SHELL` $SHELL [-l]
1187         pid = executeInLoginShell(path, taskArgs);
1188     } else {
1189         // Run process directly:
1190         //   Vim -g -f args
1191         NSTask *task = [NSTask launchedTaskWithLaunchPath:path
1192                                                 arguments:taskArgs];
1193         pid = task ? [task processIdentifier] : -1;
1194     }
1196     if (-1 != pid) {
1197         // NOTE: If the process has no arguments, then add a null argument to
1198         // the pidArguments dictionary.  This is later used to detect that a
1199         // process without arguments is being launched.
1200         if (!args)
1201             [pidArguments setObject:[NSNull null]
1202                              forKey:[NSNumber numberWithInt:pid]];
1203     } else {
1204         NSLog(@"WARNING: %s%@ failed (useLoginShell=%d)", _cmd, args,
1205                 useLoginShell);
1206     }
1208     return pid;
1211 - (NSArray *)filterFilesAndNotify:(NSArray *)filenames
1213     // Go trough 'filenames' array and make sure each file exists.  Present
1214     // warning dialog if some file was missing.
1216     NSString *firstMissingFile = nil;
1217     NSMutableArray *files = [NSMutableArray array];
1218     unsigned i, count = [filenames count];
1220     for (i = 0; i < count; ++i) {
1221         NSString *name = [filenames objectAtIndex:i];
1222         if ([[NSFileManager defaultManager] fileExistsAtPath:name]) {
1223             [files addObject:name];
1224         } else if (!firstMissingFile) {
1225             firstMissingFile = name;
1226         }
1227     }
1229     if (firstMissingFile) {
1230         NSAlert *alert = [[NSAlert alloc] init];
1231         [alert addButtonWithTitle:NSLocalizedString(@"OK",
1232                 @"Dialog button")];
1234         NSString *text;
1235         if ([files count] >= count-1) {
1236             [alert setMessageText:NSLocalizedString(@"File not found",
1237                     @"File not found dialog, title")];
1238             text = [NSString stringWithFormat:NSLocalizedString(
1239                     @"Could not open file with name %@.",
1240                     @"File not found dialog, text"), firstMissingFile];
1241         } else {
1242             [alert setMessageText:NSLocalizedString(@"Multiple files not found",
1243                     @"File not found dialog, title")];
1244             text = [NSString stringWithFormat:NSLocalizedString(
1245                     @"Could not open file with name %@, and %d other files.",
1246                     @"File not found dialog, text"),
1247                 firstMissingFile, count-[files count]-1];
1248         }
1250         [alert setInformativeText:text];
1251         [alert setAlertStyle:NSWarningAlertStyle];
1253         [alert runModal];
1254         [alert release];
1256         [NSApp replyToOpenOrPrint:NSApplicationDelegateReplyFailure];
1257     }
1259     return files;
1262 - (NSArray *)filterOpenFiles:(NSArray *)filenames
1263                openFilesDict:(NSDictionary **)openFiles
1265     // Filter out any files in the 'filenames' array that are open and return
1266     // all files that are not already open.  On return, the 'openFiles'
1267     // parameter (if non-nil) will point to a dictionary of open files, indexed
1268     // by Vim controller.
1270     NSMutableDictionary *dict = [NSMutableDictionary dictionary];
1271     NSMutableArray *files = [filenames mutableCopy];
1273     // TODO: Escape special characters in 'files'?
1274     NSString *expr = [NSString stringWithFormat:
1275             @"map([\"%@\"],\"bufloaded(v:val)\")",
1276             [files componentsJoinedByString:@"\",\""]];
1278     unsigned i, count = [vimControllers count];
1279     for (i = 0; i < count && [files count] > 0; ++i) {
1280         MMVimController *vc = [vimControllers objectAtIndex:i];
1282         // Query Vim for which files in the 'files' array are open.
1283         NSString *eval = [vc evaluateVimExpression:expr];
1284         if (!eval) continue;
1286         NSIndexSet *idxSet = [NSIndexSet indexSetWithVimList:eval];
1287         if ([idxSet count] > 0) {
1288             [dict setObject:[files objectsAtIndexes:idxSet]
1289                      forKey:[NSValue valueWithPointer:vc]];
1291             // Remove all the files that were open in this Vim process and
1292             // create a new expression to evaluate.
1293             [files removeObjectsAtIndexes:idxSet];
1294             expr = [NSString stringWithFormat:
1295                     @"map([\"%@\"],\"bufloaded(v:val)\")",
1296                     [files componentsJoinedByString:@"\",\""]];
1297         }
1298     }
1300     if (openFiles != nil)
1301         *openFiles = dict;
1303     return files;
1306 #if MM_HANDLE_XCODE_MOD_EVENT
1307 - (void)handleXcodeModEvent:(NSAppleEventDescriptor *)event
1308                  replyEvent:(NSAppleEventDescriptor *)reply
1310 #if 0
1311     // Xcode sends this event to query MacVim which open files have been
1312     // modified.
1313     NSLog(@"reply:%@", reply);
1314     NSLog(@"event:%@", event);
1316     NSEnumerator *e = [vimControllers objectEnumerator];
1317     id vc;
1318     while ((vc = [e nextObject])) {
1319         DescType type = [reply descriptorType];
1320         unsigned len = [[type data] length];
1321         NSMutableData *data = [NSMutableData data];
1323         [data appendBytes:&type length:sizeof(DescType)];
1324         [data appendBytes:&len length:sizeof(unsigned)];
1325         [data appendBytes:[reply data] length:len];
1327         [vc sendMessage:XcodeModMsgID data:data];
1328     }
1329 #endif
1331 #endif
1333 - (int)findLaunchingProcessWithoutArguments
1335     NSArray *keys = [pidArguments allKeysForObject:[NSNull null]];
1336     if ([keys count] > 0) {
1337         //NSLog(@"found launching process without arguments");
1338         return [[keys objectAtIndex:0] intValue];
1339     }
1341     return -1;
1344 - (MMVimController *)findUnusedEditor
1346     NSEnumerator *e = [vimControllers objectEnumerator];
1347     id vc;
1348     while ((vc = [e nextObject])) {
1349         if ([[[vc vimState] objectForKey:@"unusedEditor"] boolValue])
1350             return vc;
1351     }
1353     return nil;
1356 - (NSMutableDictionary *)extractArgumentsFromOdocEvent:
1357     (NSAppleEventDescriptor *)desc
1359     NSMutableDictionary *dict = [NSMutableDictionary dictionary];
1361     // 1. Extract ODB parameters (if any)
1362     NSAppleEventDescriptor *odbdesc = desc;
1363     if (![odbdesc paramDescriptorForKeyword:keyFileSender]) {
1364         // The ODB paramaters may hide inside the 'keyAEPropData' descriptor.
1365         odbdesc = [odbdesc paramDescriptorForKeyword:keyAEPropData];
1366         if (![odbdesc paramDescriptorForKeyword:keyFileSender])
1367             odbdesc = nil;
1368     }
1370     if (odbdesc) {
1371         NSAppleEventDescriptor *p =
1372                 [odbdesc paramDescriptorForKeyword:keyFileSender];
1373         if (p)
1374             [dict setObject:[NSNumber numberWithUnsignedInt:[p typeCodeValue]]
1375                      forKey:@"remoteID"];
1377         p = [odbdesc paramDescriptorForKeyword:keyFileCustomPath];
1378         if (p)
1379             [dict setObject:[p stringValue] forKey:@"remotePath"];
1381         p = [odbdesc paramDescriptorForKeyword:keyFileSenderToken];
1382         if (p) {
1383             [dict setObject:[NSNumber numberWithUnsignedLong:[p descriptorType]]
1384                      forKey:@"remoteTokenDescType"];
1385             [dict setObject:[p data] forKey:@"remoteTokenData"];
1386         }
1387     }
1389     // 2. Extract Xcode parameters (if any)
1390     NSAppleEventDescriptor *xcodedesc =
1391             [desc paramDescriptorForKeyword:keyAEPosition];
1392     if (xcodedesc) {
1393         NSRange range;
1394         MMSelectionRange *sr = (MMSelectionRange*)[[xcodedesc data] bytes];
1396         if (sr->lineNum < 0) {
1397             // Should select a range of lines.
1398             range.location = sr->startRange + 1;
1399             range.length = sr->endRange - sr->startRange + 1;
1400         } else {
1401             // Should only move cursor to a line.
1402             range.location = sr->lineNum + 1;
1403             range.length = 0;
1404         }
1406         [dict setObject:NSStringFromRange(range) forKey:@"selectionRange"];
1407     }
1409     // 3. Extract Spotlight search text (if any)
1410     NSAppleEventDescriptor *spotlightdesc = 
1411             [desc paramDescriptorForKeyword:keyAESearchText];
1412     if (spotlightdesc)
1413         [dict setObject:[spotlightdesc stringValue] forKey:@"searchText"];
1415     return dict;
1418 #ifdef MM_ENABLE_PLUGINS
1419 - (void)removePlugInMenu
1421     if ([plugInMenuItem menu])
1422         [[plugInMenuItem menu] removeItem:plugInMenuItem];
1425 - (void)addPlugInMenuToMenu:(NSMenu *)mainMenu
1427     NSMenu *windowsMenu = [mainMenu findWindowsMenu];
1429     if ([[plugInMenuItem submenu] numberOfItems] > 0) {
1430         int idx = windowsMenu ? [mainMenu indexOfItemWithSubmenu:windowsMenu]
1431                               : -1;
1432         if (idx > 0) {
1433             [mainMenu insertItem:plugInMenuItem atIndex:idx];
1434         } else {
1435             [mainMenu addItem:plugInMenuItem];
1436         }
1437     }
1439 #endif
1441 - (void)scheduleVimControllerPreloadAfterDelay:(NSTimeInterval)delay
1443     [self performSelector:@selector(preloadVimController:)
1444                withObject:nil
1445                afterDelay:delay];
1448 - (void)cancelVimControllerPreloadRequests
1450     [NSObject cancelPreviousPerformRequestsWithTarget:self
1451             selector:@selector(preloadVimController:)
1452               object:nil];
1455 - (void)preloadVimController:(id)sender
1457     // We only allow preloading of one Vim process at a time (to avoid hogging
1458     // CPU), so schedule another preload in a little while if necessary.
1459     if (-1 != preloadPid) {
1460         [self scheduleVimControllerPreloadAfterDelay:2];
1461         return;
1462     }
1464     if ([cachedVimControllers count] >= [self maxPreloadCacheSize])
1465         return;
1467     preloadPid = [self launchVimProcessWithArguments:
1468             [NSArray arrayWithObject:@"--mmwaitforack"]];
1471 - (int)maxPreloadCacheSize
1473     // The maximum number of Vim processes to keep in the cache can be
1474     // controlled via the user default "MMPreloadCacheSize".
1475     int maxCacheSize = [[NSUserDefaults standardUserDefaults]
1476             integerForKey:MMPreloadCacheSizeKey];
1477     if (maxCacheSize < 0) maxCacheSize = 0;
1478     else if (maxCacheSize > 10) maxCacheSize = 10;
1480     return maxCacheSize;
1483 - (MMVimController *)takeVimControllerFromCache
1485     // NOTE: After calling this message the backend corresponding to the
1486     // returned vim controller must be sent an acknowledgeConnection message,
1487     // else the vim process will be stuck.
1488     //
1489     // This method may return nil even though the cache might be non-empty; the
1490     // caller should handle this by starting a new Vim process.
1492     int i, count = [cachedVimControllers count];
1493     if (0 == count) return nil;
1495     // Locate the first Vim controller with up-to-date rc-files sourced.
1496     NSDate *rcDate = [self rcFilesModificationDate];
1497     for (i = 0; i < count; ++i) {
1498         MMVimController *vc = [cachedVimControllers objectAtIndex:i];
1499         NSDate *date = [vc creationDate];
1500         if ([date compare:rcDate] != NSOrderedAscending)
1501             break;
1502     }
1504     if (i > 0) {
1505         // Clear out cache entries whose vimrc/gvimrc files were sourced before
1506         // the latest modification date for those files.  This ensures that the
1507         // latest rc-files are always sourced for new windows.
1508         [self clearPreloadCacheWithCount:i];
1509     }
1511     if ([cachedVimControllers count] == 0) {
1512         [self scheduleVimControllerPreloadAfterDelay:2.0];
1513         return nil;
1514     }
1516     MMVimController *vc = [cachedVimControllers objectAtIndex:0];
1517     [vimControllers addObject:vc];
1518     [cachedVimControllers removeObjectAtIndex:0];
1519     [vc setIsPreloading:NO];
1521     // If the Vim process has finished loading then the window will displayed
1522     // now, otherwise it will be displayed when the OpenWindowMsgID message is
1523     // received.
1524     [[vc windowController] showWindow];
1526     // Since we've taken one controller from the cache we take the opportunity
1527     // to preload another.
1528     [self scheduleVimControllerPreloadAfterDelay:1];
1530     return vc;
1533 - (void)clearPreloadCacheWithCount:(int)count
1535     // Remove the 'count' first entries in the preload cache.  It is assumed
1536     // that objects are added/removed from the cache in a FIFO manner so that
1537     // this effectively clears the 'count' oldest entries.
1538     // If 'count' is negative, then the entire cache is cleared.
1540     if ([cachedVimControllers count] == 0 || count == 0)
1541         return;
1543     if (count < 0)
1544         count = [cachedVimControllers count];
1546     // Make sure the preloaded Vim processes get killed or they'll just hang
1547     // around being useless until MacVim is terminated.
1548     NSEnumerator *e = [cachedVimControllers objectEnumerator];
1549     MMVimController *vc;
1550     int n = count;
1551     while ((vc = [e nextObject]) && n-- > 0) {
1552         [[NSNotificationCenter defaultCenter] removeObserver:vc];
1553         [vc sendMessage:TerminateNowMsgID data:nil];
1555         // Since the preloaded processes were killed "prematurely" we have to
1556         // manually tell them to cleanup (it is not enough to simply release
1557         // them since deallocation and cleanup are separated).
1558         [vc cleanup];
1559     }
1561     n = count;
1562     while (n-- > 0 && [cachedVimControllers count] > 0)
1563         [cachedVimControllers removeObjectAtIndex:0];
1566 - (void)rebuildPreloadCache
1568     if ([self maxPreloadCacheSize] > 0) {
1569         [self clearPreloadCacheWithCount:-1];
1570         [self cancelVimControllerPreloadRequests];
1571         [self scheduleVimControllerPreloadAfterDelay:1.0];
1572     }
1575 - (NSDate *)rcFilesModificationDate
1577     // Check modification dates for ~/.vimrc and ~/.gvimrc and return the
1578     // latest modification date.  If ~/.vimrc does not exist, check ~/_vimrc
1579     // and similarly for gvimrc.
1580     // Returns distantPath if no rc files were found.
1582     NSDate *date = [NSDate distantPast];
1583     NSFileManager *fm = [NSFileManager defaultManager];
1585     NSString *path = [@"~/.vimrc" stringByExpandingTildeInPath];
1586     NSDictionary *attr = [fm fileAttributesAtPath:path traverseLink:YES];
1587     if (!attr) {
1588         path = [@"~/_vimrc" stringByExpandingTildeInPath];
1589         attr = [fm fileAttributesAtPath:path traverseLink:YES];
1590     }
1591     NSDate *modDate = [attr objectForKey:NSFileModificationDate];
1592     if (modDate)
1593         date = modDate;
1595     path = [@"~/.gvimrc" stringByExpandingTildeInPath];
1596     attr = [fm fileAttributesAtPath:path traverseLink:YES];
1597     if (!attr) {
1598         path = [@"~/_gvimrc" stringByExpandingTildeInPath];
1599         attr = [fm fileAttributesAtPath:path traverseLink:YES];
1600     }
1601     modDate = [attr objectForKey:NSFileModificationDate];
1602     if (modDate)
1603         date = [date laterDate:modDate];
1605     return date;
1608 - (BOOL)openVimControllerWithArguments:(NSDictionary *)arguments
1610     MMVimController *vc = [self findUnusedEditor];
1611     if (vc) {
1612         // Open files in an already open window.
1613         [[[vc windowController] window] makeKeyAndOrderFront:self];
1614         [vc passArguments:arguments];
1615     } else if ((vc = [self takeVimControllerFromCache])) {
1616         // Open files in a new window using a cached vim controller.  This
1617         // requires virtually no loading time so the new window will pop up
1618         // instantaneously.
1619         [vc passArguments:arguments];
1620         [[vc backendProxy] acknowledgeConnection];
1621     } else {
1622         // Open files in a launching Vim process or start a new process.  This
1623         // may take 1-2 seconds so there will be a visible delay before the
1624         // window appears on screen.
1625         int pid = [self findLaunchingProcessWithoutArguments];
1626         if (-1 == pid) {
1627             pid = [self launchVimProcessWithArguments:nil];
1628             if (-1 == pid)
1629                 return NO;
1630         }
1632         // TODO: If the Vim process fails to start, or if it changes PID,
1633         // then the memory allocated for these parameters will leak.
1634         // Ensure that this cannot happen or somehow detect it.
1636         if ([arguments count] > 0)
1637             [pidArguments setObject:arguments
1638                              forKey:[NSNumber numberWithInt:pid]];
1639     }
1641     return YES;
1644 - (void)activateWhenNextWindowOpens
1646     shouldActivateWhenNextWindowOpens = YES;
1649 - (void)startWatchingVimDir
1651     //NSLog(@"%s", _cmd);
1652 #if (MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_4)
1653     if (fsEventStream)
1654         return;
1655     if (NULL == FSEventStreamStart)
1656         return; // FSEvent functions are weakly linked
1658     NSString *path = [@"~/.vim" stringByExpandingTildeInPath];
1659     NSArray *pathsToWatch = [NSArray arrayWithObject:path];
1661     fsEventStream = FSEventStreamCreate(NULL, &fsEventCallback, NULL,
1662             (CFArrayRef)pathsToWatch, kFSEventStreamEventIdSinceNow,
1663             MMEventStreamLatency, kFSEventStreamCreateFlagNone);
1665     FSEventStreamScheduleWithRunLoop(fsEventStream,
1666             [[NSRunLoop currentRunLoop] getCFRunLoop],
1667             kCFRunLoopDefaultMode);
1669     FSEventStreamStart(fsEventStream);
1670     //NSLog(@"Started FS event stream");
1671 #endif
1674 - (void)stopWatchingVimDir
1676     //NSLog(@"%s", _cmd);
1677 #if (MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_4)
1678     if (NULL == FSEventStreamStop)
1679         return; // FSEvent functions are weakly linked
1681     if (fsEventStream) {
1682         FSEventStreamStop(fsEventStream);
1683         FSEventStreamInvalidate(fsEventStream);
1684         FSEventStreamRelease(fsEventStream);
1685         fsEventStream = NULL;
1686         //NSLog(@"Stopped FS event stream");
1687     }
1688 #endif
1692 - (void)handleFSEvent
1694     //NSLog(@"%s", _cmd);
1695     [self clearPreloadCacheWithCount:-1];
1697     // Several FS events may arrive in quick succession so make sure to cancel
1698     // any previous preload requests before making a new one.
1699     [self cancelVimControllerPreloadRequests];
1700     [self scheduleVimControllerPreloadAfterDelay:0.5];
1703 @end // MMAppController (Private)
1708     static int
1709 executeInLoginShell(NSString *path, NSArray *args)
1711     // Start a login shell and execute the command 'path' with arguments 'args'
1712     // in the shell.  This ensures that user environment variables are set even
1713     // when MacVim was started from the Finder.
1715     int pid = -1;
1716     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
1718     // Determine which shell to use to execute the command.  The user
1719     // may decide which shell to use by setting a user default or the
1720     // $SHELL environment variable.
1721     NSString *shell = [ud stringForKey:MMLoginShellCommandKey];
1722     if (!shell || [shell length] == 0)
1723         shell = [[[NSProcessInfo processInfo] environment]
1724             objectForKey:@"SHELL"];
1725     if (!shell)
1726         shell = @"/bin/bash";
1728     //NSLog(@"shell = %@", shell);
1730     // Bash needs the '-l' flag to launch a login shell.  The user may add
1731     // flags by setting a user default.
1732     NSString *shellArgument = [ud stringForKey:MMLoginShellArgumentKey];
1733     if (!shellArgument || [shellArgument length] == 0) {
1734         if ([[shell lastPathComponent] isEqual:@"bash"])
1735             shellArgument = @"-l";
1736         else
1737             shellArgument = nil;
1738     }
1740     //NSLog(@"shellArgument = %@", shellArgument);
1742     // Build input string to pipe to the login shell.
1743     NSMutableString *input = [NSMutableString stringWithFormat:
1744             @"exec \"%@\"", path];
1745     if (args) {
1746         // Append all arguments, making sure they are properly quoted, even
1747         // when they contain single quotes.
1748         NSEnumerator *e = [args objectEnumerator];
1749         id obj;
1751         while ((obj = [e nextObject])) {
1752             NSMutableString *arg = [NSMutableString stringWithString:obj];
1753             [arg replaceOccurrencesOfString:@"'" withString:@"'\"'\"'"
1754                                     options:NSLiteralSearch
1755                                       range:NSMakeRange(0, [arg length])];
1756             [input appendFormat:@" '%@'", arg];
1757         }
1758     }
1760     // Build the argument vector used to start the login shell.
1761     NSString *shellArg0 = [NSString stringWithFormat:@"-%@",
1762              [shell lastPathComponent]];
1763     char *shellArgv[3] = { (char *)[shellArg0 UTF8String], NULL, NULL };
1764     if (shellArgument)
1765         shellArgv[1] = (char *)[shellArgument UTF8String];
1767     // Get the C string representation of the shell path before the fork since
1768     // we must not call Foundation functions after a fork.
1769     const char *shellPath = [shell fileSystemRepresentation];
1771     // Fork and execute the process.
1772     int ds[2];
1773     if (pipe(ds)) return -1;
1775     pid = fork();
1776     if (pid == -1) {
1777         return -1;
1778     } else if (pid == 0) {
1779         // Child process
1780         if (close(ds[1]) == -1) exit(255);
1781         if (dup2(ds[0], 0) == -1) exit(255);
1783         execv(shellPath, shellArgv);
1785         // Never reached unless execv fails
1786         exit(255);
1787     } else {
1788         // Parent process
1789         if (close(ds[0]) == -1) return -1;
1791         // Send input to execute to the child process
1792         [input appendString:@"\n"];
1793         int bytes = [input lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
1795         if (write(ds[1], [input UTF8String], bytes) != bytes) return -1;
1796         if (close(ds[1]) == -1) return -1;
1797     }
1799     return pid;