8b7dac983fdb2deb24a799f404bd253cff5902fd
[MacVim.git] / src / MacVim / MMAppController.m
blob8b7dac983fdb2deb24a799f404bd253cff5902fd
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                     workingDirectory:(NSString *)cwd;
112 - (NSArray *)filterFilesAndNotify:(NSArray *)files;
113 - (NSArray *)filterOpenFiles:(NSArray *)filenames
114                openFilesDict:(NSDictionary **)openFiles;
115 #if MM_HANDLE_XCODE_MOD_EVENT
116 - (void)handleXcodeModEvent:(NSAppleEventDescriptor *)event
117                  replyEvent:(NSAppleEventDescriptor *)reply;
118 #endif
119 - (void)handleGetURLEvent:(NSAppleEventDescriptor *)event
120                replyEvent:(NSAppleEventDescriptor *)reply;
121 - (NSMutableDictionary *)extractArgumentsFromOdocEvent:
122     (NSAppleEventDescriptor *)desc;
123 - (void)scheduleVimControllerPreloadAfterDelay:(NSTimeInterval)delay;
124 - (void)cancelVimControllerPreloadRequests;
125 - (void)preloadVimController:(id)sender;
126 - (int)maxPreloadCacheSize;
127 - (MMVimController *)takeVimControllerFromCache;
128 - (void)clearPreloadCacheWithCount:(int)count;
129 - (void)rebuildPreloadCache;
130 - (NSDate *)rcFilesModificationDate;
131 - (BOOL)openVimControllerWithArguments:(NSDictionary *)arguments;
132 - (void)activateWhenNextWindowOpens;
133 - (void)startWatchingVimDir;
134 - (void)stopWatchingVimDir;
135 - (void)handleFSEvent;
136 - (void)loadDefaultFont;
137 - (int)executeInLoginShell:(NSString *)path arguments:(NSArray *)args;
138 - (void)reapChildProcesses:(id)sender;
139 - (void)processInputQueues:(id)sender;
140 - (void)addVimController:(MMVimController *)vc;
141 - (NSDictionary *)convertVimControllerArguments:(NSDictionary *)args
142                                   toCommandLine:(NSArray **)cmdline;
143 - (NSString *)workingDirectoryForArguments:(NSDictionary *)args;
144 - (NSScreen *)screenContainingPoint:(NSPoint)pt;
146 #ifdef MM_ENABLE_PLUGINS
147 - (void)removePlugInMenu;
148 - (void)addPlugInMenuToMenu:(NSMenu *)mainMenu;
149 #endif
150 @end
154 #if (MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5)
155     static void
156 fsEventCallback(ConstFSEventStreamRef streamRef,
157                 void *clientCallBackInfo,
158                 size_t numEvents,
159                 void *eventPaths,
160                 const FSEventStreamEventFlags eventFlags[],
161                 const FSEventStreamEventId eventIds[])
163     [[MMAppController sharedInstance] handleFSEvent];
165 #endif
167 @implementation MMAppController
169 + (void)initialize
171     static BOOL initDone = NO;
172     if (initDone) return;
173     initDone = YES;
175     ASLInit();
177     // HACK! The following user default must be reset, else Ctrl-q (or
178     // whichever key is specified by the default) will be blocked by the input
179     // manager (interpretKeyEvents: swallows that key).  (We can't use
180     // NSUserDefaults since it only allows us to write to the registration
181     // domain and this preference has "higher precedence" than that so such a
182     // change would have no effect.)
183     CFPreferencesSetAppValue(CFSTR("NSQuotedKeystrokeBinding"),
184                              CFSTR(""),
185                              kCFPreferencesCurrentApplication);
187     // Also disable NSRepeatCountBinding -- it is not enabled by default, but
188     // it does not make much sense to support it since Vim has its own way of
189     // dealing with repeat counts.
190     CFPreferencesSetAppValue(CFSTR("NSRepeatCountBinding"),
191                              CFSTR(""),
192                              kCFPreferencesCurrentApplication);
193     
194     NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:
195         [NSNumber numberWithBool:NO],   MMNoWindowKey,
196         [NSNumber numberWithInt:64],    MMTabMinWidthKey,
197         [NSNumber numberWithInt:6*64],  MMTabMaxWidthKey,
198         [NSNumber numberWithInt:132],   MMTabOptimumWidthKey,
199         [NSNumber numberWithBool:YES],  MMShowAddTabButtonKey,
200         [NSNumber numberWithInt:2],     MMTextInsetLeftKey,
201         [NSNumber numberWithInt:1],     MMTextInsetRightKey,
202         [NSNumber numberWithInt:1],     MMTextInsetTopKey,
203         [NSNumber numberWithInt:1],     MMTextInsetBottomKey,
204         @"MMTypesetter",                MMTypesetterKey,
205         [NSNumber numberWithFloat:1],   MMCellWidthMultiplierKey,
206         [NSNumber numberWithFloat:-1],  MMBaselineOffsetKey,
207         [NSNumber numberWithBool:YES],  MMTranslateCtrlClickKey,
208         [NSNumber numberWithInt:0],     MMOpenInCurrentWindowKey,
209         [NSNumber numberWithBool:NO],   MMNoFontSubstitutionKey,
210         [NSNumber numberWithBool:YES],  MMLoginShellKey,
211         [NSNumber numberWithInt:0],     MMRendererKey,
212         [NSNumber numberWithInt:MMUntitledWindowAlways],
213                                         MMUntitledWindowKey,
214         [NSNumber numberWithBool:NO],   MMTexturedWindowKey,
215         [NSNumber numberWithBool:NO],   MMZoomBothKey,
216         @"",                            MMLoginShellCommandKey,
217         @"",                            MMLoginShellArgumentKey,
218         [NSNumber numberWithBool:YES],  MMDialogsTrackPwdKey,
219 #ifdef MM_ENABLE_PLUGINS
220         [NSNumber numberWithBool:YES],  MMShowLeftPlugInContainerKey,
221 #endif
222         [NSNumber numberWithInt:3],     MMOpenLayoutKey,
223         [NSNumber numberWithBool:NO],   MMVerticalSplitKey,
224         [NSNumber numberWithInt:0],     MMPreloadCacheSizeKey,
225         [NSNumber numberWithInt:0],     MMLastWindowClosedBehaviorKey,
226         [NSNumber numberWithBool:YES],  MMLoadDefaultFontKey,
227 #ifdef INCLUDE_OLD_IM_CODE
228         [NSNumber numberWithBool:YES],  MMUseInlineImKey,
229 #endif // INCLUDE_OLD_IM_CODE
230         nil];
232     [[NSUserDefaults standardUserDefaults] registerDefaults:dict];
234     NSArray *types = [NSArray arrayWithObject:NSStringPboardType];
235     [NSApp registerServicesMenuSendTypes:types returnTypes:types];
237     // NOTE: Set the current directory to user's home directory, otherwise it
238     // will default to the root directory.  (This matters since new Vim
239     // processes inherit MacVim's environment variables.)
240     [[NSFileManager defaultManager] changeCurrentDirectoryPath:
241             NSHomeDirectory()];
244 - (id)init
246     if (!(self = [super init])) return nil;
248     [self loadDefaultFont];
250     vimControllers = [NSMutableArray new];
251     cachedVimControllers = [NSMutableArray new];
252     preloadPid = -1;
253     pidArguments = [NSMutableDictionary new];
254     inputQueues = [NSMutableDictionary new];
256 #ifdef MM_ENABLE_PLUGINS
257     NSString *plugInTitle = NSLocalizedString(@"Plug-In",
258                                               @"Plug-In menu title");
259     plugInMenuItem = [[NSMenuItem alloc] initWithTitle:plugInTitle
260                                                 action:NULL
261                                          keyEquivalent:@""];
262     NSMenu *submenu = [[NSMenu alloc] initWithTitle:plugInTitle];
263     [plugInMenuItem setSubmenu:submenu];
264     [submenu release];
265 #endif
267     // NOTE: Do not use the default connection since the Logitech Control
268     // Center (LCC) input manager steals and this would cause MacVim to
269     // never open any windows.  (This is a bug in LCC but since they are
270     // unlikely to fix it, we graciously give them the default connection.)
271     connection = [[NSConnection alloc] initWithReceivePort:[NSPort port]
272                                                   sendPort:nil];
273     [connection setRootObject:self];
274     [connection setRequestTimeout:MMRequestTimeout];
275     [connection setReplyTimeout:MMReplyTimeout];
277     // NOTE!  If the name of the connection changes here it must also be
278     // updated in MMBackend.m.
279     NSString *name = [NSString stringWithFormat:@"%@-connection",
280              [[NSBundle mainBundle] bundlePath]];
281     if (![connection registerName:name]) {
282         ASLogCrit(@"Failed to register connection with name '%@'", name);
283         [connection release];  connection = nil;
284     }
286     return self;
289 - (void)dealloc
291     ASLogDebug(@"");
293     [connection release];  connection = nil;
294     [inputQueues release];  inputQueues = nil;
295     [pidArguments release];  pidArguments = nil;
296     [vimControllers release];  vimControllers = nil;
297     [cachedVimControllers release];  cachedVimControllers = nil;
298     [openSelectionString release];  openSelectionString = nil;
299     [recentFilesMenuItem release];  recentFilesMenuItem = nil;
300     [defaultMainMenu release];  defaultMainMenu = nil;
301 #ifdef MM_ENABLE_PLUGINS
302     [plugInMenuItem release];  plugInMenuItem = nil;
303 #endif
304     [appMenuItemTemplate release];  appMenuItemTemplate = nil;
306     [super dealloc];
309 - (void)applicationWillFinishLaunching:(NSNotification *)notification
311     // Remember the default menu so that it can be restored if the user closes
312     // all editor windows.
313     defaultMainMenu = [[NSApp mainMenu] retain];
315     // Store a copy of the default app menu so we can use this as a template
316     // for all other menus.  We make a copy here because the "Services" menu
317     // will not yet have been populated at this time.  If we don't we get
318     // problems trying to set key equivalents later on because they might clash
319     // with items on the "Services" menu.
320     appMenuItemTemplate = [defaultMainMenu itemAtIndex:0];
321     appMenuItemTemplate = [appMenuItemTemplate copy];
323     // Set up the "Open Recent" menu. See
324     //   http://lapcatsoftware.com/blog/2007/07/10/
325     //     working-without-a-nib-part-5-open-recent-menu/
326     // and
327     //   http://www.cocoabuilder.com/archive/message/cocoa/2007/8/15/187793
328     // for more information.
329     //
330     // The menu itself is created in MainMenu.nib but we still seem to have to
331     // hack around a bit to get it to work.  (This has to be done in
332     // applicationWillFinishLaunching at the latest, otherwise it doesn't
333     // work.)
334     NSMenu *fileMenu = [defaultMainMenu findFileMenu];
335     if (fileMenu) {
336         int idx = [fileMenu indexOfItemWithAction:@selector(fileOpen:)];
337         if (idx >= 0 && idx+1 < [fileMenu numberOfItems])
339         recentFilesMenuItem = [fileMenu itemWithTitle:@"Open Recent"];
340         [[recentFilesMenuItem submenu] performSelector:@selector(_setMenuName:)
341                                         withObject:@"NSRecentDocumentsMenu"];
343         // Note: The "Recent Files" menu must be moved around since there is no
344         // -[NSApp setRecentFilesMenu:] method.  We keep a reference to it to
345         // facilitate this move (see setMainMenu: below).
346         [recentFilesMenuItem retain];
347     }
349 #if MM_HANDLE_XCODE_MOD_EVENT
350     [[NSAppleEventManager sharedAppleEventManager]
351             setEventHandler:self
352                 andSelector:@selector(handleXcodeModEvent:replyEvent:)
353               forEventClass:'KAHL'
354                  andEventID:'MOD '];
355 #endif
357     // Register 'mvim://' URL handler
358     [[NSAppleEventManager sharedAppleEventManager]
359             setEventHandler:self
360                 andSelector:@selector(handleGetURLEvent:replyEvent:)
361               forEventClass:kInternetEventClass
362                  andEventID:kAEGetURL];
364     // Disable the default Cocoa "Key Bindings" since they interfere with the
365     // way Vim handles keyboard input.  Cocoa reads bindings from
366     //     /System/Library/Frameworks/AppKit.framework/Resources/
367     //                                                  StandardKeyBinding.dict
368     // and
369     //     ~/Library/KeyBindings/DefaultKeyBinding.dict
370     // To avoid having the user accidentally break keyboard handling (by
371     // modifying the latter in some unexpected way) in MacVim we load our own
372     // key binding dictionary from Resource/KeyBinding.plist.  We can't disable
373     // the bindings completely since it would break keyboard handling in
374     // dialogs so the our custom dictionary contains all the entries from the
375     // former location.
376     //
377     // It is possible to disable key bindings completely by not calling
378     // interpretKeyEvents: in keyDown: but this also disables key bindings used
379     // by certain input methods.  E.g.  Ctrl-Shift-; would no longer work in
380     // the Kotoeri input manager.
381     //
382     // To solve this problem we access a private API and set the key binding
383     // dictionary to our own custom dictionary here.  At this time Cocoa will
384     // have already read the above mentioned dictionaries so it (hopefully)
385     // won't try to change the key binding dictionary again after this point.
386     NSKeyBindingManager *mgr = [NSKeyBindingManager sharedKeyBindingManager];
387     NSBundle *mainBundle = [NSBundle mainBundle];
388     NSString *path = [mainBundle pathForResource:@"KeyBinding"
389                                           ofType:@"plist"];
390     NSDictionary *dict = [NSDictionary dictionaryWithContentsOfFile:path];
391     if (mgr && dict) {
392         [mgr setDictionary:dict];
393     } else {
394         ASLogNotice(@"Failed to override the Cocoa key bindings.  Keyboard "
395                 "input may behave strangely as a result (path=%@).", path);
396     }
399 - (void)applicationDidFinishLaunching:(NSNotification *)notification
401     [NSApp setServicesProvider:self];
402 #ifdef MM_ENABLE_PLUGINS
403     [[MMPlugInManager sharedManager] loadAllPlugIns];
404 #endif
406     if ([self maxPreloadCacheSize] > 0) {
407         [self scheduleVimControllerPreloadAfterDelay:2];
408         [self startWatchingVimDir];
409     }
411     ASLogInfo(@"MacVim finished launching");
414 - (BOOL)applicationShouldOpenUntitledFile:(NSApplication *)sender
416     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
417     NSAppleEventManager *aem = [NSAppleEventManager sharedAppleEventManager];
418     NSAppleEventDescriptor *desc = [aem currentAppleEvent];
420     // The user default MMUntitledWindow can be set to control whether an
421     // untitled window should open on 'Open' and 'Reopen' events.
422     int untitledWindowFlag = [ud integerForKey:MMUntitledWindowKey];
424     BOOL isAppOpenEvent = [desc eventID] == kAEOpenApplication;
425     if (isAppOpenEvent && (untitledWindowFlag & MMUntitledWindowOnOpen) == 0)
426         return NO;
428     BOOL isAppReopenEvent = [desc eventID] == kAEReopenApplication;
429     if (isAppReopenEvent
430             && (untitledWindowFlag & MMUntitledWindowOnReopen) == 0)
431         return NO;
433     // When a process is started from the command line, the 'Open' event may
434     // contain a parameter to surpress the opening of an untitled window.
435     desc = [desc paramDescriptorForKeyword:keyAEPropData];
436     desc = [desc paramDescriptorForKeyword:keyMMUntitledWindow];
437     if (desc && ![desc booleanValue])
438         return NO;
440     // Never open an untitled window if there is at least one open window or if
441     // there are processes that are currently launching.
442     if ([vimControllers count] > 0 || [pidArguments count] > 0)
443         return NO;
445     // NOTE!  This way it possible to start the app with the command-line
446     // argument '-nowindow yes' and no window will be opened by default but
447     // this argument will only be heeded when the application is opening.
448     if (isAppOpenEvent && [ud boolForKey:MMNoWindowKey] == YES)
449         return NO;
451     return YES;
454 - (BOOL)applicationOpenUntitledFile:(NSApplication *)sender
456     ASLogDebug(@"Opening untitled window...");
457     [self newWindow:self];
458     return YES;
461 - (void)application:(NSApplication *)sender openFiles:(NSArray *)filenames
463     ASLogInfo(@"Opening files %@", filenames);
465     // Extract ODB/Xcode/Spotlight parameters from the current Apple event,
466     // sort the filenames, and then let openFiles:withArguments: do the heavy
467     // lifting.
469     if (!(filenames && [filenames count] > 0))
470         return;
472     // Sort filenames since the Finder doesn't take care in preserving the
473     // order in which files are selected anyway (and "sorted" is more
474     // predictable than "random").
475     if ([filenames count] > 1)
476         filenames = [filenames sortedArrayUsingSelector:
477                 @selector(localizedCompare:)];
479     // Extract ODB/Xcode/Spotlight parameters from the current Apple event
480     NSMutableDictionary *arguments = [self extractArgumentsFromOdocEvent:
481             [[NSAppleEventManager sharedAppleEventManager] currentAppleEvent]];
483     if ([self openFiles:filenames withArguments:arguments]) {
484         [NSApp replyToOpenOrPrint:NSApplicationDelegateReplySuccess];
485     } else {
486         // TODO: Notify user of failure?
487         [NSApp replyToOpenOrPrint:NSApplicationDelegateReplyFailure];
488     }
491 - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender
493     return (MMTerminateWhenLastWindowClosed ==
494             [[NSUserDefaults standardUserDefaults]
495                 integerForKey:MMLastWindowClosedBehaviorKey]);
498 - (NSApplicationTerminateReply)applicationShouldTerminate:
499     (NSApplication *)sender
501     // TODO: Follow Apple's guidelines for 'Graceful Application Termination'
502     // (in particular, allow user to review changes and save).
503     int reply = NSTerminateNow;
504     BOOL modifiedBuffers = NO;
506     // Go through windows, checking for modified buffers.  (Each Vim process
507     // tells MacVim when any buffer has been modified and MacVim sets the
508     // 'documentEdited' flag of the window correspondingly.)
509     NSEnumerator *e = [[NSApp windows] objectEnumerator];
510     id window;
511     while ((window = [e nextObject])) {
512         if ([window isDocumentEdited]) {
513             modifiedBuffers = YES;
514             break;
515         }
516     }
518     if (modifiedBuffers) {
519         NSAlert *alert = [[NSAlert alloc] init];
520         [alert setAlertStyle:NSWarningAlertStyle];
521         [alert addButtonWithTitle:NSLocalizedString(@"Quit",
522                 @"Dialog button")];
523         [alert addButtonWithTitle:NSLocalizedString(@"Cancel",
524                 @"Dialog button")];
525         [alert setMessageText:NSLocalizedString(@"Quit without saving?",
526                 @"Quit dialog with changed buffers, title")];
527         [alert setInformativeText:NSLocalizedString(
528                 @"There are modified buffers, "
529                 "if you quit now all changes will be lost.  Quit anyway?",
530                 @"Quit dialog with changed buffers, text")];
532         if ([alert runModal] != NSAlertFirstButtonReturn)
533             reply = NSTerminateCancel;
535         [alert release];
536     } else {
537         // No unmodified buffers, but give a warning if there are multiple
538         // windows and/or tabs open.
539         int numWindows = [vimControllers count];
540         int numTabs = 0;
542         // Count the number of open tabs
543         e = [vimControllers objectEnumerator];
544         id vc;
545         while ((vc = [e nextObject]))
546             numTabs += [[vc objectForVimStateKey:@"numTabs"] intValue];
548         if (numWindows > 1 || numTabs > 1) {
549             NSAlert *alert = [[NSAlert alloc] init];
550             [alert setAlertStyle:NSWarningAlertStyle];
551             [alert addButtonWithTitle:NSLocalizedString(@"Quit",
552                     @"Dialog button")];
553             [alert addButtonWithTitle:NSLocalizedString(@"Cancel",
554                     @"Dialog button")];
555             [alert setMessageText:NSLocalizedString(
556                     @"Are you sure you want to quit MacVim?",
557                     @"Quit dialog with no changed buffers, title")];
559             NSString *info = nil;
560             if (numWindows > 1) {
561                 if (numTabs > numWindows)
562                     info = [NSString stringWithFormat:NSLocalizedString(
563                             @"There are %d windows open in MacVim, with a "
564                             "total of %d tabs. Do you want to quit anyway?",
565                             @"Quit dialog with no changed buffers, text"),
566                          numWindows, numTabs];
567                 else
568                     info = [NSString stringWithFormat:NSLocalizedString(
569                             @"There are %d windows open in MacVim. "
570                             "Do you want to quit anyway?",
571                             @"Quit dialog with no changed buffers, text"),
572                         numWindows];
574             } else {
575                 info = [NSString stringWithFormat:NSLocalizedString(
576                         @"There are %d tabs open in MacVim. "
577                         "Do you want to quit anyway?",
578                         @"Quit dialog with no changed buffers, text"), 
579                      numTabs];
580             }
582             [alert setInformativeText:info];
584             if ([alert runModal] != NSAlertFirstButtonReturn)
585                 reply = NSTerminateCancel;
587             [alert release];
588         }
589     }
592     // Tell all Vim processes to terminate now (otherwise they'll leave swap
593     // files behind).
594     if (NSTerminateNow == reply) {
595         e = [vimControllers objectEnumerator];
596         id vc;
597         while ((vc = [e nextObject])) {
598             ASLogDebug(@"Terminate pid=%d", [vc pid]);
599             [vc sendMessage:TerminateNowMsgID data:nil];
600         }
602         e = [cachedVimControllers objectEnumerator];
603         while ((vc = [e nextObject])) {
604             ASLogDebug(@"Terminate pid=%d (cached)", [vc pid]);
605             [vc sendMessage:TerminateNowMsgID data:nil];
606         }
608         // If a Vim process is being preloaded as we quit we have to forcibly
609         // kill it since we have not established a connection yet.
610         if (preloadPid > 0) {
611             ASLogDebug(@"Kill incomplete preloaded process pid=%d", preloadPid);
612             kill(preloadPid, SIGKILL);
613         }
615         // If a Vim process was loading as we quit we also have to kill it.
616         e = [[pidArguments allKeys] objectEnumerator];
617         NSNumber *pidKey;
618         while ((pidKey = [e nextObject])) {
619             ASLogDebug(@"Kill incomplete process pid=%d", [pidKey intValue]);
620             kill([pidKey intValue], SIGKILL);
621         }
623         // Sleep a little to allow all the Vim processes to exit.
624         usleep(10000);
625     }
627     return reply;
630 - (void)applicationWillTerminate:(NSNotification *)notification
632     ASLogInfo(@"Terminating MacVim...");
634     [self stopWatchingVimDir];
636 #ifdef MM_ENABLE_PLUGINS
637     [[MMPlugInManager sharedManager] unloadAllPlugIns];
638 #endif
640 #if MM_HANDLE_XCODE_MOD_EVENT
641     [[NSAppleEventManager sharedAppleEventManager]
642             removeEventHandlerForEventClass:'KAHL'
643                                  andEventID:'MOD '];
644 #endif
646     // This will invalidate all connections (since they were spawned from this
647     // connection).
648     [connection invalidate];
650     // Deactivate the font we loaded from the app bundle.
651     // NOTE: This can take quite a while (~500 ms), so termination will be
652     // noticeably faster if loading of the default font is disabled.
653     if (fontContainerRef) {
654         ATSFontDeactivate(fontContainerRef, NULL, kATSOptionFlagsDefault);
655         fontContainerRef = 0;
656     }
658     [NSApp setDelegate:nil];
660     // Try to wait for all child processes to avoid leaving zombies behind (but
661     // don't wait around for too long).
662     NSDate *timeOutDate = [NSDate dateWithTimeIntervalSinceNow:2];
663     while ([timeOutDate timeIntervalSinceNow] > 0) {
664         [self reapChildProcesses:nil];
665         if (numChildProcesses <= 0)
666             break;
668         ASLogDebug(@"%d processes still left, hold on...", numChildProcesses);
670         // Run in NSConnectionReplyMode while waiting instead of calling e.g.
671         // usleep().  Otherwise incoming messages may clog up the DO queues and
672         // the outgoing TerminateNowMsgID sent earlier never reaches the Vim
673         // process.
674         // This has at least one side-effect, namely we may receive the
675         // annoying "dropping incoming DO message".  (E.g. this may happen if
676         // you quickly hit Cmd-n several times in a row and then immediately
677         // press Cmd-q, Enter.)
678         while (CFRunLoopRunInMode((CFStringRef)NSConnectionReplyMode,
679                 0.05, true) == kCFRunLoopRunHandledSource)
680             ;   // do nothing
681     }
683     if (numChildProcesses > 0) {
684         ASLogNotice(@"%d zombies left behind", numChildProcesses);
685     }
688 + (MMAppController *)sharedInstance
690     // Note: The app controller is a singleton which is instantiated in
691     // MainMenu.nib where it is also connected as the delegate of NSApp.
692     id delegate = [NSApp delegate];
693     return [delegate isKindOfClass:self] ? (MMAppController*)delegate : nil;
696 - (NSMenu *)defaultMainMenu
698     return defaultMainMenu;
701 - (NSMenuItem *)appMenuItemTemplate
703     return appMenuItemTemplate;
706 - (void)removeVimController:(id)controller
708     ASLogDebug(@"Remove Vim controller pid=%d id=%d (processingFlag=%d)",
709                [controller pid], [controller vimControllerId], processingFlag);
711     NSUInteger idx = [vimControllers indexOfObject:controller];
712     if (NSNotFound == idx) {
713         ASLogDebug(@"Controller not found, probably due to duplicate removal");
714         return;
715     }
717     [controller retain];
718     [vimControllers removeObjectAtIndex:idx];
719     [controller cleanup];
720     [controller release];
722     if (![vimControllers count]) {
723         // The last editor window just closed so restore the main menu back to
724         // its default state (which is defined in MainMenu.nib).
725         [self setMainMenu:defaultMainMenu];
727         BOOL hide = (MMHideWhenLastWindowClosed ==
728                     [[NSUserDefaults standardUserDefaults]
729                         integerForKey:MMLastWindowClosedBehaviorKey]);
730         if (hide)
731             [NSApp hide:self];
732     }
734     // There is a small delay before the Vim process actually exits so wait a
735     // little before trying to reap the child process.  If the process still
736     // hasn't exited after this wait it won't be reaped until the next time
737     // reapChildProcesses: is called (but this should be harmless).
738     [self performSelector:@selector(reapChildProcesses:)
739                withObject:nil
740                afterDelay:0.1];
743 - (void)windowControllerWillOpen:(MMWindowController *)windowController
745     NSPoint topLeft = NSZeroPoint;
746     NSWindow *topWin = [[[self topmostVimController] windowController] window];
747     NSWindow *win = [windowController window];
749     if (!win) return;
751     // If there is a window belonging to a Vim process, cascade from it,
752     // otherwise use the autosaved window position (if any).
753     if (topWin) {
754         NSRect frame = [topWin frame];
755         topLeft = NSMakePoint(frame.origin.x, NSMaxY(frame));
756     } else {
757         NSString *topLeftString = [[NSUserDefaults standardUserDefaults]
758             stringForKey:MMTopLeftPointKey];
759         if (topLeftString)
760             topLeft = NSPointFromString(topLeftString);
761     }
763     if (!NSEqualPoints(topLeft, NSZeroPoint)) {
764         // Try to tile from the correct screen in case the user has multiple
765         // monitors ([win screen] always seems to return the "main" screen).
766         NSScreen *screen = [self screenContainingPoint:topLeft];
767         if (!screen)
768             screen = [win screen];
770         if (topWin) {
771             // Do manual cascading instead of using
772             // -[MMWindow cascadeTopLeftFromPoint:] since it is rather
773             // unpredictable.
774             topLeft.x += MMCascadeHorizontalOffset;
775             topLeft.y -= MMCascadeVerticalOffset;
776         }
778         if (screen) {
779             // Constrain the window so that it is entirely visible on the
780             // screen.  If it sticks out on the right, move it all the way
781             // left.  If it sticks out on the bottom, move it all the way up.
782             // (Assumption: the cascading offsets are positive.)
783             NSRect screenFrame = [screen frame];
784             NSSize winSize = [win frame].size;
785             NSRect winFrame =
786                 { { topLeft.x, topLeft.y - winSize.height }, winSize };
788             if (NSMaxX(winFrame) > NSMaxX(screenFrame))
789                 topLeft.x = NSMinX(screenFrame);
790             if (NSMinY(winFrame) < NSMinY(screenFrame))
791                 topLeft.y = NSMaxY(screenFrame);
792         } else {
793             ASLogNotice(@"Window not on screen, don't constrain position");
794         }
796         [win setFrameTopLeftPoint:topLeft];
797     }
799     if (1 == [vimControllers count]) {
800         // The first window autosaves its position.  (The autosaving
801         // features of Cocoa are not used because we need more control over
802         // what is autosaved and when it is restored.)
803         [windowController setWindowAutosaveKey:MMTopLeftPointKey];
804     }
806     if (openSelectionString) {
807         // TODO: Pass this as a parameter instead!  Get rid of
808         // 'openSelectionString' etc.
809         //
810         // There is some text to paste into this window as a result of the
811         // services menu "Open selection ..." being used.
812         [[windowController vimController] dropString:openSelectionString];
813         [openSelectionString release];
814         openSelectionString = nil;
815     }
817     if (shouldActivateWhenNextWindowOpens) {
818         [NSApp activateIgnoringOtherApps:YES];
819         shouldActivateWhenNextWindowOpens = NO;
820     }
823 - (void)setMainMenu:(NSMenu *)mainMenu
825     if ([NSApp mainMenu] == mainMenu) return;
827     // If the new menu has a "Recent Files" dummy item, then swap the real item
828     // for the dummy.  We are forced to do this since Cocoa initializes the
829     // "Recent Files" menu and there is no way to simply point Cocoa to a new
830     // item each time the menus are swapped.
831     NSMenu *fileMenu = [mainMenu findFileMenu];
832     if (recentFilesMenuItem && fileMenu) {
833         int dummyIdx =
834                 [fileMenu indexOfItemWithAction:@selector(recentFilesDummy:)];
835         if (dummyIdx >= 0) {
836             NSMenuItem *dummyItem = [[fileMenu itemAtIndex:dummyIdx] retain];
837             [fileMenu removeItemAtIndex:dummyIdx];
839             NSMenu *recentFilesParentMenu = [recentFilesMenuItem menu];
840             int idx = [recentFilesParentMenu indexOfItem:recentFilesMenuItem];
841             if (idx >= 0) {
842                 [[recentFilesMenuItem retain] autorelease];
843                 [recentFilesParentMenu removeItemAtIndex:idx];
844                 [recentFilesParentMenu insertItem:dummyItem atIndex:idx];
845             }
847             [fileMenu insertItem:recentFilesMenuItem atIndex:dummyIdx];
848             [dummyItem release];
849         }
850     }
852     // Now set the new menu.  Notice that we keep one menu for each editor
853     // window since each editor can have its own set of menus.  When swapping
854     // menus we have to tell Cocoa where the new "MacVim", "Windows", and
855     // "Services" menu are.
856     [NSApp setMainMenu:mainMenu];
858     // Setting the "MacVim" (or "Application") menu ensures that it is typeset
859     // in boldface.  (The setAppleMenu: method used to be public but is now
860     // private so this will have to be considered a bit of a hack!)
861     NSMenu *appMenu = [mainMenu findApplicationMenu];
862     [NSApp performSelector:@selector(setAppleMenu:) withObject:appMenu];
864     NSMenu *servicesMenu = [mainMenu findServicesMenu];
865     [NSApp setServicesMenu:servicesMenu];
867     NSMenu *windowsMenu = [mainMenu findWindowsMenu];
868     if (windowsMenu) {
869         // Cocoa isn't clever enough to get rid of items it has added to the
870         // "Windows" menu so we have to do it ourselves otherwise there will be
871         // multiple menu items for each window in the "Windows" menu.
872         //   This code assumes that the only items Cocoa add are ones which
873         // send off the action makeKeyAndOrderFront:.  (Cocoa will not add
874         // another separator item if the last item on the "Windows" menu
875         // already is a separator, so we needen't worry about separators.)
876         int i, count = [windowsMenu numberOfItems];
877         for (i = count-1; i >= 0; --i) {
878             NSMenuItem *item = [windowsMenu itemAtIndex:i];
879             if ([item action] == @selector(makeKeyAndOrderFront:))
880                 [windowsMenu removeItem:item];
881         }
882     }
883     [NSApp setWindowsMenu:windowsMenu];
885 #ifdef MM_ENABLE_PLUGINS
886     // Move plugin menu from old to new main menu.
887     [self removePlugInMenu];
888     [self addPlugInMenuToMenu:mainMenu];
889 #endif
892 - (NSArray *)filterOpenFiles:(NSArray *)filenames
894     return [self filterOpenFiles:filenames openFilesDict:nil];
897 - (BOOL)openFiles:(NSArray *)filenames withArguments:(NSDictionary *)args
899     // Opening files works like this:
900     //  a) filter out any already open files
901     //  b) open any remaining files
902     //
903     // A file is opened in an untitled window if there is one (it may be
904     // currently launching, or it may already be visible), otherwise a new
905     // window is opened.
906     //
907     // Each launching Vim process has a dictionary of arguments that are passed
908     // to the process when in checks in (via connectBackend:pid:).  The
909     // arguments for each launching process can be looked up by its PID (in the
910     // pidArguments dictionary).
912     NSMutableDictionary *arguments = (args ? [[args mutableCopy] autorelease]
913                                            : [NSMutableDictionary dictionary]);
915     filenames = normalizeFilenames(filenames);
917     //
918     // a) Filter out any already open files
919     //
920     NSString *firstFile = [filenames objectAtIndex:0];
921     MMVimController *firstController = nil;
922     NSDictionary *openFilesDict = nil;
923     filenames = [self filterOpenFiles:filenames openFilesDict:&openFilesDict];
925     // Pass arguments to vim controllers that had files open.
926     id key;
927     NSEnumerator *e = [openFilesDict keyEnumerator];
929     // (Indicate that we do not wish to open any files at the moment.)
930     [arguments setObject:[NSNumber numberWithBool:YES] forKey:@"dontOpen"];
932     while ((key = [e nextObject])) {
933         NSArray *files = [openFilesDict objectForKey:key];
934         [arguments setObject:files forKey:@"filenames"];
936         MMVimController *vc = [key pointerValue];
937         [vc passArguments:arguments];
939         // If this controller holds the first file, then remember it for later.
940         if ([files containsObject:firstFile])
941             firstController = vc;
942     }
944     // The meaning of "layout" is defined by the WIN_* defines in main.c.
945     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
946     int layout = [ud integerForKey:MMOpenLayoutKey];
947     BOOL splitVert = [ud boolForKey:MMVerticalSplitKey];
948     BOOL openInCurrentWindow = [ud boolForKey:MMOpenInCurrentWindowKey];
950     if (splitVert && MMLayoutHorizontalSplit == layout)
951         layout = MMLayoutVerticalSplit;
952     if (layout < 0 || (layout > MMLayoutTabs && openInCurrentWindow))
953         layout = MMLayoutTabs;
955     if ([filenames count] == 0) {
956         // Raise the window containing the first file that was already open,
957         // and make sure that the tab containing that file is selected.  Only
958         // do this when there are no more files to open, otherwise sometimes
959         // the window with 'firstFile' will be raised, other times it might be
960         // the window that will open with the files in the 'filenames' array.
961         firstFile = [firstFile stringByEscapingSpecialFilenameCharacters];
963         NSString *bufCmd = @"tab sb";
964         switch (layout) {
965             case MMLayoutHorizontalSplit: bufCmd = @"sb"; break;
966             case MMLayoutVerticalSplit:   bufCmd = @"vert sb"; break;
967             case MMLayoutArglist:         bufCmd = @"b"; break;
968         }
970         NSString *input = [NSString stringWithFormat:@"<C-\\><C-N>"
971                 ":let oldswb=&swb|let &swb=\"useopen,usetab\"|"
972                 "%@ %@|let &swb=oldswb|unl oldswb|"
973                 "cal foreground()<CR>", bufCmd, firstFile];
975         [firstController addVimInput:input];
977         return YES;
978     }
980     // Add filenames to "Recent Files" menu, unless they are being edited
981     // remotely (using ODB).
982     if ([arguments objectForKey:@"remoteID"] == nil) {
983         [[NSDocumentController sharedDocumentController]
984                 noteNewRecentFilePaths:filenames];
985     }
987     //
988     // b) Open any remaining files
989     //
991     [arguments setObject:[NSNumber numberWithInt:layout] forKey:@"layout"];
992     [arguments setObject:filenames forKey:@"filenames"];
993     // (Indicate that files should be opened from now on.)
994     [arguments setObject:[NSNumber numberWithBool:NO] forKey:@"dontOpen"];
996     MMVimController *vc;
997     if (openInCurrentWindow && (vc = [self topmostVimController])) {
998         // Open files in an already open window.
999         [[[vc windowController] window] makeKeyAndOrderFront:self];
1000         [vc passArguments:arguments];
1001         return YES;
1002     }
1004     BOOL openOk = YES;
1005     int numFiles = [filenames count];
1006     if (MMLayoutWindows == layout && numFiles > 1) {
1007         // Open one file at a time in a new window, but don't open too many at
1008         // once (at most cap+1 windows will open).  If the user has increased
1009         // the preload cache size we'll take that as a hint that more windows
1010         // should be able to open at once.
1011         int cap = [self maxPreloadCacheSize] - 1;
1012         if (cap < 4) cap = 4;
1013         if (cap > numFiles) cap = numFiles;
1015         int i;
1016         for (i = 0; i < cap; ++i) {
1017             NSArray *a = [NSArray arrayWithObject:[filenames objectAtIndex:i]];
1018             [arguments setObject:a forKey:@"filenames"];
1020             // NOTE: We have to copy the args since we'll mutate them in the
1021             // next loop and the below call may retain the arguments while
1022             // waiting for a process to start.
1023             NSDictionary *args = [[arguments copy] autorelease];
1025             openOk = [self openVimControllerWithArguments:args];
1026             if (!openOk) break;
1027         }
1029         // Open remaining files in tabs in a new window.
1030         if (openOk && numFiles > cap) {
1031             NSRange range = { i, numFiles-cap };
1032             NSArray *a = [filenames subarrayWithRange:range];
1033             [arguments setObject:a forKey:@"filenames"];
1034             [arguments setObject:[NSNumber numberWithInt:MMLayoutTabs]
1035                           forKey:@"layout"];
1037             openOk = [self openVimControllerWithArguments:arguments];
1038         }
1039     } else {
1040         // Open all files at once.
1041         openOk = [self openVimControllerWithArguments:arguments];
1042     }
1044     return openOk;
1047 #ifdef MM_ENABLE_PLUGINS
1048 - (void)addItemToPlugInMenu:(NSMenuItem *)item
1050     NSMenu *menu = [plugInMenuItem submenu];
1051     [menu addItem:item];
1052     if ([menu numberOfItems] == 1)
1053         [self addPlugInMenuToMenu:[NSApp mainMenu]];
1056 - (void)removeItemFromPlugInMenu:(NSMenuItem *)item
1058     NSMenu *menu = [plugInMenuItem submenu];
1059     [menu removeItem:item];
1060     if ([menu numberOfItems] == 0)
1061         [self removePlugInMenu];
1063 #endif
1065 - (IBAction)newWindow:(id)sender
1067     ASLogDebug(@"Open new window");
1069     // A cached controller requires no loading times and results in the new
1070     // window popping up instantaneously.  If the cache is empty it may take
1071     // 1-2 seconds to start a new Vim process.
1072     MMVimController *vc = [self takeVimControllerFromCache];
1073     if (vc) {
1074         [[vc backendProxy] acknowledgeConnection];
1075     } else {
1076         [self launchVimProcessWithArguments:nil workingDirectory:nil];
1077     }
1080 - (IBAction)newWindowAndActivate:(id)sender
1082     [self activateWhenNextWindowOpens];
1083     [self newWindow:sender];
1086 - (IBAction)fileOpen:(id)sender
1088     ASLogDebug(@"Show file open panel");
1090     NSString *dir = nil;
1091     BOOL trackPwd = [[NSUserDefaults standardUserDefaults]
1092             boolForKey:MMDialogsTrackPwdKey];
1093     if (trackPwd) {
1094         MMVimController *vc = [self keyVimController];
1095         if (vc) dir = [vc objectForVimStateKey:@"pwd"];
1096     }
1098     NSOpenPanel *panel = [NSOpenPanel openPanel];
1099     [panel setAllowsMultipleSelection:YES];
1100     [panel setAccessoryView:showHiddenFilesView()];
1102     int result = [panel runModalForDirectory:dir file:nil types:nil];
1103     if (NSOKButton == result)
1104         [self application:NSApp openFiles:[panel filenames]];
1107 - (IBAction)selectNextWindow:(id)sender
1109     ASLogDebug(@"Select next window");
1111     unsigned i, count = [vimControllers count];
1112     if (!count) return;
1114     NSWindow *keyWindow = [NSApp keyWindow];
1115     for (i = 0; i < count; ++i) {
1116         MMVimController *vc = [vimControllers objectAtIndex:i];
1117         if ([[[vc windowController] window] isEqual:keyWindow])
1118             break;
1119     }
1121     if (i < count) {
1122         if (++i >= count)
1123             i = 0;
1124         MMVimController *vc = [vimControllers objectAtIndex:i];
1125         [[vc windowController] showWindow:self];
1126     }
1129 - (IBAction)selectPreviousWindow:(id)sender
1131     ASLogDebug(@"Select previous window");
1133     unsigned i, count = [vimControllers count];
1134     if (!count) return;
1136     NSWindow *keyWindow = [NSApp keyWindow];
1137     for (i = 0; i < count; ++i) {
1138         MMVimController *vc = [vimControllers objectAtIndex:i];
1139         if ([[[vc windowController] window] isEqual:keyWindow])
1140             break;
1141     }
1143     if (i < count) {
1144         if (i > 0) {
1145             --i;
1146         } else {
1147             i = count - 1;
1148         }
1149         MMVimController *vc = [vimControllers objectAtIndex:i];
1150         [[vc windowController] showWindow:self];
1151     }
1154 - (IBAction)orderFrontPreferencePanel:(id)sender
1156     ASLogDebug(@"Show preferences panel");
1157     [[MMPreferenceController sharedPrefsWindowController] showWindow:self];
1160 - (IBAction)openWebsite:(id)sender
1162     ASLogDebug(@"Open MacVim website");
1163     [[NSWorkspace sharedWorkspace] openURL:
1164             [NSURL URLWithString:MMWebsiteString]];
1167 - (IBAction)showVimHelp:(id)sender
1169     ASLogDebug(@"Open window with Vim help");
1170     // Open a new window with the help window maximized.
1171     [self launchVimProcessWithArguments:[NSArray arrayWithObjects:
1172                                     @"-c", @":h gui_mac", @"-c", @":res", nil]
1173                        workingDirectory:nil];
1176 - (IBAction)zoomAll:(id)sender
1178     ASLogDebug(@"Zoom all windows");
1179     [NSApp makeWindowsPerform:@selector(performZoom:) inOrder:YES];
1182 - (IBAction)atsuiButtonClicked:(id)sender
1184     ASLogDebug(@"Toggle ATSUI renderer");
1185     NSInteger renderer = MMRendererDefault;
1186     BOOL enable = ([sender state] == NSOnState);
1188     if (enable) {
1189 #if MM_ENABLE_ATSUI
1190         renderer = MMRendererATSUI;
1191 #else
1192         renderer = MMRendererCoreText;
1193 #endif
1194     }
1196     // Update the user default MMRenderer and synchronize the change so that
1197     // any new Vim process will pick up on the changed setting.
1198     CFPreferencesSetAppValue(
1199             (CFStringRef)MMRendererKey,
1200             (CFPropertyListRef)[NSNumber numberWithInt:renderer],
1201             kCFPreferencesCurrentApplication);
1202     CFPreferencesAppSynchronize(kCFPreferencesCurrentApplication);
1204     ASLogInfo(@"Use renderer=%d", renderer);
1206     // This action is called when the user clicks the "use ATSUI renderer"
1207     // button in the advanced preferences pane.
1208     [self rebuildPreloadCache];
1211 - (IBAction)loginShellButtonClicked:(id)sender
1213     ASLogDebug(@"Toggle login shell option");
1214     // This action is called when the user clicks the "use login shell" button
1215     // in the advanced preferences pane.
1216     [self rebuildPreloadCache];
1219 - (IBAction)quickstartButtonClicked:(id)sender
1221     ASLogDebug(@"Toggle Quickstart option");
1222     if ([self maxPreloadCacheSize] > 0) {
1223         [self scheduleVimControllerPreloadAfterDelay:1.0];
1224         [self startWatchingVimDir];
1225     } else {
1226         [self cancelVimControllerPreloadRequests];
1227         [self clearPreloadCacheWithCount:-1];
1228         [self stopWatchingVimDir];
1229     }
1232 - (MMVimController *)keyVimController
1234     NSWindow *keyWindow = [NSApp keyWindow];
1235     if (keyWindow) {
1236         unsigned i, count = [vimControllers count];
1237         for (i = 0; i < count; ++i) {
1238             MMVimController *vc = [vimControllers objectAtIndex:i];
1239             if ([[[vc windowController] window] isEqual:keyWindow])
1240                 return vc;
1241         }
1242     }
1244     return nil;
1247 - (unsigned)connectBackend:(byref in id <MMBackendProtocol>)proxy pid:(int)pid
1249     ASLogDebug(@"pid=%d", pid);
1251     [(NSDistantObject*)proxy setProtocolForProxy:@protocol(MMBackendProtocol)];
1253     // NOTE: Allocate the vim controller now but don't add it to the list of
1254     // controllers since this is a distributed object call and as such can
1255     // arrive at unpredictable times (e.g. while iterating the list of vim
1256     // controllers).
1257     // (What if input arrives before the vim controller is added to the list of
1258     // controllers?  This should not be a problem since the input isn't
1259     // processed immediately (see processInput:forIdentifier:).)
1260     // Also, since the app may be multithreaded (e.g. as a result of showing
1261     // the open panel) we have to ensure this call happens on the main thread,
1262     // else there is a race condition that may lead to a crash.
1263     MMVimController *vc = [[MMVimController alloc] initWithBackend:proxy
1264                                                                pid:pid];
1265     [self performSelectorOnMainThread:@selector(addVimController:)
1266                            withObject:vc
1267                         waitUntilDone:NO
1268                                 modes:[NSArray arrayWithObject:
1269                                        NSDefaultRunLoopMode]];
1271     [vc release];
1273     return [vc vimControllerId];
1276 - (oneway void)processInput:(in bycopy NSArray *)queue
1277               forIdentifier:(unsigned)identifier
1279     // NOTE: Input is not handled immediately since this is a distributed
1280     // object call and as such can arrive at unpredictable times.  Instead,
1281     // queue the input and process it when the run loop is updated.
1283     if (!(queue && identifier)) {
1284         ASLogWarn(@"Bad input for identifier=%d", identifier);
1285         return;
1286     }
1288     ASLogDebug(@"QUEUE for identifier=%d: <<< %@>>>", identifier,
1289                debugStringForMessageQueue(queue));
1291     NSNumber *key = [NSNumber numberWithUnsignedInt:identifier];
1292     NSArray *q = [inputQueues objectForKey:key];
1293     if (q) {
1294         q = [q arrayByAddingObjectsFromArray:queue];
1295         [inputQueues setObject:q forKey:key];
1296     } else {
1297         [inputQueues setObject:queue forKey:key];
1298     }
1300     // NOTE: We must use "event tracking mode" as well as "default mode",
1301     // otherwise the input queue will not be processed e.g. during live
1302     // resizing.
1303     // Also, since the app may be multithreaded (e.g. as a result of showing
1304     // the open panel) we have to ensure this call happens on the main thread,
1305     // else there is a race condition that may lead to a crash.
1306     [self performSelectorOnMainThread:@selector(processInputQueues:)
1307                            withObject:nil
1308                         waitUntilDone:NO
1309                                 modes:[NSArray arrayWithObjects:
1310                                        NSDefaultRunLoopMode,
1311                                        NSEventTrackingRunLoopMode, nil]];
1314 - (NSArray *)serverList
1316     NSMutableArray *array = [NSMutableArray array];
1318     unsigned i, count = [vimControllers count];
1319     for (i = 0; i < count; ++i) {
1320         MMVimController *controller = [vimControllers objectAtIndex:i];
1321         if ([controller serverName])
1322             [array addObject:[controller serverName]];
1323     }
1325     return array;
1328 @end // MMAppController
1333 @implementation MMAppController (MMServices)
1335 - (void)openSelection:(NSPasteboard *)pboard userData:(NSString *)userData
1336                 error:(NSString **)error
1338     if (![[pboard types] containsObject:NSStringPboardType]) {
1339         ASLogNotice(@"Pasteboard contains no NSStringPboardType");
1340         return;
1341     }
1343     ASLogInfo(@"Open new window containing current selection");
1345     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
1346     BOOL openInCurrentWindow = [ud boolForKey:MMOpenInCurrentWindowKey];
1347     MMVimController *vc;
1349     if (openInCurrentWindow && (vc = [self topmostVimController])) {
1350         [vc sendMessage:AddNewTabMsgID data:nil];
1351         [vc dropString:[pboard stringForType:NSStringPboardType]];
1352     } else {
1353         // Save the text, open a new window, and paste the text when the next
1354         // window opens.  (If this is called several times in a row, then all
1355         // but the last call may be ignored.)
1356         if (openSelectionString) [openSelectionString release];
1357         openSelectionString = [[pboard stringForType:NSStringPboardType] copy];
1359         [self newWindow:self];
1360     }
1363 - (void)openFile:(NSPasteboard *)pboard userData:(NSString *)userData
1364            error:(NSString **)error
1366     if (![[pboard types] containsObject:NSStringPboardType]) {
1367         ASLogNotice(@"Pasteboard contains no NSStringPboardType");
1368         return;
1369     }
1371     // TODO: Parse multiple filenames and create array with names.
1372     NSString *string = [pboard stringForType:NSStringPboardType];
1373     string = [string stringByTrimmingCharactersInSet:
1374             [NSCharacterSet whitespaceAndNewlineCharacterSet]];
1375     string = [string stringByStandardizingPath];
1377     ASLogInfo(@"Open new window with selected file: %@", string);
1379     NSArray *filenames = [self filterFilesAndNotify:
1380             [NSArray arrayWithObject:string]];
1381     if ([filenames count] == 0)
1382         return;
1384     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
1385     BOOL openInCurrentWindow = [ud boolForKey:MMOpenInCurrentWindowKey];
1386     MMVimController *vc;
1388     if (openInCurrentWindow && (vc = [self topmostVimController])) {
1389         [vc dropFiles:filenames forceOpen:YES];
1390     } else {
1391         [self openFiles:filenames withArguments:nil];
1392     }
1395 - (void)newFileHere:(NSPasteboard *)pboard userData:(NSString *)userData
1396               error:(NSString **)error
1398     if (![[pboard types] containsObject:NSFilenamesPboardType]) {
1399         ASLogNotice(@"Pasteboard contains no NSFilenamesPboardType");
1400         return;
1401     }
1403     NSArray *filenames = [pboard propertyListForType:NSFilenamesPboardType];
1404     NSString *path = [filenames lastObject];
1406     BOOL dirIndicator;
1407     if (![[NSFileManager defaultManager] fileExistsAtPath:path
1408                                               isDirectory:&dirIndicator]) {
1409         ASLogNotice(@"Invalid path. Cannot open new document at: %@", path);
1410         return;
1411     }
1413     ASLogInfo(@"Open new file at path=%@", path);
1415     if (!dirIndicator)
1416         path = [path stringByDeletingLastPathComponent];
1418     path = [path stringByEscapingSpecialFilenameCharacters];
1420     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
1421     BOOL openInCurrentWindow = [ud boolForKey:MMOpenInCurrentWindowKey];
1422     MMVimController *vc;
1424     if (openInCurrentWindow && (vc = [self topmostVimController])) {
1425         NSString *input = [NSString stringWithFormat:@"<C-\\><C-N>"
1426                 ":tabe|cd %@<CR>", path];
1427         [vc addVimInput:input];
1428     } else {
1429         [self launchVimProcessWithArguments:nil workingDirectory:path];
1430     }
1433 @end // MMAppController (MMServices)
1438 @implementation MMAppController (Private)
1440 - (MMVimController *)topmostVimController
1442     // Find the topmost visible window which has an associated vim controller.
1443     NSEnumerator *e = [[NSApp orderedWindows] objectEnumerator];
1444     id window;
1445     while ((window = [e nextObject]) && [window isVisible]) {
1446         unsigned i, count = [vimControllers count];
1447         for (i = 0; i < count; ++i) {
1448             MMVimController *vc = [vimControllers objectAtIndex:i];
1449             if ([[[vc windowController] window] isEqual:window])
1450                 return vc;
1451         }
1452     }
1454     return nil;
1457 - (int)launchVimProcessWithArguments:(NSArray *)args
1458                     workingDirectory:(NSString *)cwd
1460     int pid = -1;
1461     NSString *path = [[NSBundle mainBundle] pathForAuxiliaryExecutable:@"Vim"];
1463     if (!path) {
1464         ASLogCrit(@"Vim executable could not be found inside app bundle!");
1465         return -1;
1466     }
1468     // Change current working directory so that the child process picks it up.
1469     NSFileManager *fm = [NSFileManager defaultManager];
1470     NSString *restoreCwd = nil;
1471     if (cwd) {
1472         restoreCwd = [fm currentDirectoryPath];
1473         [fm changeCurrentDirectoryPath:cwd];
1474     }
1476     NSArray *taskArgs = [NSArray arrayWithObjects:@"-g", @"-f", nil];
1477     if (args)
1478         taskArgs = [taskArgs arrayByAddingObjectsFromArray:args];
1480     BOOL useLoginShell = [[NSUserDefaults standardUserDefaults]
1481             boolForKey:MMLoginShellKey];
1482     if (useLoginShell) {
1483         // Run process with a login shell, roughly:
1484         //   echo "exec Vim -g -f args" | ARGV0=-`basename $SHELL` $SHELL [-l]
1485         pid = [self executeInLoginShell:path arguments:taskArgs];
1486     } else {
1487         // Run process directly:
1488         //   Vim -g -f args
1489         NSTask *task = [NSTask launchedTaskWithLaunchPath:path
1490                                                 arguments:taskArgs];
1491         pid = task ? [task processIdentifier] : -1;
1492     }
1494     if (-1 != pid) {
1495         // The 'pidArguments' dictionary keeps arguments to be passed to the
1496         // process when it connects (this is in contrast to arguments which are
1497         // passed on the command line, like '-f' and '-g').
1498         // If this method is called with nil arguments we take this as a hint
1499         // that this is an "untitled window" being launched and add a null
1500         // object to the 'pidArguments' dictionary.  This way we can detect if
1501         // an untitled window is being launched by looking for null objects in
1502         // this dictionary.
1503         // If this method is called with non-nil arguments then it is assumed
1504         // that the caller takes care of adding items to 'pidArguments' as
1505         // necessary (only some arguments are passed on connect, e.g. files to
1506         // open).
1507         if (!args)
1508             [pidArguments setObject:[NSNull null]
1509                              forKey:[NSNumber numberWithInt:pid]];
1510     } else {
1511         ASLogWarn(@"Failed to launch Vim process: args=%@, useLoginShell=%d",
1512                   args, useLoginShell);
1513     }
1515     // Now that child has launched, restore the current working directory.
1516     if (restoreCwd)
1517         [fm changeCurrentDirectoryPath:restoreCwd];
1519     return pid;
1522 - (NSArray *)filterFilesAndNotify:(NSArray *)filenames
1524     // Go trough 'filenames' array and make sure each file exists.  Present
1525     // warning dialog if some file was missing.
1527     NSString *firstMissingFile = nil;
1528     NSMutableArray *files = [NSMutableArray array];
1529     unsigned i, count = [filenames count];
1531     for (i = 0; i < count; ++i) {
1532         NSString *name = [filenames objectAtIndex:i];
1533         if ([[NSFileManager defaultManager] fileExistsAtPath:name]) {
1534             [files addObject:name];
1535         } else if (!firstMissingFile) {
1536             firstMissingFile = name;
1537         }
1538     }
1540     if (firstMissingFile) {
1541         NSAlert *alert = [[NSAlert alloc] init];
1542         [alert addButtonWithTitle:NSLocalizedString(@"OK",
1543                 @"Dialog button")];
1545         NSString *text;
1546         if ([files count] >= count-1) {
1547             [alert setMessageText:NSLocalizedString(@"File not found",
1548                     @"File not found dialog, title")];
1549             text = [NSString stringWithFormat:NSLocalizedString(
1550                     @"Could not open file with name %@.",
1551                     @"File not found dialog, text"), firstMissingFile];
1552         } else {
1553             [alert setMessageText:NSLocalizedString(@"Multiple files not found",
1554                     @"File not found dialog, title")];
1555             text = [NSString stringWithFormat:NSLocalizedString(
1556                     @"Could not open file with name %@, and %d other files.",
1557                     @"File not found dialog, text"),
1558                 firstMissingFile, count-[files count]-1];
1559         }
1561         [alert setInformativeText:text];
1562         [alert setAlertStyle:NSWarningAlertStyle];
1564         [alert runModal];
1565         [alert release];
1567         [NSApp replyToOpenOrPrint:NSApplicationDelegateReplyFailure];
1568     }
1570     return files;
1573 - (NSArray *)filterOpenFiles:(NSArray *)filenames
1574                openFilesDict:(NSDictionary **)openFiles
1576     // Filter out any files in the 'filenames' array that are open and return
1577     // all files that are not already open.  On return, the 'openFiles'
1578     // parameter (if non-nil) will point to a dictionary of open files, indexed
1579     // by Vim controller.
1581     NSMutableDictionary *dict = [NSMutableDictionary dictionary];
1582     NSMutableArray *files = [filenames mutableCopy];
1584     // TODO: Escape special characters in 'files'?
1585     NSString *expr = [NSString stringWithFormat:
1586             @"map([\"%@\"],\"bufloaded(v:val)\")",
1587             [files componentsJoinedByString:@"\",\""]];
1589     unsigned i, count = [vimControllers count];
1590     for (i = 0; i < count && [files count] > 0; ++i) {
1591         MMVimController *vc = [vimControllers objectAtIndex:i];
1593         // Query Vim for which files in the 'files' array are open.
1594         NSString *eval = [vc evaluateVimExpression:expr];
1595         if (!eval) continue;
1597         NSIndexSet *idxSet = [NSIndexSet indexSetWithVimList:eval];
1598         if ([idxSet count] > 0) {
1599             [dict setObject:[files objectsAtIndexes:idxSet]
1600                      forKey:[NSValue valueWithPointer:vc]];
1602             // Remove all the files that were open in this Vim process and
1603             // create a new expression to evaluate.
1604             [files removeObjectsAtIndexes:idxSet];
1605             expr = [NSString stringWithFormat:
1606                     @"map([\"%@\"],\"bufloaded(v:val)\")",
1607                     [files componentsJoinedByString:@"\",\""]];
1608         }
1609     }
1611     if (openFiles != nil)
1612         *openFiles = dict;
1614     return [files autorelease];
1617 #if MM_HANDLE_XCODE_MOD_EVENT
1618 - (void)handleXcodeModEvent:(NSAppleEventDescriptor *)event
1619                  replyEvent:(NSAppleEventDescriptor *)reply
1621 #if 0
1622     // Xcode sends this event to query MacVim which open files have been
1623     // modified.
1624     ASLogDebug(@"reply:%@", reply);
1625     ASLogDebug(@"event:%@", event);
1627     NSEnumerator *e = [vimControllers objectEnumerator];
1628     id vc;
1629     while ((vc = [e nextObject])) {
1630         DescType type = [reply descriptorType];
1631         unsigned len = [[type data] length];
1632         NSMutableData *data = [NSMutableData data];
1634         [data appendBytes:&type length:sizeof(DescType)];
1635         [data appendBytes:&len length:sizeof(unsigned)];
1636         [data appendBytes:[reply data] length:len];
1638         [vc sendMessage:XcodeModMsgID data:data];
1639     }
1640 #endif
1642 #endif
1644 - (void)handleGetURLEvent:(NSAppleEventDescriptor *)event
1645                replyEvent:(NSAppleEventDescriptor *)reply
1647     NSString *urlString = [[event paramDescriptorForKeyword:keyDirectObject]
1648         stringValue];
1649     NSURL *url = [NSURL URLWithString:urlString];
1651     // We try to be compatible with TextMate's URL scheme here, as documented
1652     // at http://blog.macromates.com/2007/the-textmate-url-scheme/ . Currently,
1653     // this means that:
1654     //
1655     // The format is: mvim://open?<arguments> where arguments can be:
1656     //
1657     // * url â€” the actual file to open (i.e. a file://… URL), if you leave
1658     //         out this argument, the frontmost document is implied.
1659     // * line â€” line number to go to (one based).
1660     // * column â€” column number to go to (one based).
1661     //
1662     // Example: mvim://open?url=file:///etc/profile&line=20
1664     if ([[url host] isEqualToString:@"open"]) {
1665         NSMutableDictionary *dict = [NSMutableDictionary dictionary];
1667         // Parse query ("url=file://...&line=14") into a dictionary
1668         NSArray *queries = [[url query] componentsSeparatedByString:@"&"];
1669         NSEnumerator *enumerator = [queries objectEnumerator];
1670         NSString *param;
1671         while ((param = [enumerator nextObject])) {
1672             NSArray *arr = [param componentsSeparatedByString:@"="];
1673             if ([arr count] == 2) {
1674                 [dict setValue:[[arr lastObject]
1675                             stringByReplacingPercentEscapesUsingEncoding:
1676                                 NSUTF8StringEncoding]
1677                         forKey:[[arr objectAtIndex:0]
1678                             stringByReplacingPercentEscapesUsingEncoding:
1679                                 NSUTF8StringEncoding]];
1680             }
1681         }
1683         // Actually open the file.
1684         NSString *file = [dict objectForKey:@"url"];
1685         if (file != nil) {
1686             NSURL *fileUrl= [NSURL URLWithString:file];
1687             // TextMate only opens files that already exist.
1688             if ([fileUrl isFileURL]
1689                     && [[NSFileManager defaultManager] fileExistsAtPath:
1690                            [fileUrl path]]) {
1691                 // Strip 'file://' path, else application:openFiles: might think
1692                 // the file is not yet open.
1693                 NSArray *filenames = [NSArray arrayWithObject:[fileUrl path]];
1695                 // Look for the line and column options.
1696                 NSDictionary *args = nil;
1697                 NSString *line = [dict objectForKey:@"line"];
1698                 if (line) {
1699                     NSString *column = [dict objectForKey:@"column"];
1700                     if (column)
1701                         args = [NSDictionary dictionaryWithObjectsAndKeys:
1702                                 line, @"cursorLine",
1703                                 column, @"cursorColumn",
1704                                 nil];
1705                     else
1706                         args = [NSDictionary dictionaryWithObject:line
1707                                 forKey:@"cursorLine"];
1708                 }
1710                 [self openFiles:filenames withArguments:args];
1711             }
1712         }
1713     } else {
1714         NSAlert *alert = [[NSAlert alloc] init];
1715         [alert addButtonWithTitle:NSLocalizedString(@"OK",
1716             @"Dialog button")];
1718         [alert setMessageText:NSLocalizedString(@"Unknown URL Scheme",
1719             @"Unknown URL Scheme dialog, title")];
1720         [alert setInformativeText:[NSString stringWithFormat:NSLocalizedString(
1721             @"This version of MacVim does not support \"%@\""
1722             @" in its URL scheme.",
1723             @"Unknown URL Scheme dialog, text"),
1724             [url host]]];
1726         [alert setAlertStyle:NSWarningAlertStyle];
1727         [alert runModal];
1728         [alert release];
1729     }
1732 - (NSMutableDictionary *)extractArgumentsFromOdocEvent:
1733     (NSAppleEventDescriptor *)desc
1735     NSMutableDictionary *dict = [NSMutableDictionary dictionary];
1737     // 1. Extract ODB parameters (if any)
1738     NSAppleEventDescriptor *odbdesc = desc;
1739     if (![odbdesc paramDescriptorForKeyword:keyFileSender]) {
1740         // The ODB paramaters may hide inside the 'keyAEPropData' descriptor.
1741         odbdesc = [odbdesc paramDescriptorForKeyword:keyAEPropData];
1742         if (![odbdesc paramDescriptorForKeyword:keyFileSender])
1743             odbdesc = nil;
1744     }
1746     if (odbdesc) {
1747         NSAppleEventDescriptor *p =
1748                 [odbdesc paramDescriptorForKeyword:keyFileSender];
1749         if (p)
1750             [dict setObject:[NSNumber numberWithUnsignedInt:[p typeCodeValue]]
1751                      forKey:@"remoteID"];
1753         p = [odbdesc paramDescriptorForKeyword:keyFileCustomPath];
1754         if (p)
1755             [dict setObject:[p stringValue] forKey:@"remotePath"];
1757         p = [odbdesc paramDescriptorForKeyword:keyFileSenderToken];
1758         if (p) {
1759             [dict setObject:[NSNumber numberWithUnsignedLong:[p descriptorType]]
1760                      forKey:@"remoteTokenDescType"];
1761             [dict setObject:[p data] forKey:@"remoteTokenData"];
1762         }
1763     }
1765     // 2. Extract Xcode parameters (if any)
1766     NSAppleEventDescriptor *xcodedesc =
1767             [desc paramDescriptorForKeyword:keyAEPosition];
1768     if (xcodedesc) {
1769         NSRange range;
1770         NSData *data = [xcodedesc data];
1771         NSUInteger length = [data length];
1773         if (length == sizeof(MMXcodeSelectionRange)) {
1774             MMXcodeSelectionRange *sr = (MMXcodeSelectionRange*)[data bytes];
1775             ASLogDebug(@"Xcode selection range (%d,%d,%d,%d,%d,%d)",
1776                     sr->unused1, sr->lineNum, sr->startRange, sr->endRange,
1777                     sr->unused2, sr->theDate);
1779             if (sr->lineNum < 0) {
1780                 // Should select a range of lines.
1781                 range.location = sr->startRange + 1;
1782                 range.length = sr->endRange - sr->startRange + 1;
1783             } else {
1784                 // Should only move cursor to a line.
1785                 range.location = sr->lineNum + 1;
1786                 range.length = 0;
1787             }
1789             [dict setObject:NSStringFromRange(range) forKey:@"selectionRange"];
1790         } else {
1791             ASLogErr(@"Xcode selection range size mismatch! got=%d expected=%d",
1792                     length, sizeof(MMXcodeSelectionRange));
1793         }
1794     }
1796     // 3. Extract Spotlight search text (if any)
1797     NSAppleEventDescriptor *spotlightdesc = 
1798             [desc paramDescriptorForKeyword:keyAESearchText];
1799     if (spotlightdesc)
1800         [dict setObject:[spotlightdesc stringValue] forKey:@"searchText"];
1802     return dict;
1805 #ifdef MM_ENABLE_PLUGINS
1806 - (void)removePlugInMenu
1808     if ([plugInMenuItem menu])
1809         [[plugInMenuItem menu] removeItem:plugInMenuItem];
1812 - (void)addPlugInMenuToMenu:(NSMenu *)mainMenu
1814     NSMenu *windowsMenu = [mainMenu findWindowsMenu];
1816     if ([[plugInMenuItem submenu] numberOfItems] > 0) {
1817         int idx = windowsMenu ? [mainMenu indexOfItemWithSubmenu:windowsMenu]
1818                               : -1;
1819         if (idx > 0) {
1820             [mainMenu insertItem:plugInMenuItem atIndex:idx];
1821         } else {
1822             [mainMenu addItem:plugInMenuItem];
1823         }
1824     }
1826 #endif
1828 - (void)scheduleVimControllerPreloadAfterDelay:(NSTimeInterval)delay
1830     [self performSelector:@selector(preloadVimController:)
1831                withObject:nil
1832                afterDelay:delay];
1835 - (void)cancelVimControllerPreloadRequests
1837     [NSObject cancelPreviousPerformRequestsWithTarget:self
1838             selector:@selector(preloadVimController:)
1839               object:nil];
1842 - (void)preloadVimController:(id)sender
1844     // We only allow preloading of one Vim process at a time (to avoid hogging
1845     // CPU), so schedule another preload in a little while if necessary.
1846     if (-1 != preloadPid) {
1847         [self scheduleVimControllerPreloadAfterDelay:2];
1848         return;
1849     }
1851     if ([cachedVimControllers count] >= [self maxPreloadCacheSize])
1852         return;
1854     preloadPid = [self launchVimProcessWithArguments:
1855                                     [NSArray arrayWithObject:@"--mmwaitforack"]
1856                                     workingDirectory:nil];
1859 - (int)maxPreloadCacheSize
1861     // The maximum number of Vim processes to keep in the cache can be
1862     // controlled via the user default "MMPreloadCacheSize".
1863     int maxCacheSize = [[NSUserDefaults standardUserDefaults]
1864             integerForKey:MMPreloadCacheSizeKey];
1865     if (maxCacheSize < 0) maxCacheSize = 0;
1866     else if (maxCacheSize > 10) maxCacheSize = 10;
1868     return maxCacheSize;
1871 - (MMVimController *)takeVimControllerFromCache
1873     // NOTE: After calling this message the backend corresponding to the
1874     // returned vim controller must be sent an acknowledgeConnection message,
1875     // else the vim process will be stuck.
1876     //
1877     // This method may return nil even though the cache might be non-empty; the
1878     // caller should handle this by starting a new Vim process.
1880     int i, count = [cachedVimControllers count];
1881     if (0 == count) return nil;
1883     // Locate the first Vim controller with up-to-date rc-files sourced.
1884     NSDate *rcDate = [self rcFilesModificationDate];
1885     for (i = 0; i < count; ++i) {
1886         MMVimController *vc = [cachedVimControllers objectAtIndex:i];
1887         NSDate *date = [vc creationDate];
1888         if ([date compare:rcDate] != NSOrderedAscending)
1889             break;
1890     }
1892     if (i > 0) {
1893         // Clear out cache entries whose vimrc/gvimrc files were sourced before
1894         // the latest modification date for those files.  This ensures that the
1895         // latest rc-files are always sourced for new windows.
1896         [self clearPreloadCacheWithCount:i];
1897     }
1899     if ([cachedVimControllers count] == 0) {
1900         [self scheduleVimControllerPreloadAfterDelay:2.0];
1901         return nil;
1902     }
1904     MMVimController *vc = [cachedVimControllers objectAtIndex:0];
1905     [vimControllers addObject:vc];
1906     [cachedVimControllers removeObjectAtIndex:0];
1907     [vc setIsPreloading:NO];
1909     // If the Vim process has finished loading then the window will displayed
1910     // now, otherwise it will be displayed when the OpenWindowMsgID message is
1911     // received.
1912     [[vc windowController] showWindow];
1914     // Since we've taken one controller from the cache we take the opportunity
1915     // to preload another.
1916     [self scheduleVimControllerPreloadAfterDelay:1];
1918     return vc;
1921 - (void)clearPreloadCacheWithCount:(int)count
1923     // Remove the 'count' first entries in the preload cache.  It is assumed
1924     // that objects are added/removed from the cache in a FIFO manner so that
1925     // this effectively clears the 'count' oldest entries.
1926     // If 'count' is negative, then the entire cache is cleared.
1928     if ([cachedVimControllers count] == 0 || count == 0)
1929         return;
1931     if (count < 0)
1932         count = [cachedVimControllers count];
1934     // Make sure the preloaded Vim processes get killed or they'll just hang
1935     // around being useless until MacVim is terminated.
1936     NSEnumerator *e = [cachedVimControllers objectEnumerator];
1937     MMVimController *vc;
1938     int n = count;
1939     while ((vc = [e nextObject]) && n-- > 0) {
1940         [[NSNotificationCenter defaultCenter] removeObserver:vc];
1941         [vc sendMessage:TerminateNowMsgID data:nil];
1943         // Since the preloaded processes were killed "prematurely" we have to
1944         // manually tell them to cleanup (it is not enough to simply release
1945         // them since deallocation and cleanup are separated).
1946         [vc cleanup];
1947     }
1949     n = count;
1950     while (n-- > 0 && [cachedVimControllers count] > 0)
1951         [cachedVimControllers removeObjectAtIndex:0];
1953     // There is a small delay before the Vim process actually exits so wait a
1954     // little before trying to reap the child process.  If the process still
1955     // hasn't exited after this wait it won't be reaped until the next time
1956     // reapChildProcesses: is called (but this should be harmless).
1957     [self performSelector:@selector(reapChildProcesses:)
1958                withObject:nil
1959                afterDelay:0.1];
1962 - (void)rebuildPreloadCache
1964     if ([self maxPreloadCacheSize] > 0) {
1965         [self clearPreloadCacheWithCount:-1];
1966         [self cancelVimControllerPreloadRequests];
1967         [self scheduleVimControllerPreloadAfterDelay:1.0];
1968     }
1972 // HACK: fileAttributesAtPath was deprecated in 10.5
1973 #if (MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5)
1974 #define MM_fileAttributes(fm,p) [fm attributesOfItemAtPath:p error:NULL]
1975 #else
1976 #define MM_fileAttributes(fm,p) [fm fileAttributesAtPath:p traverseLink:YES]
1977 #endif
1978 - (NSDate *)rcFilesModificationDate
1980     // Check modification dates for ~/.vimrc and ~/.gvimrc and return the
1981     // latest modification date.  If ~/.vimrc does not exist, check ~/_vimrc
1982     // and similarly for gvimrc.
1983     // Returns distantPath if no rc files were found.
1985     NSDate *date = [NSDate distantPast];
1986     NSFileManager *fm = [NSFileManager defaultManager];
1988     NSString *path = [@"~/.vimrc" stringByExpandingTildeInPath];
1989     NSDictionary *attr = MM_fileAttributes(fm, path);
1990     if (!attr) {
1991         path = [@"~/_vimrc" stringByExpandingTildeInPath];
1992         attr = MM_fileAttributes(fm, path);
1993     }
1994     NSDate *modDate = [attr objectForKey:NSFileModificationDate];
1995     if (modDate)
1996         date = modDate;
1998     path = [@"~/.gvimrc" stringByExpandingTildeInPath];
1999     attr = MM_fileAttributes(fm, path);
2000     if (!attr) {
2001         path = [@"~/_gvimrc" stringByExpandingTildeInPath];
2002         attr = MM_fileAttributes(fm, path);
2003     }
2004     modDate = [attr objectForKey:NSFileModificationDate];
2005     if (modDate)
2006         date = [date laterDate:modDate];
2008     return date;
2010 #undef MM_fileAttributes
2012 - (BOOL)openVimControllerWithArguments:(NSDictionary *)arguments
2014     MMVimController *vc = [self takeVimControllerFromCache];
2015     if (vc) {
2016         // Open files in a new window using a cached vim controller.  This
2017         // requires virtually no loading time so the new window will pop up
2018         // instantaneously.
2019         [vc passArguments:arguments];
2020         [[vc backendProxy] acknowledgeConnection];
2021     } else {
2022         NSArray *cmdline = nil;
2023         NSString *cwd = [self workingDirectoryForArguments:arguments];
2024         arguments = [self convertVimControllerArguments:arguments
2025                                           toCommandLine:&cmdline];
2026         int pid = [self launchVimProcessWithArguments:cmdline
2027                                      workingDirectory:cwd];
2028         if (-1 == pid)
2029             return NO;
2031         // TODO: If the Vim process fails to start, or if it changes PID,
2032         // then the memory allocated for these parameters will leak.
2033         // Ensure that this cannot happen or somehow detect it.
2035         if ([arguments count] > 0)
2036             [pidArguments setObject:arguments
2037                              forKey:[NSNumber numberWithInt:pid]];
2038     }
2040     return YES;
2043 - (void)activateWhenNextWindowOpens
2045     ASLogDebug(@"Activate MacVim when next window opens");
2046     shouldActivateWhenNextWindowOpens = YES;
2049 - (void)startWatchingVimDir
2051 #if (MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5)
2052     if (fsEventStream)
2053         return;
2054     if (NULL == FSEventStreamStart)
2055         return; // FSEvent functions are weakly linked
2057     NSString *path = [@"~/.vim" stringByExpandingTildeInPath];
2058     NSArray *pathsToWatch = [NSArray arrayWithObject:path];
2060     fsEventStream = FSEventStreamCreate(NULL, &fsEventCallback, NULL,
2061             (CFArrayRef)pathsToWatch, kFSEventStreamEventIdSinceNow,
2062             MMEventStreamLatency, kFSEventStreamCreateFlagNone);
2064     FSEventStreamScheduleWithRunLoop(fsEventStream,
2065             [[NSRunLoop currentRunLoop] getCFRunLoop],
2066             kCFRunLoopDefaultMode);
2068     FSEventStreamStart(fsEventStream);
2069     ASLogDebug(@"Started FS event stream");
2070 #endif
2073 - (void)stopWatchingVimDir
2075 #if (MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5)
2076     if (NULL == FSEventStreamStop)
2077         return; // FSEvent functions are weakly linked
2079     if (fsEventStream) {
2080         FSEventStreamStop(fsEventStream);
2081         FSEventStreamInvalidate(fsEventStream);
2082         FSEventStreamRelease(fsEventStream);
2083         fsEventStream = NULL;
2084         ASLogDebug(@"Stopped FS event stream");
2085     }
2086 #endif
2090 - (void)handleFSEvent
2092     [self clearPreloadCacheWithCount:-1];
2094     // Several FS events may arrive in quick succession so make sure to cancel
2095     // any previous preload requests before making a new one.
2096     [self cancelVimControllerPreloadRequests];
2097     [self scheduleVimControllerPreloadAfterDelay:0.5];
2100 - (void)loadDefaultFont
2102     // It is possible to set a user default to avoid loading the default font
2103     // (this cuts down on startup time).
2104     if (![[NSUserDefaults standardUserDefaults] boolForKey:MMLoadDefaultFontKey]
2105             || fontContainerRef) {
2106         ASLogInfo(@"Skip loading of the default font...");
2107         return;
2108     }
2110     ASLogInfo(@"Loading the default font...");
2112     // Load all fonts in the Resouces folder of the app bundle.
2113     NSString *fontsFolder = [[NSBundle mainBundle] resourcePath];
2114     if (fontsFolder) {
2115         NSURL *fontsURL = [NSURL fileURLWithPath:fontsFolder];
2116         if (fontsURL) {
2117             FSRef fsRef;
2118             CFURLGetFSRef((CFURLRef)fontsURL, &fsRef);
2120 #if (MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5)
2121             // This is the font activation API for OS X 10.5.  Only compile
2122             // this code if we're building on OS X 10.5 or later.
2123             if (NULL != ATSFontActivateFromFileReference) { // Weakly linked
2124                 ATSFontActivateFromFileReference(&fsRef, kATSFontContextLocal,
2125                                                  kATSFontFormatUnspecified,
2126                                                  NULL, kATSOptionFlagsDefault,
2127                                                  &fontContainerRef);
2128             }
2129 #endif
2130 #if (MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_5)
2131             // The following font activation API was deprecated in OS X 10.5.
2132             // Don't compile this code unless we're targeting OS X 10.4.
2133             FSSpec fsSpec;
2134             if (fontContainerRef == 0 &&
2135                     FSGetCatalogInfo(&fsRef, kFSCatInfoNone, NULL, NULL,
2136                                      &fsSpec, NULL) == noErr) {
2137                 ATSFontActivateFromFileSpecification(&fsSpec,
2138                         kATSFontContextLocal, kATSFontFormatUnspecified, NULL,
2139                         kATSOptionFlagsDefault, &fontContainerRef);
2140             }
2141 #endif
2142         }
2143     }
2145     if (!fontContainerRef) {
2146         ASLogNotice(@"Failed to activate the default font (the app bundle "
2147                     "may be incomplete)");
2148     }
2151 - (int)executeInLoginShell:(NSString *)path arguments:(NSArray *)args
2153     // Start a login shell and execute the command 'path' with arguments 'args'
2154     // in the shell.  This ensures that user environment variables are set even
2155     // when MacVim was started from the Finder.
2157     int pid = -1;
2158     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
2160     // Determine which shell to use to execute the command.  The user
2161     // may decide which shell to use by setting a user default or the
2162     // $SHELL environment variable.
2163     NSString *shell = [ud stringForKey:MMLoginShellCommandKey];
2164     if (!shell || [shell length] == 0)
2165         shell = [[[NSProcessInfo processInfo] environment]
2166             objectForKey:@"SHELL"];
2167     if (!shell)
2168         shell = @"/bin/bash";
2170     // Bash needs the '-l' flag to launch a login shell.  The user may add
2171     // flags by setting a user default.
2172     NSString *shellArgument = [ud stringForKey:MMLoginShellArgumentKey];
2173     if (!shellArgument || [shellArgument length] == 0) {
2174         if ([[shell lastPathComponent] isEqual:@"bash"])
2175             shellArgument = @"-l";
2176         else
2177             shellArgument = nil;
2178     }
2180     // Build input string to pipe to the login shell.
2181     NSMutableString *input = [NSMutableString stringWithFormat:
2182             @"exec \"%@\"", path];
2183     if (args) {
2184         // Append all arguments, making sure they are properly quoted, even
2185         // when they contain single quotes.
2186         NSEnumerator *e = [args objectEnumerator];
2187         id obj;
2189         while ((obj = [e nextObject])) {
2190             NSMutableString *arg = [NSMutableString stringWithString:obj];
2191             [arg replaceOccurrencesOfString:@"'" withString:@"'\"'\"'"
2192                                     options:NSLiteralSearch
2193                                       range:NSMakeRange(0, [arg length])];
2194             [input appendFormat:@" '%@'", arg];
2195         }
2196     }
2198     // Build the argument vector used to start the login shell.
2199     NSString *shellArg0 = [NSString stringWithFormat:@"-%@",
2200              [shell lastPathComponent]];
2201     char *shellArgv[3] = { (char *)[shellArg0 UTF8String], NULL, NULL };
2202     if (shellArgument)
2203         shellArgv[1] = (char *)[shellArgument UTF8String];
2205     // Get the C string representation of the shell path before the fork since
2206     // we must not call Foundation functions after a fork.
2207     const char *shellPath = [shell fileSystemRepresentation];
2209     // Fork and execute the process.
2210     int ds[2];
2211     if (pipe(ds)) return -1;
2213     pid = fork();
2214     if (pid == -1) {
2215         return -1;
2216     } else if (pid == 0) {
2217         // Child process
2219         if (close(ds[1]) == -1) exit(255);
2220         if (dup2(ds[0], 0) == -1) exit(255);
2222         // Without the following call warning messages like this appear on the
2223         // console:
2224         //     com.apple.launchd[69] : Stray process with PGID equal to this
2225         //                             dead job: PID 1589 PPID 1 Vim
2226         setsid();
2228         execv(shellPath, shellArgv);
2230         // Never reached unless execv fails
2231         exit(255);
2232     } else {
2233         // Parent process
2234         if (close(ds[0]) == -1) return -1;
2236         // Send input to execute to the child process
2237         [input appendString:@"\n"];
2238         int bytes = [input lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
2240         if (write(ds[1], [input UTF8String], bytes) != bytes) return -1;
2241         if (close(ds[1]) == -1) return -1;
2243         ++numChildProcesses;
2244         ASLogDebug(@"new process pid=%d (count=%d)", pid, numChildProcesses);
2245     }
2247     return pid;
2250 - (void)reapChildProcesses:(id)sender
2252     // NOTE: numChildProcesses (currently) only counts the number of Vim
2253     // processes that have been started with executeInLoginShell::.  If other
2254     // processes are spawned this code may need to be adjusted (or
2255     // numChildProcesses needs to be incremented when such a process is
2256     // started).
2257     while (numChildProcesses > 0) {
2258         int status = 0;
2259         int pid = waitpid(-1, &status, WNOHANG);
2260         if (pid <= 0)
2261             break;
2263         ASLogDebug(@"Wait for pid=%d complete", pid);
2264         --numChildProcesses;
2265     }
2268 - (void)processInputQueues:(id)sender
2270     // NOTE: Because we use distributed objects it is quite possible for this
2271     // function to be re-entered.  This can cause all sorts of unexpected
2272     // problems so we guard against it here so that the rest of the code does
2273     // not need to worry about it.
2275     // The processing flag is > 0 if this function is already on the call
2276     // stack; < 0 if this function was also re-entered.
2277     if (processingFlag != 0) {
2278         ASLogDebug(@"BUSY!");
2279         processingFlag = -1;
2280         return;
2281     }
2283     // NOTE: Be _very_ careful that no exceptions can be raised between here
2284     // and the point at which 'processingFlag' is reset.  Otherwise the above
2285     // test could end up always failing and no input queues would ever be
2286     // processed!
2287     processingFlag = 1;
2289     // NOTE: New input may arrive while we're busy processing; we deal with
2290     // this by putting the current queue aside and creating a new input queue
2291     // for future input.
2292     NSDictionary *queues = inputQueues;
2293     inputQueues = [NSMutableDictionary new];
2295     // Pass each input queue on to the vim controller with matching
2296     // identifier (and note that it could be cached).
2297     NSEnumerator *e = [queues keyEnumerator];
2298     NSNumber *key;
2299     while ((key = [e nextObject])) {
2300         unsigned ukey = [key unsignedIntValue];
2301         int i = 0, count = [vimControllers count];
2302         for (i = 0; i < count; ++i) {
2303             MMVimController *vc = [vimControllers objectAtIndex:i];
2304             if (ukey == [vc vimControllerId]) {
2305                 [vc processInputQueue:[queues objectForKey:key]]; // !exceptions
2306                 break;
2307             }
2308         }
2310         if (i < count) continue;
2312         count = [cachedVimControllers count];
2313         for (i = 0; i < count; ++i) {
2314             MMVimController *vc = [cachedVimControllers objectAtIndex:i];
2315             if (ukey == [vc vimControllerId]) {
2316                 [vc processInputQueue:[queues objectForKey:key]]; // !exceptions
2317                 break;
2318             }
2319         }
2321         if (i == count) {
2322             ASLogWarn(@"No Vim controller for identifier=%d", ukey);
2323         }
2324     }
2326     [queues release];
2328     // If new input arrived while we were processing it would have been
2329     // blocked so we have to schedule it to be processed again.
2330     if (processingFlag < 0)
2331         [self performSelectorOnMainThread:@selector(processInputQueues:)
2332                                withObject:nil
2333                             waitUntilDone:NO
2334                                     modes:[NSArray arrayWithObjects:
2335                                            NSDefaultRunLoopMode,
2336                                            NSEventTrackingRunLoopMode, nil]];
2338     processingFlag = 0;
2341 - (void)addVimController:(MMVimController *)vc
2343     ASLogDebug(@"Add Vim controller pid=%d id=%d",
2344             [vc pid], [vc vimControllerId]);
2346     int pid = [vc pid];
2347     NSNumber *pidKey = [NSNumber numberWithInt:pid];
2349     if (preloadPid == pid) {
2350         // This controller was preloaded, so add it to the cache and
2351         // schedule another vim process to be preloaded.
2352         preloadPid = -1;
2353         [vc setIsPreloading:YES];
2354         [cachedVimControllers addObject:vc];
2355         [self scheduleVimControllerPreloadAfterDelay:1];
2356     } else {
2357         [vimControllers addObject:vc];
2359         id args = [pidArguments objectForKey:pidKey];
2360         if (args && [NSNull null] != args)
2361             [vc passArguments:args];
2363         // HACK!  MacVim does not get activated if it is launched from the
2364         // terminal, so we forcibly activate here unless it is an untitled
2365         // window opening.  Untitled windows are treated differently, else
2366         // MacVim would steal the focus if another app was activated while the
2367         // untitled window was loading.
2368         if (!args || args != [NSNull null])
2369             [self activateWhenNextWindowOpens];
2371         if (args)
2372             [pidArguments removeObjectForKey:pidKey];
2373     }
2376 - (NSDictionary *)convertVimControllerArguments:(NSDictionary *)args
2377                                   toCommandLine:(NSArray **)cmdline
2379     // Take all arguments out of 'args' and put them on an array suitable to
2380     // pass as arguments to launchVimProcessWithArguments:.  The untouched
2381     // dictionary items are returned in a new autoreleased dictionary.
2383     if (cmdline)
2384         *cmdline = nil;
2386     NSArray *filenames = [args objectForKey:@"filenames"];
2387     int numFiles = filenames ? [filenames count] : 0;
2388     BOOL openFiles = ![[args objectForKey:@"dontOpen"] boolValue];
2390     if (numFiles <= 0 || !openFiles)
2391         return args;
2393     NSMutableArray *a = [NSMutableArray array];
2394     NSMutableDictionary *d = [[args mutableCopy] autorelease];
2396     // Search for text using "+/text".
2397     NSString *searchText = [args objectForKey:@"searchText"];
2398     if (searchText) {
2399         // TODO: If the search pattern is not found an error is shown when
2400         // starting.  Figure out a way to get rid of this message (The help
2401         // says to use ':silent exe "normal /pat\<CR>"' but this does not
2402         // work.)
2403         [a addObject:[NSString stringWithFormat:@"+/%@", searchText]];
2405         [d removeObjectForKey:@"searchText"];
2406     }
2408     // Position cursor using "+line" or "-c :cal cursor(line,column)".
2409     NSString *lineString = [args objectForKey:@"cursorLine"];
2410     if (lineString && [lineString intValue] > 0) {
2411         NSString *columnString = [args objectForKey:@"cursorColumn"];
2412         if (columnString && [columnString intValue] > 0) {
2413             [a addObject:@"-c"];
2414             [a addObject:[NSString stringWithFormat:@":cal cursor(%@,%@)",
2415                           lineString, columnString]];
2417             [d removeObjectForKey:@"cursorColumn"];
2418         } else {
2419             [a addObject:[NSString stringWithFormat:@"+%@", lineString]];
2420         }
2422         [d removeObjectForKey:@"cursorLine"];
2423     }
2425     // Set selection using normal mode commands.
2426     NSString *rangeString = [args objectForKey:@"selectionRange"];
2427     if (rangeString) {
2428         NSRange r = NSRangeFromString(rangeString);
2429         [a addObject:@"-c"];
2430         if (r.length > 0) {
2431             // Select given range.
2432             [a addObject:[NSString stringWithFormat:@"norm %dGV%dGz.0",
2433                                                 NSMaxRange(r), r.location]];
2434         } else {
2435             // Position cursor on start of range.
2436             [a addObject:[NSString stringWithFormat:@"norm %dGz.0",
2437                                                                 r.location]];
2438         }
2440         [d removeObjectForKey:@"selectionRange"];
2441     }
2443     // Choose file layout using "-[o|O|p]".
2444     int layout = [[args objectForKey:@"layout"] intValue];
2445     switch (layout) {
2446         case MMLayoutHorizontalSplit: [a addObject:@"-o"]; break;
2447         case MMLayoutVerticalSplit:   [a addObject:@"-O"]; break;
2448         case MMLayoutTabs:            [a addObject:@"-p"]; break;
2449     }
2450     [d removeObjectForKey:@"layout"];
2453     // Last of all add the names of all files to open (DO NOT add more args
2454     // after this point).
2455     [a addObjectsFromArray:filenames];
2457     if ([args objectForKey:@"remoteID"]) {
2458         // These files should be edited remotely so keep the filenames on the
2459         // argument list -- they will need to be passed back to Vim when it
2460         // checks in.  Also set the 'dontOpen' flag or the files will be
2461         // opened twice.
2462         [d setObject:[NSNumber numberWithBool:YES] forKey:@"dontOpen"];
2463     } else {
2464         [d removeObjectForKey:@"dontOpen"];
2465         [d removeObjectForKey:@"filenames"];
2466     }
2468     if (cmdline)
2469         *cmdline = a;
2471     return d;
2474 - (NSString *)workingDirectoryForArguments:(NSDictionary *)args
2476     // Find the "filenames" argument and pick the first path that actually
2477     // exists and return it.
2478     // TODO: Return common parent directory in the case of multiple files?
2479     NSFileManager *fm = [NSFileManager defaultManager];
2480     NSArray *filenames = [args objectForKey:@"filenames"];
2481     NSUInteger i, count = [filenames count];
2482     for (i = 0; i < count; ++i) {
2483         BOOL isdir;
2484         NSString *file = [filenames objectAtIndex:i];
2485         if ([fm fileExistsAtPath:file isDirectory:&isdir])
2486             return isdir ? file : [file stringByDeletingLastPathComponent];
2487     }
2489     return nil;
2492 - (NSScreen *)screenContainingPoint:(NSPoint)pt
2494     NSArray *screens = [NSScreen screens];
2495     NSUInteger i, count = [screens count];
2496     for (i = 0; i < count; ++i) {
2497         NSScreen *screen = [screens objectAtIndex:i];
2498         NSRect frame = [screen frame];
2499         if (NSPointInRect(pt, frame))
2500             return screen;
2501     }
2503     return nil;
2506 @end // MMAppController (Private)