Prepare for 64 bit
[MacVim.git] / src / MacVim / MMAppController.m
blob50d4a4ca9b736425130bf232f95ee1cefaaef42b
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 #if (MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5)
65 // Latency (in s) between FS event occuring and being reported to MacVim.
66 // Should be small so that MacVim is notified of changes to the ~/.vim
67 // directory more or less immediately.
68 static CFTimeInterval MMEventStreamLatency = 0.1;
69 #endif
71 static float MMCascadeHorizontalOffset = 21;
72 static float MMCascadeVerticalOffset = 23;
75 #pragma pack(push,1)
76 // The alignment and sizes of these fields are based on trial-and-error.  It
77 // may be necessary to adjust them to fit if Xcode ever changes this struct.
78 typedef struct
80     int16_t unused1;      // 0 (not used)
81     int16_t lineNum;      // line to select (< 0 to specify range)
82     int32_t startRange;   // start of selection range (if line < 0)
83     int32_t endRange;     // end of selection range (if line < 0)
84     int32_t unused2;      // 0 (not used)
85     int32_t theDate;      // modification date/time
86 } MMXcodeSelectionRange;
87 #pragma pack(pop)
90 // This is a private AppKit API gleaned from class-dump.
91 @interface NSKeyBindingManager : NSObject
92 + (id)sharedKeyBindingManager;
93 - (id)dictionary;
94 - (void)setDictionary:(id)arg1;
95 @end
98 @interface MMAppController (MMServices)
99 - (void)openSelection:(NSPasteboard *)pboard userData:(NSString *)userData
100                 error:(NSString **)error;
101 - (void)openFile:(NSPasteboard *)pboard userData:(NSString *)userData
102            error:(NSString **)error;
103 - (void)newFileHere:(NSPasteboard *)pboard userData:(NSString *)userData
104               error:(NSString **)error;
105 @end
108 @interface MMAppController (Private)
109 - (MMVimController *)topmostVimController;
110 - (int)launchVimProcessWithArguments:(NSArray *)args;
111 - (NSArray *)filterFilesAndNotify:(NSArray *)files;
112 - (NSArray *)filterOpenFiles:(NSArray *)filenames
113                openFilesDict:(NSDictionary **)openFiles;
114 #if MM_HANDLE_XCODE_MOD_EVENT
115 - (void)handleXcodeModEvent:(NSAppleEventDescriptor *)event
116                  replyEvent:(NSAppleEventDescriptor *)reply;
117 #endif
118 - (void)handleGetURLEvent:(NSAppleEventDescriptor *)event
119                replyEvent:(NSAppleEventDescriptor *)reply;
120 - (int)findLaunchingProcessWithoutArguments;
121 - (MMVimController *)findUnusedEditor;
122 - (NSMutableDictionary *)extractArgumentsFromOdocEvent:
123     (NSAppleEventDescriptor *)desc;
124 - (void)scheduleVimControllerPreloadAfterDelay:(NSTimeInterval)delay;
125 - (void)cancelVimControllerPreloadRequests;
126 - (void)preloadVimController:(id)sender;
127 - (int)maxPreloadCacheSize;
128 - (MMVimController *)takeVimControllerFromCache;
129 - (void)clearPreloadCacheWithCount:(int)count;
130 - (void)rebuildPreloadCache;
131 - (NSDate *)rcFilesModificationDate;
132 - (BOOL)openVimControllerWithArguments:(NSDictionary *)arguments;
133 - (void)activateWhenNextWindowOpens;
134 - (void)startWatchingVimDir;
135 - (void)stopWatchingVimDir;
136 - (void)handleFSEvent;
137 - (void)loadDefaultFont;
138 - (int)executeInLoginShell:(NSString *)path arguments:(NSArray *)args;
139 - (void)reapChildProcesses:(id)sender;
140 - (void)processInputQueues:(id)sender;
141 - (void)addVimController:(MMVimController *)vc;
143 #ifdef MM_ENABLE_PLUGINS
144 - (void)removePlugInMenu;
145 - (void)addPlugInMenuToMenu:(NSMenu *)mainMenu;
146 #endif
147 @end
151 #if (MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5)
152     static void
153 fsEventCallback(ConstFSEventStreamRef streamRef,
154                 void *clientCallBackInfo,
155                 size_t numEvents,
156                 void *eventPaths,
157                 const FSEventStreamEventFlags eventFlags[],
158                 const FSEventStreamEventId eventIds[])
160     [[MMAppController sharedInstance] handleFSEvent];
162 #endif
164 @implementation MMAppController
166 + (void)initialize
168     static BOOL initDone = NO;
169     if (initDone) return;
170     initDone = YES;
172     ASLInit();
174     // HACK! The following user default must be reset, else Ctrl-q (or
175     // whichever key is specified by the default) will be blocked by the input
176     // manager (interpretKeyEvents: swallows that key).  (We can't use
177     // NSUserDefaults since it only allows us to write to the registration
178     // domain and this preference has "higher precedence" than that so such a
179     // change would have no effect.)
180     CFPreferencesSetAppValue(CFSTR("NSQuotedKeystrokeBinding"),
181                              CFSTR(""),
182                              kCFPreferencesCurrentApplication);
183     
184     NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:
185         [NSNumber numberWithBool:NO],   MMNoWindowKey,
186         [NSNumber numberWithInt:64],    MMTabMinWidthKey,
187         [NSNumber numberWithInt:6*64],  MMTabMaxWidthKey,
188         [NSNumber numberWithInt:132],   MMTabOptimumWidthKey,
189         [NSNumber numberWithBool:YES],  MMShowAddTabButtonKey,
190         [NSNumber numberWithInt:2],     MMTextInsetLeftKey,
191         [NSNumber numberWithInt:1],     MMTextInsetRightKey,
192         [NSNumber numberWithInt:1],     MMTextInsetTopKey,
193         [NSNumber numberWithInt:1],     MMTextInsetBottomKey,
194         @"MMTypesetter",                MMTypesetterKey,
195         [NSNumber numberWithFloat:1],   MMCellWidthMultiplierKey,
196         [NSNumber numberWithFloat:-1],  MMBaselineOffsetKey,
197         [NSNumber numberWithBool:YES],  MMTranslateCtrlClickKey,
198         [NSNumber numberWithInt:0],     MMOpenInCurrentWindowKey,
199         [NSNumber numberWithBool:NO],   MMNoFontSubstitutionKey,
200         [NSNumber numberWithBool:YES],  MMLoginShellKey,
201         [NSNumber numberWithBool:NO],   MMAtsuiRendererKey,
202         [NSNumber numberWithInt:MMUntitledWindowAlways],
203                                         MMUntitledWindowKey,
204         [NSNumber numberWithBool:NO],   MMTexturedWindowKey,
205         [NSNumber numberWithBool:NO],   MMZoomBothKey,
206         @"",                            MMLoginShellCommandKey,
207         @"",                            MMLoginShellArgumentKey,
208         [NSNumber numberWithBool:YES],  MMDialogsTrackPwdKey,
209 #ifdef MM_ENABLE_PLUGINS
210         [NSNumber numberWithBool:YES],  MMShowLeftPlugInContainerKey,
211 #endif
212         [NSNumber numberWithInt:3],     MMOpenLayoutKey,
213         [NSNumber numberWithBool:NO],   MMVerticalSplitKey,
214         [NSNumber numberWithInt:0],     MMPreloadCacheSizeKey,
215         [NSNumber numberWithInt:0],     MMLastWindowClosedBehaviorKey,
216         [NSNumber numberWithBool:YES],  MMLoadDefaultFontKey,
217 #ifdef INCLUDE_OLD_IM_CODE
218         [NSNumber numberWithBool:YES],  MMUseInlineImKey,
219 #endif // INCLUDE_OLD_IM_CODE
220         nil];
222     [[NSUserDefaults standardUserDefaults] registerDefaults:dict];
224     NSArray *types = [NSArray arrayWithObject:NSStringPboardType];
225     [NSApp registerServicesMenuSendTypes:types returnTypes:types];
227     // NOTE: Set the current directory to user's home directory, otherwise it
228     // will default to the root directory.  (This matters since new Vim
229     // processes inherit MacVim's environment variables.)
230     [[NSFileManager defaultManager] changeCurrentDirectoryPath:
231             NSHomeDirectory()];
234 - (id)init
236     if (!(self = [super init])) return nil;
238     [self loadDefaultFont];
240     vimControllers = [NSMutableArray new];
241     cachedVimControllers = [NSMutableArray new];
242     preloadPid = -1;
243     pidArguments = [NSMutableDictionary new];
244     inputQueues = [NSMutableDictionary new];
246 #ifdef MM_ENABLE_PLUGINS
247     NSString *plugInTitle = NSLocalizedString(@"Plug-In",
248                                               @"Plug-In menu title");
249     plugInMenuItem = [[NSMenuItem alloc] initWithTitle:plugInTitle
250                                                 action:NULL
251                                          keyEquivalent:@""];
252     NSMenu *submenu = [[NSMenu alloc] initWithTitle:plugInTitle];
253     [plugInMenuItem setSubmenu:submenu];
254     [submenu release];
255 #endif
257     // NOTE: Do not use the default connection since the Logitech Control
258     // Center (LCC) input manager steals and this would cause MacVim to
259     // never open any windows.  (This is a bug in LCC but since they are
260     // unlikely to fix it, we graciously give them the default connection.)
261     connection = [[NSConnection alloc] initWithReceivePort:[NSPort port]
262                                                   sendPort:nil];
263     [connection setRootObject:self];
264     [connection setRequestTimeout:MMRequestTimeout];
265     [connection setReplyTimeout:MMReplyTimeout];
267     // NOTE!  If the name of the connection changes here it must also be
268     // updated in MMBackend.m.
269     NSString *name = [NSString stringWithFormat:@"%@-connection",
270              [[NSBundle mainBundle] bundlePath]];
271     if (![connection registerName:name]) {
272         ASLogCrit(@"Failed to register connection with name '%@'", name);
273         [connection release];  connection = nil;
274     }
276     return self;
279 - (void)dealloc
281     ASLogDebug(@"");
283     [connection release];  connection = nil;
284     [inputQueues release];  inputQueues = nil;
285     [pidArguments release];  pidArguments = nil;
286     [vimControllers release];  vimControllers = nil;
287     [cachedVimControllers release];  cachedVimControllers = nil;
288     [openSelectionString release];  openSelectionString = nil;
289     [recentFilesMenuItem release];  recentFilesMenuItem = nil;
290     [defaultMainMenu release];  defaultMainMenu = nil;
291 #ifdef MM_ENABLE_PLUGINS
292     [plugInMenuItem release];  plugInMenuItem = nil;
293 #endif
294     [appMenuItemTemplate release];  appMenuItemTemplate = nil;
296     [super dealloc];
299 - (void)applicationWillFinishLaunching:(NSNotification *)notification
301     // Remember the default menu so that it can be restored if the user closes
302     // all editor windows.
303     defaultMainMenu = [[NSApp mainMenu] retain];
305     // Store a copy of the default app menu so we can use this as a template
306     // for all other menus.  We make a copy here because the "Services" menu
307     // will not yet have been populated at this time.  If we don't we get
308     // problems trying to set key equivalents later on because they might clash
309     // with items on the "Services" menu.
310     appMenuItemTemplate = [defaultMainMenu itemAtIndex:0];
311     appMenuItemTemplate = [appMenuItemTemplate copy];
313     // Set up the "Open Recent" menu. See
314     //   http://lapcatsoftware.com/blog/2007/07/10/
315     //     working-without-a-nib-part-5-open-recent-menu/
316     // and
317     //   http://www.cocoabuilder.com/archive/message/cocoa/2007/8/15/187793
318     // for more information.
319     //
320     // The menu itself is created in MainMenu.nib but we still seem to have to
321     // hack around a bit to get it to work.  (This has to be done in
322     // applicationWillFinishLaunching at the latest, otherwise it doesn't
323     // work.)
324     NSMenu *fileMenu = [defaultMainMenu findFileMenu];
325     if (fileMenu) {
326         int idx = [fileMenu indexOfItemWithAction:@selector(fileOpen:)];
327         if (idx >= 0 && idx+1 < [fileMenu numberOfItems])
329         recentFilesMenuItem = [fileMenu itemWithTitle:@"Open Recent"];
330         [[recentFilesMenuItem submenu] performSelector:@selector(_setMenuName:)
331                                         withObject:@"NSRecentDocumentsMenu"];
333         // Note: The "Recent Files" menu must be moved around since there is no
334         // -[NSApp setRecentFilesMenu:] method.  We keep a reference to it to
335         // facilitate this move (see setMainMenu: below).
336         [recentFilesMenuItem retain];
337     }
339 #if MM_HANDLE_XCODE_MOD_EVENT
340     [[NSAppleEventManager sharedAppleEventManager]
341             setEventHandler:self
342                 andSelector:@selector(handleXcodeModEvent:replyEvent:)
343               forEventClass:'KAHL'
344                  andEventID:'MOD '];
345 #endif
347     // Register 'mvim://' URL handler
348     [[NSAppleEventManager sharedAppleEventManager]
349             setEventHandler:self
350                 andSelector:@selector(handleGetURLEvent:replyEvent:)
351               forEventClass:kInternetEventClass
352                  andEventID:kAEGetURL];
354     // Disable the default Cocoa "Key Bindings" since they interfere with the
355     // way Vim handles keyboard input.  Cocoa reads bindings from
356     //     /System/Library/Frameworks/AppKit.framework/Resources/
357     //                                                  StandardKeyBinding.dict
358     // and
359     //     ~/Library/KeyBindings/DefaultKeyBinding.dict
360     // To avoid having the user accidentally break keyboard handling (by
361     // modifying the latter in some unexpected way) in MacVim we load our own
362     // key binding dictionary from Resource/KeyBinding.plist.  We can't disable
363     // the bindings completely since it would break keyboard handling in
364     // dialogs so the our custom dictionary contains all the entries from the
365     // former location.
366     //
367     // It is possible to disable key bindings completely by not calling
368     // interpretKeyEvents: in keyDown: but this also disables key bindings used
369     // by certain input methods.  E.g.  Ctrl-Shift-; would no longer work in
370     // the Kotoeri input manager.
371     //
372     // To solve this problem we access a private API and set the key binding
373     // dictionary to our own custom dictionary here.  At this time Cocoa will
374     // have already read the above mentioned dictionaries so it (hopefully)
375     // won't try to change the key binding dictionary again after this point.
376     NSKeyBindingManager *mgr = [NSKeyBindingManager sharedKeyBindingManager];
377     NSBundle *mainBundle = [NSBundle mainBundle];
378     NSString *path = [mainBundle pathForResource:@"KeyBinding"
379                                           ofType:@"plist"];
380     NSDictionary *dict = [NSDictionary dictionaryWithContentsOfFile:path];
381     if (mgr && dict) {
382         [mgr setDictionary:dict];
383     } else {
384         ASLogNotice(@"Failed to override the Cocoa key bindings.  Keyboard "
385                 "input may behave strangely as a result (path=%@).", path);
386     }
389 - (void)applicationDidFinishLaunching:(NSNotification *)notification
391     [NSApp setServicesProvider:self];
392 #ifdef MM_ENABLE_PLUGINS
393     [[MMPlugInManager sharedManager] loadAllPlugIns];
394 #endif
396     if ([self maxPreloadCacheSize] > 0) {
397         [self scheduleVimControllerPreloadAfterDelay:2];
398         [self startWatchingVimDir];
399     }
401     ASLogInfo(@"MacVim finished launching");
404 - (BOOL)applicationShouldOpenUntitledFile:(NSApplication *)sender
406     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
407     NSAppleEventManager *aem = [NSAppleEventManager sharedAppleEventManager];
408     NSAppleEventDescriptor *desc = [aem currentAppleEvent];
410     // The user default MMUntitledWindow can be set to control whether an
411     // untitled window should open on 'Open' and 'Reopen' events.
412     int untitledWindowFlag = [ud integerForKey:MMUntitledWindowKey];
414     BOOL isAppOpenEvent = [desc eventID] == kAEOpenApplication;
415     if (isAppOpenEvent && (untitledWindowFlag & MMUntitledWindowOnOpen) == 0)
416         return NO;
418     BOOL isAppReopenEvent = [desc eventID] == kAEReopenApplication;
419     if (isAppReopenEvent
420             && (untitledWindowFlag & MMUntitledWindowOnReopen) == 0)
421         return NO;
423     // When a process is started from the command line, the 'Open' event may
424     // contain a parameter to surpress the opening of an untitled window.
425     desc = [desc paramDescriptorForKeyword:keyAEPropData];
426     desc = [desc paramDescriptorForKeyword:keyMMUntitledWindow];
427     if (desc && ![desc booleanValue])
428         return NO;
430     // Never open an untitled window if there is at least one open window or if
431     // there are processes that are currently launching.
432     if ([vimControllers count] > 0 || [pidArguments count] > 0)
433         return NO;
435     // NOTE!  This way it possible to start the app with the command-line
436     // argument '-nowindow yes' and no window will be opened by default but
437     // this argument will only be heeded when the application is opening.
438     if (isAppOpenEvent && [ud boolForKey:MMNoWindowKey] == YES)
439         return NO;
441     return YES;
444 - (BOOL)applicationOpenUntitledFile:(NSApplication *)sender
446     ASLogDebug(@"Opening untitled window...");
447     [self newWindow:self];
448     return YES;
451 - (void)application:(NSApplication *)sender openFiles:(NSArray *)filenames
453     ASLogInfo(@"Opening files %@", filenames);
455     // Extract ODB/Xcode/Spotlight parameters from the current Apple event,
456     // sort the filenames, and then let openFiles:withArguments: do the heavy
457     // lifting.
459     if (!(filenames && [filenames count] > 0))
460         return;
462     // Sort filenames since the Finder doesn't take care in preserving the
463     // order in which files are selected anyway (and "sorted" is more
464     // predictable than "random").
465     if ([filenames count] > 1)
466         filenames = [filenames sortedArrayUsingSelector:
467                 @selector(localizedCompare:)];
469     // Extract ODB/Xcode/Spotlight parameters from the current Apple event
470     NSMutableDictionary *arguments = [self extractArgumentsFromOdocEvent:
471             [[NSAppleEventManager sharedAppleEventManager] currentAppleEvent]];
473     if ([self openFiles:filenames withArguments:arguments]) {
474         [NSApp replyToOpenOrPrint:NSApplicationDelegateReplySuccess];
475     } else {
476         // TODO: Notify user of failure?
477         [NSApp replyToOpenOrPrint:NSApplicationDelegateReplyFailure];
478     }
481 - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender
483     return (MMTerminateWhenLastWindowClosed ==
484             [[NSUserDefaults standardUserDefaults]
485                 integerForKey:MMLastWindowClosedBehaviorKey]);
488 - (NSApplicationTerminateReply)applicationShouldTerminate:
489     (NSApplication *)sender
491     // TODO: Follow Apple's guidelines for 'Graceful Application Termination'
492     // (in particular, allow user to review changes and save).
493     int reply = NSTerminateNow;
494     BOOL modifiedBuffers = NO;
496     // Go through windows, checking for modified buffers.  (Each Vim process
497     // tells MacVim when any buffer has been modified and MacVim sets the
498     // 'documentEdited' flag of the window correspondingly.)
499     NSEnumerator *e = [[NSApp windows] objectEnumerator];
500     id window;
501     while ((window = [e nextObject])) {
502         if ([window isDocumentEdited]) {
503             modifiedBuffers = YES;
504             break;
505         }
506     }
508     if (modifiedBuffers) {
509         NSAlert *alert = [[NSAlert alloc] init];
510         [alert setAlertStyle:NSWarningAlertStyle];
511         [alert addButtonWithTitle:NSLocalizedString(@"Quit",
512                 @"Dialog button")];
513         [alert addButtonWithTitle:NSLocalizedString(@"Cancel",
514                 @"Dialog button")];
515         [alert setMessageText:NSLocalizedString(@"Quit without saving?",
516                 @"Quit dialog with changed buffers, title")];
517         [alert setInformativeText:NSLocalizedString(
518                 @"There are modified buffers, "
519                 "if you quit now all changes will be lost.  Quit anyway?",
520                 @"Quit dialog with changed buffers, text")];
522         if ([alert runModal] != NSAlertFirstButtonReturn)
523             reply = NSTerminateCancel;
525         [alert release];
526     } else {
527         // No unmodified buffers, but give a warning if there are multiple
528         // windows and/or tabs open.
529         int numWindows = [vimControllers count];
530         int numTabs = 0;
532         // Count the number of open tabs
533         e = [vimControllers objectEnumerator];
534         id vc;
535         while ((vc = [e nextObject]))
536             numTabs += [[vc objectForVimStateKey:@"numTabs"] intValue];
538         if (numWindows > 1 || numTabs > 1) {
539             NSAlert *alert = [[NSAlert alloc] init];
540             [alert setAlertStyle:NSWarningAlertStyle];
541             [alert addButtonWithTitle:NSLocalizedString(@"Quit",
542                     @"Dialog button")];
543             [alert addButtonWithTitle:NSLocalizedString(@"Cancel",
544                     @"Dialog button")];
545             [alert setMessageText:NSLocalizedString(
546                     @"Are you sure you want to quit MacVim?",
547                     @"Quit dialog with no changed buffers, title")];
549             NSString *info = nil;
550             if (numWindows > 1) {
551                 if (numTabs > numWindows)
552                     info = [NSString stringWithFormat:NSLocalizedString(
553                             @"There are %d windows open in MacVim, with a "
554                             "total of %d tabs. Do you want to quit anyway?",
555                             @"Quit dialog with no changed buffers, text"),
556                          numWindows, numTabs];
557                 else
558                     info = [NSString stringWithFormat:NSLocalizedString(
559                             @"There are %d windows open in MacVim. "
560                             "Do you want to quit anyway?",
561                             @"Quit dialog with no changed buffers, text"),
562                         numWindows];
564             } else {
565                 info = [NSString stringWithFormat:NSLocalizedString(
566                         @"There are %d tabs open in MacVim. "
567                         "Do you want to quit anyway?",
568                         @"Quit dialog with no changed buffers, text"), 
569                      numTabs];
570             }
572             [alert setInformativeText:info];
574             if ([alert runModal] != NSAlertFirstButtonReturn)
575                 reply = NSTerminateCancel;
577             [alert release];
578         }
579     }
582     // Tell all Vim processes to terminate now (otherwise they'll leave swap
583     // files behind).
584     if (NSTerminateNow == reply) {
585         e = [vimControllers objectEnumerator];
586         id vc;
587         while ((vc = [e nextObject])) {
588             ASLogDebug(@"Terminate pid=%d", [vc pid]);
589             [vc sendMessage:TerminateNowMsgID data:nil];
590         }
592         e = [cachedVimControllers objectEnumerator];
593         while ((vc = [e nextObject])) {
594             ASLogDebug(@"Terminate pid=%d (cached)", [vc pid]);
595             [vc sendMessage:TerminateNowMsgID data:nil];
596         }
598         // If a Vim process is being preloaded as we quit we have to forcibly
599         // kill it since we have not established a connection yet.
600         if (preloadPid > 0) {
601             ASLogDebug(@"Kill incomplete preloaded process pid=%d", preloadPid);
602             kill(preloadPid, SIGKILL);
603         }
605         // If a Vim process was loading as we quit we also have to kill it.
606         e = [[pidArguments allKeys] objectEnumerator];
607         NSNumber *pidKey;
608         while ((pidKey = [e nextObject])) {
609             ASLogDebug(@"Kill incomplete process pid=%d", [pidKey intValue]);
610             kill([pidKey intValue], SIGKILL);
611         }
613         // Sleep a little to allow all the Vim processes to exit.
614         usleep(10000);
615     }
617     return reply;
620 - (void)applicationWillTerminate:(NSNotification *)notification
622     ASLogInfo(@"Terminating MacVim...");
624     [self stopWatchingVimDir];
626 #ifdef MM_ENABLE_PLUGINS
627     [[MMPlugInManager sharedManager] unloadAllPlugIns];
628 #endif
630 #if MM_HANDLE_XCODE_MOD_EVENT
631     [[NSAppleEventManager sharedAppleEventManager]
632             removeEventHandlerForEventClass:'KAHL'
633                                  andEventID:'MOD '];
634 #endif
636     // This will invalidate all connections (since they were spawned from this
637     // connection).
638     [connection invalidate];
640     // Deactivate the font we loaded from the app bundle.
641     // NOTE: This can take quite a while (~500 ms), so termination will be
642     // noticeably faster if loading of the default font is disabled.
643     if (fontContainerRef) {
644         ATSFontDeactivate(fontContainerRef, NULL, kATSOptionFlagsDefault);
645         fontContainerRef = 0;
646     }
648     [NSApp setDelegate:nil];
650     // Try to wait for all child processes to avoid leaving zombies behind (but
651     // don't wait around for too long).
652     NSDate *timeOutDate = [NSDate dateWithTimeIntervalSinceNow:2];
653     while ([timeOutDate timeIntervalSinceNow] > 0) {
654         [self reapChildProcesses:nil];
655         if (numChildProcesses <= 0)
656             break;
658         ASLogDebug(@"%d processes still left, hold on...", numChildProcesses);
660         // Run in NSConnectionReplyMode while waiting instead of calling e.g.
661         // usleep().  Otherwise incoming messages may clog up the DO queues and
662         // the outgoing TerminateNowMsgID sent earlier never reaches the Vim
663         // process.
664         // This has at least one side-effect, namely we may receive the
665         // annoying "dropping incoming DO message".  (E.g. this may happen if
666         // you quickly hit Cmd-n several times in a row and then immediately
667         // press Cmd-q, Enter.)
668         while (CFRunLoopRunInMode((CFStringRef)NSConnectionReplyMode,
669                 0.05, true) == kCFRunLoopRunHandledSource)
670             ;   // do nothing
671     }
673     if (numChildProcesses > 0) {
674         ASLogNotice(@"%d zombies left behind", numChildProcesses);
675     }
678 + (MMAppController *)sharedInstance
680     // Note: The app controller is a singleton which is instantiated in
681     // MainMenu.nib where it is also connected as the delegate of NSApp.
682     id delegate = [NSApp delegate];
683     return [delegate isKindOfClass:self] ? (MMAppController*)delegate : nil;
686 - (NSMenu *)defaultMainMenu
688     return defaultMainMenu;
691 - (NSMenuItem *)appMenuItemTemplate
693     return appMenuItemTemplate;
696 - (void)removeVimController:(id)controller
698     ASLogDebug(@"Remove Vim controller pid=%d id=%d (processingFlag=%d)",
699                [controller pid], [controller vimControllerId], processingFlag);
701     NSUInteger idx = [vimControllers indexOfObject:controller];
702     if (NSNotFound == idx) {
703         ASLogDebug(@"Controller not found, probably due to duplicate removal");
704         return;
705     }
707     [controller retain];
708     [vimControllers removeObjectAtIndex:idx];
709     [controller cleanup];
710     [controller release];
712     if (![vimControllers count]) {
713         // The last editor window just closed so restore the main menu back to
714         // its default state (which is defined in MainMenu.nib).
715         [self setMainMenu:defaultMainMenu];
717         BOOL hide = (MMHideWhenLastWindowClosed ==
718                     [[NSUserDefaults standardUserDefaults]
719                         integerForKey:MMLastWindowClosedBehaviorKey]);
720         if (hide)
721             [NSApp hide:self];
722     }
724     // There is a small delay before the Vim process actually exits so wait a
725     // little before trying to reap the child process.  If the process still
726     // hasn't exited after this wait it won't be reaped until the next time
727     // reapChildProcesses: is called (but this should be harmless).
728     [self performSelector:@selector(reapChildProcesses:)
729                withObject:nil
730                afterDelay:0.1];
733 - (void)windowControllerWillOpen:(MMWindowController *)windowController
735     NSPoint topLeft = NSZeroPoint;
736     NSWindow *topWin = [[[self topmostVimController] windowController] window];
737     NSWindow *win = [windowController window];
739     if (!win) return;
741     // If there is a window belonging to a Vim process, cascade from it,
742     // otherwise use the autosaved window position (if any).
743     if (topWin) {
744         NSRect frame = [topWin frame];
745         topLeft = NSMakePoint(frame.origin.x, NSMaxY(frame));
746     } else {
747         NSString *topLeftString = [[NSUserDefaults standardUserDefaults]
748             stringForKey:MMTopLeftPointKey];
749         if (topLeftString)
750             topLeft = NSPointFromString(topLeftString);
751     }
753     if (!NSEqualPoints(topLeft, NSZeroPoint)) {
754         if (topWin) {
755             // Do manual cascading instead of using
756             // -[MMWindow cascadeTopLeftFromPoint:] since it is rather
757             // unpredictable.
758             topLeft.x += MMCascadeHorizontalOffset;
759             topLeft.y -= MMCascadeVerticalOffset;
760         }
762         NSScreen *screen = [win screen];
763         if (screen) {
764             // Constrain the window so that it is entirely visible on the
765             // screen.  If it sticks out on the right, move it all the way
766             // left.  If it sticks out on the bottom, move it all the way up.
767             // (Assumption: the cascading offsets are positive.)
768             NSRect screenFrame = [screen frame];
769             NSSize winSize = [win frame].size;
770             NSRect winFrame =
771                 { { topLeft.x, topLeft.y - winSize.height }, winSize };
773             if (NSMaxX(winFrame) > NSMaxX(screenFrame))
774                 topLeft.x = NSMinX(screenFrame);
775             if (NSMinY(winFrame) < NSMinY(screenFrame))
776                 topLeft.y = NSMaxY(screenFrame);
777         } else {
778             ASLogNotice(@"Window not on screen, don't constrain position");
779         }
781         [win setFrameTopLeftPoint:topLeft];
782     }
784     if (1 == [vimControllers count]) {
785         // The first window autosaves its position.  (The autosaving
786         // features of Cocoa are not used because we need more control over
787         // what is autosaved and when it is restored.)
788         [windowController setWindowAutosaveKey:MMTopLeftPointKey];
789     }
791     if (openSelectionString) {
792         // TODO: Pass this as a parameter instead!  Get rid of
793         // 'openSelectionString' etc.
794         //
795         // There is some text to paste into this window as a result of the
796         // services menu "Open selection ..." being used.
797         [[windowController vimController] dropString:openSelectionString];
798         [openSelectionString release];
799         openSelectionString = nil;
800     }
802     if (shouldActivateWhenNextWindowOpens) {
803         [NSApp activateIgnoringOtherApps:YES];
804         shouldActivateWhenNextWindowOpens = NO;
805     }
808 - (void)setMainMenu:(NSMenu *)mainMenu
810     if ([NSApp mainMenu] == mainMenu) return;
812     // If the new menu has a "Recent Files" dummy item, then swap the real item
813     // for the dummy.  We are forced to do this since Cocoa initializes the
814     // "Recent Files" menu and there is no way to simply point Cocoa to a new
815     // item each time the menus are swapped.
816     NSMenu *fileMenu = [mainMenu findFileMenu];
817     if (recentFilesMenuItem && fileMenu) {
818         int dummyIdx =
819                 [fileMenu indexOfItemWithAction:@selector(recentFilesDummy:)];
820         if (dummyIdx >= 0) {
821             NSMenuItem *dummyItem = [[fileMenu itemAtIndex:dummyIdx] retain];
822             [fileMenu removeItemAtIndex:dummyIdx];
824             NSMenu *recentFilesParentMenu = [recentFilesMenuItem menu];
825             int idx = [recentFilesParentMenu indexOfItem:recentFilesMenuItem];
826             if (idx >= 0) {
827                 [[recentFilesMenuItem retain] autorelease];
828                 [recentFilesParentMenu removeItemAtIndex:idx];
829                 [recentFilesParentMenu insertItem:dummyItem atIndex:idx];
830             }
832             [fileMenu insertItem:recentFilesMenuItem atIndex:dummyIdx];
833             [dummyItem release];
834         }
835     }
837     // Now set the new menu.  Notice that we keep one menu for each editor
838     // window since each editor can have its own set of menus.  When swapping
839     // menus we have to tell Cocoa where the new "MacVim", "Windows", and
840     // "Services" menu are.
841     [NSApp setMainMenu:mainMenu];
843     // Setting the "MacVim" (or "Application") menu ensures that it is typeset
844     // in boldface.  (The setAppleMenu: method used to be public but is now
845     // private so this will have to be considered a bit of a hack!)
846     NSMenu *appMenu = [mainMenu findApplicationMenu];
847     [NSApp performSelector:@selector(setAppleMenu:) withObject:appMenu];
849     NSMenu *servicesMenu = [mainMenu findServicesMenu];
850     [NSApp setServicesMenu:servicesMenu];
852     NSMenu *windowsMenu = [mainMenu findWindowsMenu];
853     if (windowsMenu) {
854         // Cocoa isn't clever enough to get rid of items it has added to the
855         // "Windows" menu so we have to do it ourselves otherwise there will be
856         // multiple menu items for each window in the "Windows" menu.
857         //   This code assumes that the only items Cocoa add are ones which
858         // send off the action makeKeyAndOrderFront:.  (Cocoa will not add
859         // another separator item if the last item on the "Windows" menu
860         // already is a separator, so we needen't worry about separators.)
861         int i, count = [windowsMenu numberOfItems];
862         for (i = count-1; i >= 0; --i) {
863             NSMenuItem *item = [windowsMenu itemAtIndex:i];
864             if ([item action] == @selector(makeKeyAndOrderFront:))
865                 [windowsMenu removeItem:item];
866         }
867     }
868     [NSApp setWindowsMenu:windowsMenu];
870 #ifdef MM_ENABLE_PLUGINS
871     // Move plugin menu from old to new main menu.
872     [self removePlugInMenu];
873     [self addPlugInMenuToMenu:mainMenu];
874 #endif
877 - (NSArray *)filterOpenFiles:(NSArray *)filenames
879     return [self filterOpenFiles:filenames openFilesDict:nil];
882 - (BOOL)openFiles:(NSArray *)filenames withArguments:(NSDictionary *)args
884     // Opening files works like this:
885     //  a) filter out any already open files
886     //  b) open any remaining files
887     //
888     // A file is opened in an untitled window if there is one (it may be
889     // currently launching, or it may already be visible), otherwise a new
890     // window is opened.
891     //
892     // Each launching Vim process has a dictionary of arguments that are passed
893     // to the process when in checks in (via connectBackend:pid:).  The
894     // arguments for each launching process can be looked up by its PID (in the
895     // pidArguments dictionary).
897     NSMutableDictionary *arguments = (args ? [[args mutableCopy] autorelease]
898                                            : [NSMutableDictionary dictionary]);
900     filenames = normalizeFilenames(filenames);
902     //
903     // a) Filter out any already open files
904     //
905     NSString *firstFile = [filenames objectAtIndex:0];
906     MMVimController *firstController = nil;
907     NSDictionary *openFilesDict = nil;
908     filenames = [self filterOpenFiles:filenames openFilesDict:&openFilesDict];
910     // Pass arguments to vim controllers that had files open.
911     id key;
912     NSEnumerator *e = [openFilesDict keyEnumerator];
914     // (Indicate that we do not wish to open any files at the moment.)
915     [arguments setObject:[NSNumber numberWithBool:YES] forKey:@"dontOpen"];
917     while ((key = [e nextObject])) {
918         NSArray *files = [openFilesDict objectForKey:key];
919         [arguments setObject:files forKey:@"filenames"];
921         MMVimController *vc = [key pointerValue];
922         [vc passArguments:arguments];
924         // If this controller holds the first file, then remember it for later.
925         if ([files containsObject:firstFile])
926             firstController = vc;
927     }
929     // The meaning of "layout" is defined by the WIN_* defines in main.c.
930     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
931     int layout = [ud integerForKey:MMOpenLayoutKey];
932     BOOL splitVert = [ud boolForKey:MMVerticalSplitKey];
933     BOOL openInCurrentWindow = [ud boolForKey:MMOpenInCurrentWindowKey];
935     if (splitVert && MMLayoutHorizontalSplit == layout)
936         layout = MMLayoutVerticalSplit;
937     if (layout < 0 || (layout > MMLayoutTabs && openInCurrentWindow))
938         layout = MMLayoutTabs;
940     if ([filenames count] == 0) {
941         // Raise the window containing the first file that was already open,
942         // and make sure that the tab containing that file is selected.  Only
943         // do this when there are no more files to open, otherwise sometimes
944         // the window with 'firstFile' will be raised, other times it might be
945         // the window that will open with the files in the 'filenames' array.
946         firstFile = [firstFile stringByEscapingSpecialFilenameCharacters];
948         NSString *bufCmd = @"tab sb";
949         switch (layout) {
950             case MMLayoutHorizontalSplit: bufCmd = @"sb"; break;
951             case MMLayoutVerticalSplit:   bufCmd = @"vert sb"; break;
952             case MMLayoutArglist:         bufCmd = @"b"; break;
953         }
955         NSString *input = [NSString stringWithFormat:@"<C-\\><C-N>"
956                 ":let oldswb=&swb|let &swb=\"useopen,usetab\"|"
957                 "%@ %@|let &swb=oldswb|unl oldswb|"
958                 "cal foreground()<CR>", bufCmd, firstFile];
960         [firstController addVimInput:input];
962         return YES;
963     }
965     // Add filenames to "Recent Files" menu, unless they are being edited
966     // remotely (using ODB).
967     if ([arguments objectForKey:@"remoteID"] == nil) {
968         [[NSDocumentController sharedDocumentController]
969                 noteNewRecentFilePaths:filenames];
970     }
972     //
973     // b) Open any remaining files
974     //
976     [arguments setObject:[NSNumber numberWithInt:layout] forKey:@"layout"];
977     [arguments setObject:filenames forKey:@"filenames"];
978     // (Indicate that files should be opened from now on.)
979     [arguments setObject:[NSNumber numberWithBool:NO] forKey:@"dontOpen"];
981     MMVimController *vc;
982     if (openInCurrentWindow && (vc = [self topmostVimController])) {
983         // Open files in an already open window.
984         [[[vc windowController] window] makeKeyAndOrderFront:self];
985         [vc passArguments:arguments];
986         return YES;
987     }
989     BOOL openOk = YES;
990     int numFiles = [filenames count];
991     if (MMLayoutWindows == layout && numFiles > 1) {
992         // Open one file at a time in a new window, but don't open too many at
993         // once (at most cap+1 windows will open).  If the user has increased
994         // the preload cache size we'll take that as a hint that more windows
995         // should be able to open at once.
996         int cap = [self maxPreloadCacheSize] - 1;
997         if (cap < 4) cap = 4;
998         if (cap > numFiles) cap = numFiles;
1000         int i;
1001         for (i = 0; i < cap; ++i) {
1002             NSArray *a = [NSArray arrayWithObject:[filenames objectAtIndex:i]];
1003             [arguments setObject:a forKey:@"filenames"];
1005             // NOTE: We have to copy the args since we'll mutate them in the
1006             // next loop and the below call may retain the arguments while
1007             // waiting for a process to start.
1008             NSDictionary *args = [[arguments copy] autorelease];
1010             openOk = [self openVimControllerWithArguments:args];
1011             if (!openOk) break;
1012         }
1014         // Open remaining files in tabs in a new window.
1015         if (openOk && numFiles > cap) {
1016             NSRange range = { i, numFiles-cap };
1017             NSArray *a = [filenames subarrayWithRange:range];
1018             [arguments setObject:a forKey:@"filenames"];
1019             [arguments setObject:[NSNumber numberWithInt:MMLayoutTabs]
1020                           forKey:@"layout"];
1022             openOk = [self openVimControllerWithArguments:arguments];
1023         }
1024     } else {
1025         // Open all files at once.
1026         openOk = [self openVimControllerWithArguments:arguments];
1027     }
1029     return openOk;
1032 #ifdef MM_ENABLE_PLUGINS
1033 - (void)addItemToPlugInMenu:(NSMenuItem *)item
1035     NSMenu *menu = [plugInMenuItem submenu];
1036     [menu addItem:item];
1037     if ([menu numberOfItems] == 1)
1038         [self addPlugInMenuToMenu:[NSApp mainMenu]];
1041 - (void)removeItemFromPlugInMenu:(NSMenuItem *)item
1043     NSMenu *menu = [plugInMenuItem submenu];
1044     [menu removeItem:item];
1045     if ([menu numberOfItems] == 0)
1046         [self removePlugInMenu];
1048 #endif
1050 - (IBAction)newWindow:(id)sender
1052     ASLogDebug(@"Open new window");
1054     // A cached controller requires no loading times and results in the new
1055     // window popping up instantaneously.  If the cache is empty it may take
1056     // 1-2 seconds to start a new Vim process.
1057     MMVimController *vc = [self takeVimControllerFromCache];
1058     if (vc) {
1059         [[vc backendProxy] acknowledgeConnection];
1060     } else {
1061         [self launchVimProcessWithArguments:nil];
1062     }
1065 - (IBAction)newWindowAndActivate:(id)sender
1067     [self activateWhenNextWindowOpens];
1068     [self newWindow:sender];
1071 - (IBAction)fileOpen:(id)sender
1073     ASLogDebug(@"Show file open panel");
1075     NSString *dir = nil;
1076     BOOL trackPwd = [[NSUserDefaults standardUserDefaults]
1077             boolForKey:MMDialogsTrackPwdKey];
1078     if (trackPwd) {
1079         MMVimController *vc = [self keyVimController];
1080         if (vc) dir = [vc objectForVimStateKey:@"pwd"];
1081     }
1083     NSOpenPanel *panel = [NSOpenPanel openPanel];
1084     [panel setAllowsMultipleSelection:YES];
1085     [panel setAccessoryView:showHiddenFilesView()];
1087     int result = [panel runModalForDirectory:dir file:nil types:nil];
1088     if (NSOKButton == result)
1089         [self application:NSApp openFiles:[panel filenames]];
1092 - (IBAction)selectNextWindow:(id)sender
1094     ASLogDebug(@"Select next window");
1096     unsigned i, count = [vimControllers count];
1097     if (!count) return;
1099     NSWindow *keyWindow = [NSApp keyWindow];
1100     for (i = 0; i < count; ++i) {
1101         MMVimController *vc = [vimControllers objectAtIndex:i];
1102         if ([[[vc windowController] window] isEqual:keyWindow])
1103             break;
1104     }
1106     if (i < count) {
1107         if (++i >= count)
1108             i = 0;
1109         MMVimController *vc = [vimControllers objectAtIndex:i];
1110         [[vc windowController] showWindow:self];
1111     }
1114 - (IBAction)selectPreviousWindow:(id)sender
1116     ASLogDebug(@"Select previous window");
1118     unsigned i, count = [vimControllers count];
1119     if (!count) return;
1121     NSWindow *keyWindow = [NSApp keyWindow];
1122     for (i = 0; i < count; ++i) {
1123         MMVimController *vc = [vimControllers objectAtIndex:i];
1124         if ([[[vc windowController] window] isEqual:keyWindow])
1125             break;
1126     }
1128     if (i < count) {
1129         if (i > 0) {
1130             --i;
1131         } else {
1132             i = count - 1;
1133         }
1134         MMVimController *vc = [vimControllers objectAtIndex:i];
1135         [[vc windowController] showWindow:self];
1136     }
1139 - (IBAction)orderFrontPreferencePanel:(id)sender
1141     ASLogDebug(@"Show preferences panel");
1142     [[MMPreferenceController sharedPrefsWindowController] showWindow:self];
1145 - (IBAction)openWebsite:(id)sender
1147     ASLogDebug(@"Open MacVim website");
1148     [[NSWorkspace sharedWorkspace] openURL:
1149             [NSURL URLWithString:MMWebsiteString]];
1152 - (IBAction)showVimHelp:(id)sender
1154     ASLogDebug(@"Open window with Vim help");
1155     // Open a new window with the help window maximized.
1156     [self launchVimProcessWithArguments:[NSArray arrayWithObjects:
1157             @"-c", @":h gui_mac", @"-c", @":res", nil]];
1160 - (IBAction)zoomAll:(id)sender
1162     ASLogDebug(@"Zoom all windows");
1163     [NSApp makeWindowsPerform:@selector(performZoom:) inOrder:YES];
1166 - (IBAction)atsuiButtonClicked:(id)sender
1168     ASLogDebug(@"Toggle ATSUI renderer");
1169     // This action is called when the user clicks the "use ATSUI renderer"
1170     // button in the advanced preferences pane.
1171     [self rebuildPreloadCache];
1174 - (IBAction)loginShellButtonClicked:(id)sender
1176     ASLogDebug(@"Toggle login shell option");
1177     // This action is called when the user clicks the "use login shell" button
1178     // in the advanced preferences pane.
1179     [self rebuildPreloadCache];
1182 - (IBAction)quickstartButtonClicked:(id)sender
1184     ASLogDebug(@"Toggle Quickstart option");
1185     if ([self maxPreloadCacheSize] > 0) {
1186         [self scheduleVimControllerPreloadAfterDelay:1.0];
1187         [self startWatchingVimDir];
1188     } else {
1189         [self cancelVimControllerPreloadRequests];
1190         [self clearPreloadCacheWithCount:-1];
1191         [self stopWatchingVimDir];
1192     }
1195 - (MMVimController *)keyVimController
1197     NSWindow *keyWindow = [NSApp keyWindow];
1198     if (keyWindow) {
1199         unsigned i, count = [vimControllers count];
1200         for (i = 0; i < count; ++i) {
1201             MMVimController *vc = [vimControllers objectAtIndex:i];
1202             if ([[[vc windowController] window] isEqual:keyWindow])
1203                 return vc;
1204         }
1205     }
1207     return nil;
1210 - (unsigned)connectBackend:(byref in id <MMBackendProtocol>)proxy pid:(int)pid
1212     ASLogDebug(@"pid=%d", pid);
1214     [(NSDistantObject*)proxy setProtocolForProxy:@protocol(MMBackendProtocol)];
1216     // NOTE: Allocate the vim controller now but don't add it to the list of
1217     // controllers since this is a distributed object call and as such can
1218     // arrive at unpredictable times (e.g. while iterating the list of vim
1219     // controllers).
1220     // (What if input arrives before the vim controller is added to the list of
1221     // controllers?  This should not be a problem since the input isn't
1222     // processed immediately (see processInput:forIdentifier:).)
1223     // Also, since the app may be multithreaded (e.g. as a result of showing
1224     // the open panel) we have to ensure this call happens on the main thread,
1225     // else there is a race condition that may lead to a crash.
1226     MMVimController *vc = [[MMVimController alloc] initWithBackend:proxy
1227                                                                pid:pid];
1228     [self performSelectorOnMainThread:@selector(addVimController:)
1229                            withObject:vc
1230                         waitUntilDone:NO
1231                                 modes:[NSArray arrayWithObject:
1232                                        NSDefaultRunLoopMode]];
1234     [vc release];
1236     return [vc vimControllerId];
1239 - (oneway void)processInput:(in bycopy NSArray *)queue
1240               forIdentifier:(unsigned)identifier
1242     // NOTE: Input is not handled immediately since this is a distributed
1243     // object call and as such can arrive at unpredictable times.  Instead,
1244     // queue the input and process it when the run loop is updated.
1246     if (!(queue && identifier)) {
1247         ASLogWarn(@"Bad input for identifier=%d", identifier);
1248         return;
1249     }
1251     ASLogDebug(@"QUEUE for identifier=%d: <<< %@>>>", identifier,
1252                debugStringForMessageQueue(queue));
1254     NSNumber *key = [NSNumber numberWithUnsignedInt:identifier];
1255     NSArray *q = [inputQueues objectForKey:key];
1256     if (q) {
1257         q = [q arrayByAddingObjectsFromArray:queue];
1258         [inputQueues setObject:q forKey:key];
1259     } else {
1260         [inputQueues setObject:queue forKey:key];
1261     }
1263     // NOTE: We must use "event tracking mode" as well as "default mode",
1264     // otherwise the input queue will not be processed e.g. during live
1265     // resizing.
1266     // Also, since the app may be multithreaded (e.g. as a result of showing
1267     // the open panel) we have to ensure this call happens on the main thread,
1268     // else there is a race condition that may lead to a crash.
1269     [self performSelectorOnMainThread:@selector(processInputQueues:)
1270                            withObject:nil
1271                         waitUntilDone:NO
1272                                 modes:[NSArray arrayWithObjects:
1273                                        NSDefaultRunLoopMode,
1274                                        NSEventTrackingRunLoopMode, nil]];
1277 - (NSArray *)serverList
1279     NSMutableArray *array = [NSMutableArray array];
1281     unsigned i, count = [vimControllers count];
1282     for (i = 0; i < count; ++i) {
1283         MMVimController *controller = [vimControllers objectAtIndex:i];
1284         if ([controller serverName])
1285             [array addObject:[controller serverName]];
1286     }
1288     return array;
1291 @end // MMAppController
1296 @implementation MMAppController (MMServices)
1298 - (void)openSelection:(NSPasteboard *)pboard userData:(NSString *)userData
1299                 error:(NSString **)error
1301     if (![[pboard types] containsObject:NSStringPboardType]) {
1302         ASLogNotice(@"Pasteboard contains no NSStringPboardType");
1303         return;
1304     }
1306     ASLogInfo(@"Open new window containing current selection");
1308     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
1309     BOOL openInCurrentWindow = [ud boolForKey:MMOpenInCurrentWindowKey];
1310     MMVimController *vc;
1312     if (openInCurrentWindow && (vc = [self topmostVimController])) {
1313         [vc sendMessage:AddNewTabMsgID data:nil];
1314         [vc dropString:[pboard stringForType:NSStringPboardType]];
1315     } else {
1316         // Save the text, open a new window, and paste the text when the next
1317         // window opens.  (If this is called several times in a row, then all
1318         // but the last call may be ignored.)
1319         if (openSelectionString) [openSelectionString release];
1320         openSelectionString = [[pboard stringForType:NSStringPboardType] copy];
1322         [self newWindow:self];
1323     }
1326 - (void)openFile:(NSPasteboard *)pboard userData:(NSString *)userData
1327            error:(NSString **)error
1329     if (![[pboard types] containsObject:NSStringPboardType]) {
1330         ASLogNotice(@"Pasteboard contains no NSStringPboardType");
1331         return;
1332     }
1334     // TODO: Parse multiple filenames and create array with names.
1335     NSString *string = [pboard stringForType:NSStringPboardType];
1336     string = [string stringByTrimmingCharactersInSet:
1337             [NSCharacterSet whitespaceAndNewlineCharacterSet]];
1338     string = [string stringByStandardizingPath];
1340     ASLogInfo(@"Open new window with selected file: %@", string);
1342     NSArray *filenames = [self filterFilesAndNotify:
1343             [NSArray arrayWithObject:string]];
1344     if ([filenames count] == 0)
1345         return;
1347     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
1348     BOOL openInCurrentWindow = [ud boolForKey:MMOpenInCurrentWindowKey];
1349     MMVimController *vc;
1351     if (openInCurrentWindow && (vc = [self topmostVimController])) {
1352         [vc dropFiles:filenames forceOpen:YES];
1353     } else {
1354         [self openFiles:filenames withArguments:nil];
1355     }
1358 - (void)newFileHere:(NSPasteboard *)pboard userData:(NSString *)userData
1359               error:(NSString **)error
1361     if (![[pboard types] containsObject:NSStringPboardType]) {
1362         ASLogNotice(@"Pasteboard contains no NSStringPboardType");
1363         return;
1364     }
1366     NSString *path = [pboard stringForType:NSStringPboardType];
1367     path = [path stringByExpandingTildeInPath];
1369     BOOL dirIndicator;
1370     if (![[NSFileManager defaultManager] fileExistsAtPath:path
1371                                               isDirectory:&dirIndicator]) {
1372         ASLogNotice(@"Invalid path. Cannot open new document at: %@", path);
1373         return;
1374     }
1376     ASLogInfo(@"Open new file at path=%@", path);
1378     if (!dirIndicator)
1379         path = [path stringByDeletingLastPathComponent];
1381     path = [path stringByEscapingSpecialFilenameCharacters];
1383     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
1384     BOOL openInCurrentWindow = [ud boolForKey:MMOpenInCurrentWindowKey];
1385     MMVimController *vc;
1387     if (openInCurrentWindow && (vc = [self topmostVimController])) {
1388         NSString *input = [NSString stringWithFormat:@"<C-\\><C-N>"
1389                 ":tabe|cd %@<CR>", path];
1390         [vc addVimInput:input];
1391     } else {
1392         NSString *input = [NSString stringWithFormat:@":cd %@", path];
1393         [self launchVimProcessWithArguments:[NSArray arrayWithObjects:
1394                                              @"-c", input, nil]];
1395     }
1398 @end // MMAppController (MMServices)
1403 @implementation MMAppController (Private)
1405 - (MMVimController *)topmostVimController
1407     // Find the topmost visible window which has an associated vim controller.
1408     NSEnumerator *e = [[NSApp orderedWindows] objectEnumerator];
1409     id window;
1410     while ((window = [e nextObject]) && [window isVisible]) {
1411         unsigned i, count = [vimControllers count];
1412         for (i = 0; i < count; ++i) {
1413             MMVimController *vc = [vimControllers objectAtIndex:i];
1414             if ([[[vc windowController] window] isEqual:window])
1415                 return vc;
1416         }
1417     }
1419     return nil;
1422 - (int)launchVimProcessWithArguments:(NSArray *)args
1424     int pid = -1;
1425     NSString *path = [[NSBundle mainBundle] pathForAuxiliaryExecutable:@"Vim"];
1427     if (!path) {
1428         ASLogCrit(@"Vim executable could not be found inside app bundle!");
1429         return -1;
1430     }
1432     NSArray *taskArgs = [NSArray arrayWithObjects:@"-g", @"-f", nil];
1433     if (args)
1434         taskArgs = [taskArgs arrayByAddingObjectsFromArray:args];
1436     BOOL useLoginShell = [[NSUserDefaults standardUserDefaults]
1437             boolForKey:MMLoginShellKey];
1438     if (useLoginShell) {
1439         // Run process with a login shell, roughly:
1440         //   echo "exec Vim -g -f args" | ARGV0=-`basename $SHELL` $SHELL [-l]
1441         pid = [self executeInLoginShell:path arguments:taskArgs];
1442     } else {
1443         // Run process directly:
1444         //   Vim -g -f args
1445         NSTask *task = [NSTask launchedTaskWithLaunchPath:path
1446                                                 arguments:taskArgs];
1447         pid = task ? [task processIdentifier] : -1;
1448     }
1450     if (-1 != pid) {
1451         // The 'pidArguments' dictionary keeps arguments to be passed to the
1452         // process when it connects (this is in contrast to arguments which are
1453         // passed on the command line, like '-f' and '-g').
1454         // If this method is called with nil arguments we take this as a hint
1455         // that this is an "untitled window" being launched and add a null
1456         // object to the 'pidArguments' dictionary.  This way we can detect if
1457         // an untitled window is being launched by looking for null objects in
1458         // this dictionary.
1459         // If this method is called with non-nil arguments then it is assumed
1460         // that the caller takes care of adding items to 'pidArguments' as
1461         // necessary (only some arguments are passed on connect, e.g. files to
1462         // open).
1463         if (!args)
1464             [pidArguments setObject:[NSNull null]
1465                              forKey:[NSNumber numberWithInt:pid]];
1466     } else {
1467         ASLogWarn(@"Failed to launch Vim process: args=%@, useLoginShell=%d",
1468                   args, useLoginShell);
1469     }
1471     return pid;
1474 - (NSArray *)filterFilesAndNotify:(NSArray *)filenames
1476     // Go trough 'filenames' array and make sure each file exists.  Present
1477     // warning dialog if some file was missing.
1479     NSString *firstMissingFile = nil;
1480     NSMutableArray *files = [NSMutableArray array];
1481     unsigned i, count = [filenames count];
1483     for (i = 0; i < count; ++i) {
1484         NSString *name = [filenames objectAtIndex:i];
1485         if ([[NSFileManager defaultManager] fileExistsAtPath:name]) {
1486             [files addObject:name];
1487         } else if (!firstMissingFile) {
1488             firstMissingFile = name;
1489         }
1490     }
1492     if (firstMissingFile) {
1493         NSAlert *alert = [[NSAlert alloc] init];
1494         [alert addButtonWithTitle:NSLocalizedString(@"OK",
1495                 @"Dialog button")];
1497         NSString *text;
1498         if ([files count] >= count-1) {
1499             [alert setMessageText:NSLocalizedString(@"File not found",
1500                     @"File not found dialog, title")];
1501             text = [NSString stringWithFormat:NSLocalizedString(
1502                     @"Could not open file with name %@.",
1503                     @"File not found dialog, text"), firstMissingFile];
1504         } else {
1505             [alert setMessageText:NSLocalizedString(@"Multiple files not found",
1506                     @"File not found dialog, title")];
1507             text = [NSString stringWithFormat:NSLocalizedString(
1508                     @"Could not open file with name %@, and %d other files.",
1509                     @"File not found dialog, text"),
1510                 firstMissingFile, count-[files count]-1];
1511         }
1513         [alert setInformativeText:text];
1514         [alert setAlertStyle:NSWarningAlertStyle];
1516         [alert runModal];
1517         [alert release];
1519         [NSApp replyToOpenOrPrint:NSApplicationDelegateReplyFailure];
1520     }
1522     return files;
1525 - (NSArray *)filterOpenFiles:(NSArray *)filenames
1526                openFilesDict:(NSDictionary **)openFiles
1528     // Filter out any files in the 'filenames' array that are open and return
1529     // all files that are not already open.  On return, the 'openFiles'
1530     // parameter (if non-nil) will point to a dictionary of open files, indexed
1531     // by Vim controller.
1533     NSMutableDictionary *dict = [NSMutableDictionary dictionary];
1534     NSMutableArray *files = [filenames mutableCopy];
1536     // TODO: Escape special characters in 'files'?
1537     NSString *expr = [NSString stringWithFormat:
1538             @"map([\"%@\"],\"bufloaded(v:val)\")",
1539             [files componentsJoinedByString:@"\",\""]];
1541     unsigned i, count = [vimControllers count];
1542     for (i = 0; i < count && [files count] > 0; ++i) {
1543         MMVimController *vc = [vimControllers objectAtIndex:i];
1545         // Query Vim for which files in the 'files' array are open.
1546         NSString *eval = [vc evaluateVimExpression:expr];
1547         if (!eval) continue;
1549         NSIndexSet *idxSet = [NSIndexSet indexSetWithVimList:eval];
1550         if ([idxSet count] > 0) {
1551             [dict setObject:[files objectsAtIndexes:idxSet]
1552                      forKey:[NSValue valueWithPointer:vc]];
1554             // Remove all the files that were open in this Vim process and
1555             // create a new expression to evaluate.
1556             [files removeObjectsAtIndexes:idxSet];
1557             expr = [NSString stringWithFormat:
1558                     @"map([\"%@\"],\"bufloaded(v:val)\")",
1559                     [files componentsJoinedByString:@"\",\""]];
1560         }
1561     }
1563     if (openFiles != nil)
1564         *openFiles = dict;
1566     return [files autorelease];
1569 #if MM_HANDLE_XCODE_MOD_EVENT
1570 - (void)handleXcodeModEvent:(NSAppleEventDescriptor *)event
1571                  replyEvent:(NSAppleEventDescriptor *)reply
1573 #if 0
1574     // Xcode sends this event to query MacVim which open files have been
1575     // modified.
1576     ASLogDebug(@"reply:%@", reply);
1577     ASLogDebug(@"event:%@", event);
1579     NSEnumerator *e = [vimControllers objectEnumerator];
1580     id vc;
1581     while ((vc = [e nextObject])) {
1582         DescType type = [reply descriptorType];
1583         unsigned len = [[type data] length];
1584         NSMutableData *data = [NSMutableData data];
1586         [data appendBytes:&type length:sizeof(DescType)];
1587         [data appendBytes:&len length:sizeof(unsigned)];
1588         [data appendBytes:[reply data] length:len];
1590         [vc sendMessage:XcodeModMsgID data:data];
1591     }
1592 #endif
1594 #endif
1596 - (void)handleGetURLEvent:(NSAppleEventDescriptor *)event
1597                replyEvent:(NSAppleEventDescriptor *)reply
1599     NSString *urlString = [[event paramDescriptorForKeyword:keyDirectObject]
1600         stringValue];
1601     NSURL *url = [NSURL URLWithString:urlString];
1603     // We try to be compatible with TextMate's URL scheme here, as documented
1604     // at http://blog.macromates.com/2007/the-textmate-url-scheme/ . Currently,
1605     // this means that:
1606     //
1607     // The format is: mvim://open?<arguments> where arguments can be:
1608     //
1609     // * url â€” the actual file to open (i.e. a file://… URL), if you leave
1610     //         out this argument, the frontmost document is implied.
1611     // * line â€” line number to go to (one based).
1612     // * column â€” column number to go to (one based).
1613     //
1614     // Example: mvim://open?url=file:///etc/profile&line=20
1616     if ([[url host] isEqualToString:@"open"]) {
1617         NSMutableDictionary *dict = [NSMutableDictionary dictionary];
1619         // Parse query ("url=file://...&line=14") into a dictionary
1620         NSArray *queries = [[url query] componentsSeparatedByString:@"&"];
1621         NSEnumerator *enumerator = [queries objectEnumerator];
1622         NSString *param;
1623         while ((param = [enumerator nextObject])) {
1624             NSArray *arr = [param componentsSeparatedByString:@"="];
1625             if ([arr count] == 2) {
1626                 [dict setValue:[[arr lastObject]
1627                             stringByReplacingPercentEscapesUsingEncoding:
1628                                 NSUTF8StringEncoding]
1629                         forKey:[[arr objectAtIndex:0]
1630                             stringByReplacingPercentEscapesUsingEncoding:
1631                                 NSUTF8StringEncoding]];
1632             }
1633         }
1635         // Actually open the file.
1636         NSString *file = [dict objectForKey:@"url"];
1637         if (file != nil) {
1638             NSURL *fileUrl= [NSURL URLWithString:file];
1639             // TextMate only opens files that already exist.
1640             if ([fileUrl isFileURL]
1641                     && [[NSFileManager defaultManager] fileExistsAtPath:
1642                            [fileUrl path]]) {
1643                 // Strip 'file://' path, else application:openFiles: might think
1644                 // the file is not yet open.
1645                 NSArray *filenames = [NSArray arrayWithObject:[fileUrl path]];
1647                 // Look for the line and column options.
1648                 NSDictionary *args = nil;
1649                 NSString *line = [dict objectForKey:@"line"];
1650                 if (line) {
1651                     NSString *column = [dict objectForKey:@"column"];
1652                     if (column)
1653                         args = [NSDictionary dictionaryWithObjectsAndKeys:
1654                                 line, @"cursorLine",
1655                                 column, @"cursorColumn",
1656                                 nil];
1657                     else
1658                         args = [NSDictionary dictionaryWithObject:line
1659                                 forKey:@"cursorLine"];
1660                 }
1662                 [self openFiles:filenames withArguments:args];
1663             }
1664         }
1665     } else {
1666         NSAlert *alert = [[NSAlert alloc] init];
1667         [alert addButtonWithTitle:NSLocalizedString(@"OK",
1668             @"Dialog button")];
1670         [alert setMessageText:NSLocalizedString(@"Unknown URL Scheme",
1671             @"Unknown URL Scheme dialog, title")];
1672         [alert setInformativeText:[NSString stringWithFormat:NSLocalizedString(
1673             @"This version of MacVim does not support \"%@\""
1674             @" in its URL scheme.",
1675             @"Unknown URL Scheme dialog, text"),
1676             [url host]]];
1678         [alert setAlertStyle:NSWarningAlertStyle];
1679         [alert runModal];
1680         [alert release];
1681     }
1685 - (int)findLaunchingProcessWithoutArguments
1687     NSArray *keys = [pidArguments allKeysForObject:[NSNull null]];
1688     if ([keys count] > 0)
1689         return [[keys objectAtIndex:0] intValue];
1691     return -1;
1694 - (MMVimController *)findUnusedEditor
1696     NSEnumerator *e = [vimControllers objectEnumerator];
1697     id vc;
1698     while ((vc = [e nextObject])) {
1699         if ([[vc objectForVimStateKey:@"unusedEditor"] boolValue])
1700             return vc;
1701     }
1703     return nil;
1706 - (NSMutableDictionary *)extractArgumentsFromOdocEvent:
1707     (NSAppleEventDescriptor *)desc
1709     NSMutableDictionary *dict = [NSMutableDictionary dictionary];
1711     // 1. Extract ODB parameters (if any)
1712     NSAppleEventDescriptor *odbdesc = desc;
1713     if (![odbdesc paramDescriptorForKeyword:keyFileSender]) {
1714         // The ODB paramaters may hide inside the 'keyAEPropData' descriptor.
1715         odbdesc = [odbdesc paramDescriptorForKeyword:keyAEPropData];
1716         if (![odbdesc paramDescriptorForKeyword:keyFileSender])
1717             odbdesc = nil;
1718     }
1720     if (odbdesc) {
1721         NSAppleEventDescriptor *p =
1722                 [odbdesc paramDescriptorForKeyword:keyFileSender];
1723         if (p)
1724             [dict setObject:[NSNumber numberWithUnsignedInt:[p typeCodeValue]]
1725                      forKey:@"remoteID"];
1727         p = [odbdesc paramDescriptorForKeyword:keyFileCustomPath];
1728         if (p)
1729             [dict setObject:[p stringValue] forKey:@"remotePath"];
1731         p = [odbdesc paramDescriptorForKeyword:keyFileSenderToken];
1732         if (p) {
1733             [dict setObject:[NSNumber numberWithUnsignedLong:[p descriptorType]]
1734                      forKey:@"remoteTokenDescType"];
1735             [dict setObject:[p data] forKey:@"remoteTokenData"];
1736         }
1737     }
1739     // 2. Extract Xcode parameters (if any)
1740     NSAppleEventDescriptor *xcodedesc =
1741             [desc paramDescriptorForKeyword:keyAEPosition];
1742     if (xcodedesc) {
1743         NSRange range;
1744         NSData *data = [xcodedesc data];
1745         NSUInteger length = [data length];
1747         if (length == sizeof(MMXcodeSelectionRange)) {
1748             MMXcodeSelectionRange *sr = (MMXcodeSelectionRange*)[data bytes];
1749             ASLogDebug(@"Xcode selection range (%d,%d,%d,%d,%d,%d)",
1750                     sr->unused1, sr->lineNum, sr->startRange, sr->endRange,
1751                     sr->unused2, sr->theDate);
1753             if (sr->lineNum < 0) {
1754                 // Should select a range of lines.
1755                 range.location = sr->startRange + 1;
1756                 range.length = sr->endRange - sr->startRange + 1;
1757             } else {
1758                 // Should only move cursor to a line.
1759                 range.location = sr->lineNum + 1;
1760                 range.length = 0;
1761             }
1763             [dict setObject:NSStringFromRange(range) forKey:@"selectionRange"];
1764         } else {
1765             ASLogErr(@"Xcode selection range size mismatch! got=%d expected=%d",
1766                     length, sizeof(MMXcodeSelectionRange));
1767         }
1768     }
1770     // 3. Extract Spotlight search text (if any)
1771     NSAppleEventDescriptor *spotlightdesc = 
1772             [desc paramDescriptorForKeyword:keyAESearchText];
1773     if (spotlightdesc)
1774         [dict setObject:[spotlightdesc stringValue] forKey:@"searchText"];
1776     return dict;
1779 #ifdef MM_ENABLE_PLUGINS
1780 - (void)removePlugInMenu
1782     if ([plugInMenuItem menu])
1783         [[plugInMenuItem menu] removeItem:plugInMenuItem];
1786 - (void)addPlugInMenuToMenu:(NSMenu *)mainMenu
1788     NSMenu *windowsMenu = [mainMenu findWindowsMenu];
1790     if ([[plugInMenuItem submenu] numberOfItems] > 0) {
1791         int idx = windowsMenu ? [mainMenu indexOfItemWithSubmenu:windowsMenu]
1792                               : -1;
1793         if (idx > 0) {
1794             [mainMenu insertItem:plugInMenuItem atIndex:idx];
1795         } else {
1796             [mainMenu addItem:plugInMenuItem];
1797         }
1798     }
1800 #endif
1802 - (void)scheduleVimControllerPreloadAfterDelay:(NSTimeInterval)delay
1804     [self performSelector:@selector(preloadVimController:)
1805                withObject:nil
1806                afterDelay:delay];
1809 - (void)cancelVimControllerPreloadRequests
1811     [NSObject cancelPreviousPerformRequestsWithTarget:self
1812             selector:@selector(preloadVimController:)
1813               object:nil];
1816 - (void)preloadVimController:(id)sender
1818     // We only allow preloading of one Vim process at a time (to avoid hogging
1819     // CPU), so schedule another preload in a little while if necessary.
1820     if (-1 != preloadPid) {
1821         [self scheduleVimControllerPreloadAfterDelay:2];
1822         return;
1823     }
1825     if ([cachedVimControllers count] >= [self maxPreloadCacheSize])
1826         return;
1828     preloadPid = [self launchVimProcessWithArguments:
1829             [NSArray arrayWithObject:@"--mmwaitforack"]];
1832 - (int)maxPreloadCacheSize
1834     // The maximum number of Vim processes to keep in the cache can be
1835     // controlled via the user default "MMPreloadCacheSize".
1836     int maxCacheSize = [[NSUserDefaults standardUserDefaults]
1837             integerForKey:MMPreloadCacheSizeKey];
1838     if (maxCacheSize < 0) maxCacheSize = 0;
1839     else if (maxCacheSize > 10) maxCacheSize = 10;
1841     return maxCacheSize;
1844 - (MMVimController *)takeVimControllerFromCache
1846     // NOTE: After calling this message the backend corresponding to the
1847     // returned vim controller must be sent an acknowledgeConnection message,
1848     // else the vim process will be stuck.
1849     //
1850     // This method may return nil even though the cache might be non-empty; the
1851     // caller should handle this by starting a new Vim process.
1853     int i, count = [cachedVimControllers count];
1854     if (0 == count) return nil;
1856     // Locate the first Vim controller with up-to-date rc-files sourced.
1857     NSDate *rcDate = [self rcFilesModificationDate];
1858     for (i = 0; i < count; ++i) {
1859         MMVimController *vc = [cachedVimControllers objectAtIndex:i];
1860         NSDate *date = [vc creationDate];
1861         if ([date compare:rcDate] != NSOrderedAscending)
1862             break;
1863     }
1865     if (i > 0) {
1866         // Clear out cache entries whose vimrc/gvimrc files were sourced before
1867         // the latest modification date for those files.  This ensures that the
1868         // latest rc-files are always sourced for new windows.
1869         [self clearPreloadCacheWithCount:i];
1870     }
1872     if ([cachedVimControllers count] == 0) {
1873         [self scheduleVimControllerPreloadAfterDelay:2.0];
1874         return nil;
1875     }
1877     MMVimController *vc = [cachedVimControllers objectAtIndex:0];
1878     [vimControllers addObject:vc];
1879     [cachedVimControllers removeObjectAtIndex:0];
1880     [vc setIsPreloading:NO];
1882     // If the Vim process has finished loading then the window will displayed
1883     // now, otherwise it will be displayed when the OpenWindowMsgID message is
1884     // received.
1885     [[vc windowController] showWindow];
1887     // Since we've taken one controller from the cache we take the opportunity
1888     // to preload another.
1889     [self scheduleVimControllerPreloadAfterDelay:1];
1891     return vc;
1894 - (void)clearPreloadCacheWithCount:(int)count
1896     // Remove the 'count' first entries in the preload cache.  It is assumed
1897     // that objects are added/removed from the cache in a FIFO manner so that
1898     // this effectively clears the 'count' oldest entries.
1899     // If 'count' is negative, then the entire cache is cleared.
1901     if ([cachedVimControllers count] == 0 || count == 0)
1902         return;
1904     if (count < 0)
1905         count = [cachedVimControllers count];
1907     // Make sure the preloaded Vim processes get killed or they'll just hang
1908     // around being useless until MacVim is terminated.
1909     NSEnumerator *e = [cachedVimControllers objectEnumerator];
1910     MMVimController *vc;
1911     int n = count;
1912     while ((vc = [e nextObject]) && n-- > 0) {
1913         [[NSNotificationCenter defaultCenter] removeObserver:vc];
1914         [vc sendMessage:TerminateNowMsgID data:nil];
1916         // Since the preloaded processes were killed "prematurely" we have to
1917         // manually tell them to cleanup (it is not enough to simply release
1918         // them since deallocation and cleanup are separated).
1919         [vc cleanup];
1920     }
1922     n = count;
1923     while (n-- > 0 && [cachedVimControllers count] > 0)
1924         [cachedVimControllers removeObjectAtIndex:0];
1926     // There is a small delay before the Vim process actually exits so wait a
1927     // little before trying to reap the child process.  If the process still
1928     // hasn't exited after this wait it won't be reaped until the next time
1929     // reapChildProcesses: is called (but this should be harmless).
1930     [self performSelector:@selector(reapChildProcesses:)
1931                withObject:nil
1932                afterDelay:0.1];
1935 - (void)rebuildPreloadCache
1937     if ([self maxPreloadCacheSize] > 0) {
1938         [self clearPreloadCacheWithCount:-1];
1939         [self cancelVimControllerPreloadRequests];
1940         [self scheduleVimControllerPreloadAfterDelay:1.0];
1941     }
1945 // HACK: fileAttributesAtPath was deprecated in 10.5
1946 #if (MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5)
1947 #define MM_fileAttributes(fm,p) [fm attributesOfItemAtPath:p error:NULL]
1948 #else
1949 #define MM_fileAttributes(fm,p) [fm fileAttributesAtPath:p traverseLink:YES]
1950 #endif
1951 - (NSDate *)rcFilesModificationDate
1953     // Check modification dates for ~/.vimrc and ~/.gvimrc and return the
1954     // latest modification date.  If ~/.vimrc does not exist, check ~/_vimrc
1955     // and similarly for gvimrc.
1956     // Returns distantPath if no rc files were found.
1958     NSDate *date = [NSDate distantPast];
1959     NSFileManager *fm = [NSFileManager defaultManager];
1961     NSString *path = [@"~/.vimrc" stringByExpandingTildeInPath];
1962     NSDictionary *attr = MM_fileAttributes(fm, path);
1963     if (!attr) {
1964         path = [@"~/_vimrc" stringByExpandingTildeInPath];
1965         attr = MM_fileAttributes(fm, path);
1966     }
1967     NSDate *modDate = [attr objectForKey:NSFileModificationDate];
1968     if (modDate)
1969         date = modDate;
1971     path = [@"~/.gvimrc" stringByExpandingTildeInPath];
1972     attr = MM_fileAttributes(fm, path);
1973     if (!attr) {
1974         path = [@"~/_gvimrc" stringByExpandingTildeInPath];
1975         attr = MM_fileAttributes(fm, path);
1976     }
1977     modDate = [attr objectForKey:NSFileModificationDate];
1978     if (modDate)
1979         date = [date laterDate:modDate];
1981     return date;
1983 #undef MM_fileAttributes
1985 - (BOOL)openVimControllerWithArguments:(NSDictionary *)arguments
1987     MMVimController *vc = [self findUnusedEditor];
1988     if (vc) {
1989         // Open files in an already open window.
1990         [[[vc windowController] window] makeKeyAndOrderFront:self];
1991         [vc passArguments:arguments];
1992     } else if ((vc = [self takeVimControllerFromCache])) {
1993         // Open files in a new window using a cached vim controller.  This
1994         // requires virtually no loading time so the new window will pop up
1995         // instantaneously.
1996         [vc passArguments:arguments];
1997         [[vc backendProxy] acknowledgeConnection];
1998     } else {
1999         // Open files in a launching Vim process or start a new process.  This
2000         // may take 1-2 seconds so there will be a visible delay before the
2001         // window appears on screen.
2002         int pid = [self findLaunchingProcessWithoutArguments];
2003         if (-1 == pid) {
2004             pid = [self launchVimProcessWithArguments:nil];
2005             if (-1 == pid)
2006                 return NO;
2007         }
2009         // TODO: If the Vim process fails to start, or if it changes PID,
2010         // then the memory allocated for these parameters will leak.
2011         // Ensure that this cannot happen or somehow detect it.
2013         if ([arguments count] > 0)
2014             [pidArguments setObject:arguments
2015                              forKey:[NSNumber numberWithInt:pid]];
2016     }
2018     return YES;
2021 - (void)activateWhenNextWindowOpens
2023     ASLogDebug(@"Activate MacVim when next window opens");
2024     shouldActivateWhenNextWindowOpens = YES;
2027 - (void)startWatchingVimDir
2029 #if (MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5)
2030     if (fsEventStream)
2031         return;
2032     if (NULL == FSEventStreamStart)
2033         return; // FSEvent functions are weakly linked
2035     NSString *path = [@"~/.vim" stringByExpandingTildeInPath];
2036     NSArray *pathsToWatch = [NSArray arrayWithObject:path];
2038     fsEventStream = FSEventStreamCreate(NULL, &fsEventCallback, NULL,
2039             (CFArrayRef)pathsToWatch, kFSEventStreamEventIdSinceNow,
2040             MMEventStreamLatency, kFSEventStreamCreateFlagNone);
2042     FSEventStreamScheduleWithRunLoop(fsEventStream,
2043             [[NSRunLoop currentRunLoop] getCFRunLoop],
2044             kCFRunLoopDefaultMode);
2046     FSEventStreamStart(fsEventStream);
2047     ASLogDebug(@"Started FS event stream");
2048 #endif
2051 - (void)stopWatchingVimDir
2053 #if (MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5)
2054     if (NULL == FSEventStreamStop)
2055         return; // FSEvent functions are weakly linked
2057     if (fsEventStream) {
2058         FSEventStreamStop(fsEventStream);
2059         FSEventStreamInvalidate(fsEventStream);
2060         FSEventStreamRelease(fsEventStream);
2061         fsEventStream = NULL;
2062         ASLogDebug(@"Stopped FS event stream");
2063     }
2064 #endif
2068 - (void)handleFSEvent
2070     [self clearPreloadCacheWithCount:-1];
2072     // Several FS events may arrive in quick succession so make sure to cancel
2073     // any previous preload requests before making a new one.
2074     [self cancelVimControllerPreloadRequests];
2075     [self scheduleVimControllerPreloadAfterDelay:0.5];
2078 - (void)loadDefaultFont
2080     // It is possible to set a user default to avoid loading the default font
2081     // (this cuts down on startup time).
2082     if (![[NSUserDefaults standardUserDefaults] boolForKey:MMLoadDefaultFontKey]
2083             || fontContainerRef) {
2084         ASLogInfo(@"Skip loading of the default font...");
2085         return;
2086     }
2088     ASLogInfo(@"Loading the default font...");
2090     // Load all fonts in the Resouces folder of the app bundle.
2091     NSString *fontsFolder = [[NSBundle mainBundle] resourcePath];
2092     if (fontsFolder) {
2093         NSURL *fontsURL = [NSURL fileURLWithPath:fontsFolder];
2094         if (fontsURL) {
2095             FSRef fsRef;
2096             CFURLGetFSRef((CFURLRef)fontsURL, &fsRef);
2098 #if (MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5)
2099             // This is the font activation API for OS X 10.5.  Only compile
2100             // this code if we're building on OS X 10.5 or later.
2101             if (NULL != ATSFontActivateFromFileReference) { // Weakly linked
2102                 ATSFontActivateFromFileReference(&fsRef, kATSFontContextLocal,
2103                                                  kATSFontFormatUnspecified,
2104                                                  NULL, kATSOptionFlagsDefault,
2105                                                  &fontContainerRef);
2106             }
2107 #endif
2108 #if (MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_5)
2109             // The following font activation API was deprecated in OS X 10.5.
2110             // Don't compile this code unless we're targeting OS X 10.4.
2111             FSSpec fsSpec;
2112             if (fontContainerRef == 0 &&
2113                     FSGetCatalogInfo(&fsRef, kFSCatInfoNone, NULL, NULL,
2114                                      &fsSpec, NULL) == noErr) {
2115                 ATSFontActivateFromFileSpecification(&fsSpec,
2116                         kATSFontContextLocal, kATSFontFormatUnspecified, NULL,
2117                         kATSOptionFlagsDefault, &fontContainerRef);
2118             }
2119 #endif
2120         }
2121     }
2123     if (!fontContainerRef) {
2124         ASLogNotice(@"Failed to activate the default font (the app bundle "
2125                     "may be incomplete)");
2126     }
2129 - (int)executeInLoginShell:(NSString *)path arguments:(NSArray *)args
2131     // Start a login shell and execute the command 'path' with arguments 'args'
2132     // in the shell.  This ensures that user environment variables are set even
2133     // when MacVim was started from the Finder.
2135     int pid = -1;
2136     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
2138     // Determine which shell to use to execute the command.  The user
2139     // may decide which shell to use by setting a user default or the
2140     // $SHELL environment variable.
2141     NSString *shell = [ud stringForKey:MMLoginShellCommandKey];
2142     if (!shell || [shell length] == 0)
2143         shell = [[[NSProcessInfo processInfo] environment]
2144             objectForKey:@"SHELL"];
2145     if (!shell)
2146         shell = @"/bin/bash";
2148     // Bash needs the '-l' flag to launch a login shell.  The user may add
2149     // flags by setting a user default.
2150     NSString *shellArgument = [ud stringForKey:MMLoginShellArgumentKey];
2151     if (!shellArgument || [shellArgument length] == 0) {
2152         if ([[shell lastPathComponent] isEqual:@"bash"])
2153             shellArgument = @"-l";
2154         else
2155             shellArgument = nil;
2156     }
2158     // Build input string to pipe to the login shell.
2159     NSMutableString *input = [NSMutableString stringWithFormat:
2160             @"exec \"%@\"", path];
2161     if (args) {
2162         // Append all arguments, making sure they are properly quoted, even
2163         // when they contain single quotes.
2164         NSEnumerator *e = [args objectEnumerator];
2165         id obj;
2167         while ((obj = [e nextObject])) {
2168             NSMutableString *arg = [NSMutableString stringWithString:obj];
2169             [arg replaceOccurrencesOfString:@"'" withString:@"'\"'\"'"
2170                                     options:NSLiteralSearch
2171                                       range:NSMakeRange(0, [arg length])];
2172             [input appendFormat:@" '%@'", arg];
2173         }
2174     }
2176     // Build the argument vector used to start the login shell.
2177     NSString *shellArg0 = [NSString stringWithFormat:@"-%@",
2178              [shell lastPathComponent]];
2179     char *shellArgv[3] = { (char *)[shellArg0 UTF8String], NULL, NULL };
2180     if (shellArgument)
2181         shellArgv[1] = (char *)[shellArgument UTF8String];
2183     // Get the C string representation of the shell path before the fork since
2184     // we must not call Foundation functions after a fork.
2185     const char *shellPath = [shell fileSystemRepresentation];
2187     // Fork and execute the process.
2188     int ds[2];
2189     if (pipe(ds)) return -1;
2191     pid = fork();
2192     if (pid == -1) {
2193         return -1;
2194     } else if (pid == 0) {
2195         // Child process
2197         if (close(ds[1]) == -1) exit(255);
2198         if (dup2(ds[0], 0) == -1) exit(255);
2200         // Without the following call warning messages like this appear on the
2201         // console:
2202         //     com.apple.launchd[69] : Stray process with PGID equal to this
2203         //                             dead job: PID 1589 PPID 1 Vim
2204         setsid();
2206         execv(shellPath, shellArgv);
2208         // Never reached unless execv fails
2209         exit(255);
2210     } else {
2211         // Parent process
2212         if (close(ds[0]) == -1) return -1;
2214         // Send input to execute to the child process
2215         [input appendString:@"\n"];
2216         int bytes = [input lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
2218         if (write(ds[1], [input UTF8String], bytes) != bytes) return -1;
2219         if (close(ds[1]) == -1) return -1;
2221         ++numChildProcesses;
2222         ASLogDebug(@"new process pid=%d (count=%d)", pid, numChildProcesses);
2223     }
2225     return pid;
2228 - (void)reapChildProcesses:(id)sender
2230     // NOTE: numChildProcesses (currently) only counts the number of Vim
2231     // processes that have been started with executeInLoginShell::.  If other
2232     // processes are spawned this code may need to be adjusted (or
2233     // numChildProcesses needs to be incremented when such a process is
2234     // started).
2235     while (numChildProcesses > 0) {
2236         int status = 0;
2237         int pid = waitpid(-1, &status, WNOHANG);
2238         if (pid <= 0)
2239             break;
2241         ASLogDebug(@"Wait for pid=%d complete", pid);
2242         --numChildProcesses;
2243     }
2246 - (void)processInputQueues:(id)sender
2248     // NOTE: Because we use distributed objects it is quite possible for this
2249     // function to be re-entered.  This can cause all sorts of unexpected
2250     // problems so we guard against it here so that the rest of the code does
2251     // not need to worry about it.
2253     // The processing flag is > 0 if this function is already on the call
2254     // stack; < 0 if this function was also re-entered.
2255     if (processingFlag != 0) {
2256         ASLogDebug(@"BUSY!");
2257         processingFlag = -1;
2258         return;
2259     }
2261     // NOTE: Be _very_ careful that no exceptions can be raised between here
2262     // and the point at which 'processingFlag' is reset.  Otherwise the above
2263     // test could end up always failing and no input queues would ever be
2264     // processed!
2265     processingFlag = 1;
2267     // NOTE: New input may arrive while we're busy processing; we deal with
2268     // this by putting the current queue aside and creating a new input queue
2269     // for future input.
2270     NSDictionary *queues = inputQueues;
2271     inputQueues = [NSMutableDictionary new];
2273     // Pass each input queue on to the vim controller with matching
2274     // identifier (and note that it could be cached).
2275     NSEnumerator *e = [queues keyEnumerator];
2276     NSNumber *key;
2277     while ((key = [e nextObject])) {
2278         unsigned ukey = [key unsignedIntValue];
2279         int i = 0, count = [vimControllers count];
2280         for (i = 0; i < count; ++i) {
2281             MMVimController *vc = [vimControllers objectAtIndex:i];
2282             if (ukey == [vc vimControllerId]) {
2283                 [vc processInputQueue:[queues objectForKey:key]]; // !exceptions
2284                 break;
2285             }
2286         }
2288         if (i < count) continue;
2290         count = [cachedVimControllers count];
2291         for (i = 0; i < count; ++i) {
2292             MMVimController *vc = [cachedVimControllers objectAtIndex:i];
2293             if (ukey == [vc vimControllerId]) {
2294                 [vc processInputQueue:[queues objectForKey:key]]; // !exceptions
2295                 break;
2296             }
2297         }
2299         if (i == count) {
2300             ASLogWarn(@"No Vim controller for identifier=%d", ukey);
2301         }
2302     }
2304     [queues release];
2306     // If new input arrived while we were processing it would have been
2307     // blocked so we have to schedule it to be processed again.
2308     if (processingFlag < 0)
2309         [self performSelectorOnMainThread:@selector(processInputQueues:)
2310                                withObject:nil
2311                             waitUntilDone:NO
2312                                     modes:[NSArray arrayWithObjects:
2313                                            NSDefaultRunLoopMode,
2314                                            NSEventTrackingRunLoopMode, nil]];
2316     processingFlag = 0;
2319 - (void)addVimController:(MMVimController *)vc
2321     ASLogDebug(@"Add Vim controller pid=%d id=%d",
2322             [vc pid], [vc vimControllerId]);
2324     int pid = [vc pid];
2325     NSNumber *pidKey = [NSNumber numberWithInt:pid];
2327     if (preloadPid == pid) {
2328         // This controller was preloaded, so add it to the cache and
2329         // schedule another vim process to be preloaded.
2330         preloadPid = -1;
2331         [vc setIsPreloading:YES];
2332         [cachedVimControllers addObject:vc];
2333         [self scheduleVimControllerPreloadAfterDelay:1];
2334     } else {
2335         [vimControllers addObject:vc];
2337         id args = [pidArguments objectForKey:pidKey];
2338         if (args && [NSNull null] != args)
2339             [vc passArguments:args];
2341         // HACK!  MacVim does not get activated if it is launched from the
2342         // terminal, so we forcibly activate here unless it is an untitled
2343         // window opening.  Untitled windows are treated differently, else
2344         // MacVim would steal the focus if another app was activated while the
2345         // untitled window was loading.
2346         if (!args || args != [NSNull null])
2347             [self activateWhenNextWindowOpens];
2349         if (args)
2350             [pidArguments removeObjectForKey:pidKey];
2351     }
2354 @end // MMAppController (Private)