Remove feat/core-text from README-repo.txt
[MacVim.git] / src / MacVim / MMAppController.m
blobe0895dd112d1cda41db7b96d4c7bcb7f9cde0367
1 /* vi:set ts=8 sts=4 sw=4 ft=objc:
2  *
3  * VIM - Vi IMproved            by Bram Moolenaar
4  *                              MacVim GUI port by Bjorn Winckler
5  *
6  * Do ":help uganda"  in Vim to read copying and usage conditions.
7  * Do ":help credits" in Vim to see a list of people who contributed.
8  * See README.txt for an overview of the Vim source code.
9  */
11  * MMAppController
12  *
13  * MMAppController is the delegate of NSApp and as such handles file open
14  * requests, application termination, etc.  It sets up a named NSConnection on
15  * which it listens to incoming connections from Vim processes.  It also
16  * coordinates all MMVimControllers and takes care of the main menu.
17  *
18  * A new Vim process is started by calling launchVimProcessWithArguments:.
19  * When the Vim process is initialized it notifies the app controller by
20  * sending a connectBackend:pid: message.  At this point a new MMVimController
21  * is allocated.  Afterwards, the Vim process communicates directly with its
22  * MMVimController.
23  *
24  * A Vim process started from the command line connects directly by sending the
25  * connectBackend:pid: message (launchVimProcessWithArguments: is never called
26  * in this case).
27  *
28  * The main menu is handled as follows.  Each Vim controller keeps its own main
29  * menu.  All menus except the "MacVim" menu are controlled by the Vim process.
30  * The app controller also keeps a reference to the "default main menu" which
31  * is set up in MainMenu.nib.  When no editor window is open the default main
32  * menu is used.  When a new editor window becomes main its main menu becomes
33  * the new main menu, this is done in -[MMAppController setMainMenu:].
34  *   NOTE: Certain heuristics are used to find the "MacVim", "Windows", "File",
35  * and "Services" menu.  If MainMenu.nib changes these heuristics may have to
36  * change as well.  For specifics see the find... methods defined in the NSMenu
37  * category "MMExtras".
38  */
40 #import "MMAppController.h"
41 #import "MMPreferenceController.h"
42 #import "MMVimController.h"
43 #import "MMWindowController.h"
44 #import "Miscellaneous.h"
46 #ifdef MM_ENABLE_PLUGINS
47 #import "MMPlugInManager.h"
48 #endif
50 #import <unistd.h>
51 #import <CoreServices/CoreServices.h>
54 #define MM_HANDLE_XCODE_MOD_EVENT 0
58 // Default timeout intervals on all connections.
59 static NSTimeInterval MMRequestTimeout = 5;
60 static NSTimeInterval MMReplyTimeout = 5;
62 static NSString *MMWebsiteString = @"http://code.google.com/p/macvim/";
64 #if (MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5)
65 // Latency (in s) between FS event occuring and being reported to MacVim.
66 // Should be small so that MacVim is notified of changes to the ~/.vim
67 // directory more or less immediately.
68 static CFTimeInterval MMEventStreamLatency = 0.1;
69 #endif
71 static float MMCascadeHorizontalOffset = 21;
72 static float MMCascadeVerticalOffset = 23;
75 #pragma pack(push,1)
76 // The alignment and sizes of these fields are based on trial-and-error.  It
77 // may be necessary to adjust them to fit if Xcode ever changes this struct.
78 typedef struct
80     int16_t unused1;      // 0 (not used)
81     int16_t lineNum;      // line to select (< 0 to specify range)
82     int32_t startRange;   // start of selection range (if line < 0)
83     int32_t endRange;     // end of selection range (if line < 0)
84     int32_t unused2;      // 0 (not used)
85     int32_t theDate;      // modification date/time
86 } MMXcodeSelectionRange;
87 #pragma pack(pop)
90 // This is a private AppKit API gleaned from class-dump.
91 @interface NSKeyBindingManager : NSObject
92 + (id)sharedKeyBindingManager;
93 - (id)dictionary;
94 - (void)setDictionary:(id)arg1;
95 @end
98 @interface MMAppController (MMServices)
99 - (void)openSelection:(NSPasteboard *)pboard userData:(NSString *)userData
100                 error:(NSString **)error;
101 - (void)openFile:(NSPasteboard *)pboard userData:(NSString *)userData
102            error:(NSString **)error;
103 - (void)newFileHere:(NSPasteboard *)pboard userData:(NSString *)userData
104               error:(NSString **)error;
105 @end
108 @interface MMAppController (Private)
109 - (MMVimController *)topmostVimController;
110 - (int)launchVimProcessWithArguments:(NSArray *)args;
111 - (NSArray *)filterFilesAndNotify:(NSArray *)files;
112 - (NSArray *)filterOpenFiles:(NSArray *)filenames
113                openFilesDict:(NSDictionary **)openFiles;
114 #if MM_HANDLE_XCODE_MOD_EVENT
115 - (void)handleXcodeModEvent:(NSAppleEventDescriptor *)event
116                  replyEvent:(NSAppleEventDescriptor *)reply;
117 #endif
118 - (void)handleGetURLEvent:(NSAppleEventDescriptor *)event
119                replyEvent:(NSAppleEventDescriptor *)reply;
120 - (int)findLaunchingProcessWithoutArguments;
121 - (MMVimController *)findUnusedEditor;
122 - (NSMutableDictionary *)extractArgumentsFromOdocEvent:
123     (NSAppleEventDescriptor *)desc;
124 - (void)scheduleVimControllerPreloadAfterDelay:(NSTimeInterval)delay;
125 - (void)cancelVimControllerPreloadRequests;
126 - (void)preloadVimController:(id)sender;
127 - (int)maxPreloadCacheSize;
128 - (MMVimController *)takeVimControllerFromCache;
129 - (void)clearPreloadCacheWithCount:(int)count;
130 - (void)rebuildPreloadCache;
131 - (NSDate *)rcFilesModificationDate;
132 - (BOOL)openVimControllerWithArguments:(NSDictionary *)arguments;
133 - (void)activateWhenNextWindowOpens;
134 - (void)startWatchingVimDir;
135 - (void)stopWatchingVimDir;
136 - (void)handleFSEvent;
137 - (void)loadDefaultFont;
138 - (int)executeInLoginShell:(NSString *)path arguments:(NSArray *)args;
139 - (void)reapChildProcesses:(id)sender;
140 - (void)processInputQueues:(id)sender;
141 - (void)addVimController:(MMVimController *)vc;
142 - (NSScreen *)screenContainingPoint:(NSPoint)pt;
144 #ifdef MM_ENABLE_PLUGINS
145 - (void)removePlugInMenu;
146 - (void)addPlugInMenuToMenu:(NSMenu *)mainMenu;
147 #endif
148 @end
152 #if (MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5)
153     static void
154 fsEventCallback(ConstFSEventStreamRef streamRef,
155                 void *clientCallBackInfo,
156                 size_t numEvents,
157                 void *eventPaths,
158                 const FSEventStreamEventFlags eventFlags[],
159                 const FSEventStreamEventId eventIds[])
161     [[MMAppController sharedInstance] handleFSEvent];
163 #endif
165 @implementation MMAppController
167 + (void)initialize
169     static BOOL initDone = NO;
170     if (initDone) return;
171     initDone = YES;
173     ASLInit();
175     // HACK! The following user default must be reset, else Ctrl-q (or
176     // whichever key is specified by the default) will be blocked by the input
177     // manager (interpretKeyEvents: swallows that key).  (We can't use
178     // NSUserDefaults since it only allows us to write to the registration
179     // domain and this preference has "higher precedence" than that so such a
180     // change would have no effect.)
181     CFPreferencesSetAppValue(CFSTR("NSQuotedKeystrokeBinding"),
182                              CFSTR(""),
183                              kCFPreferencesCurrentApplication);
185     // Also disable NSRepeatCountBinding -- it is not enabled by default, but
186     // it does not make much sense to support it since Vim has its own way of
187     // dealing with repeat counts.
188     CFPreferencesSetAppValue(CFSTR("NSRepeatCountBinding"),
189                              CFSTR(""),
190                              kCFPreferencesCurrentApplication);
191     
192     NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:
193         [NSNumber numberWithBool:NO],   MMNoWindowKey,
194         [NSNumber numberWithInt:64],    MMTabMinWidthKey,
195         [NSNumber numberWithInt:6*64],  MMTabMaxWidthKey,
196         [NSNumber numberWithInt:132],   MMTabOptimumWidthKey,
197         [NSNumber numberWithBool:YES],  MMShowAddTabButtonKey,
198         [NSNumber numberWithInt:2],     MMTextInsetLeftKey,
199         [NSNumber numberWithInt:1],     MMTextInsetRightKey,
200         [NSNumber numberWithInt:1],     MMTextInsetTopKey,
201         [NSNumber numberWithInt:1],     MMTextInsetBottomKey,
202         @"MMTypesetter",                MMTypesetterKey,
203         [NSNumber numberWithFloat:1],   MMCellWidthMultiplierKey,
204         [NSNumber numberWithFloat:-1],  MMBaselineOffsetKey,
205         [NSNumber numberWithBool:YES],  MMTranslateCtrlClickKey,
206         [NSNumber numberWithInt:0],     MMOpenInCurrentWindowKey,
207         [NSNumber numberWithBool:NO],   MMNoFontSubstitutionKey,
208         [NSNumber numberWithBool:YES],  MMLoginShellKey,
209         [NSNumber numberWithBool:NO],   MMAtsuiRendererKey,
210         [NSNumber numberWithInt:0],     MMRendererKey,
211         [NSNumber numberWithInt:MMUntitledWindowAlways],
212                                         MMUntitledWindowKey,
213         [NSNumber numberWithBool:NO],   MMTexturedWindowKey,
214         [NSNumber numberWithBool:NO],   MMZoomBothKey,
215         @"",                            MMLoginShellCommandKey,
216         @"",                            MMLoginShellArgumentKey,
217         [NSNumber numberWithBool:YES],  MMDialogsTrackPwdKey,
218 #ifdef MM_ENABLE_PLUGINS
219         [NSNumber numberWithBool:YES],  MMShowLeftPlugInContainerKey,
220 #endif
221         [NSNumber numberWithInt:3],     MMOpenLayoutKey,
222         [NSNumber numberWithBool:NO],   MMVerticalSplitKey,
223         [NSNumber numberWithInt:0],     MMPreloadCacheSizeKey,
224         [NSNumber numberWithInt:0],     MMLastWindowClosedBehaviorKey,
225         [NSNumber numberWithBool:YES],  MMLoadDefaultFontKey,
226 #ifdef INCLUDE_OLD_IM_CODE
227         [NSNumber numberWithBool:YES],  MMUseInlineImKey,
228 #endif // INCLUDE_OLD_IM_CODE
229         nil];
231     [[NSUserDefaults standardUserDefaults] registerDefaults:dict];
233     NSArray *types = [NSArray arrayWithObject:NSStringPboardType];
234     [NSApp registerServicesMenuSendTypes:types returnTypes:types];
236     // NOTE: Set the current directory to user's home directory, otherwise it
237     // will default to the root directory.  (This matters since new Vim
238     // processes inherit MacVim's environment variables.)
239     [[NSFileManager defaultManager] changeCurrentDirectoryPath:
240             NSHomeDirectory()];
243 - (id)init
245     if (!(self = [super init])) return nil;
247     [self loadDefaultFont];
249     vimControllers = [NSMutableArray new];
250     cachedVimControllers = [NSMutableArray new];
251     preloadPid = -1;
252     pidArguments = [NSMutableDictionary new];
253     inputQueues = [NSMutableDictionary new];
255 #ifdef MM_ENABLE_PLUGINS
256     NSString *plugInTitle = NSLocalizedString(@"Plug-In",
257                                               @"Plug-In menu title");
258     plugInMenuItem = [[NSMenuItem alloc] initWithTitle:plugInTitle
259                                                 action:NULL
260                                          keyEquivalent:@""];
261     NSMenu *submenu = [[NSMenu alloc] initWithTitle:plugInTitle];
262     [plugInMenuItem setSubmenu:submenu];
263     [submenu release];
264 #endif
266     // NOTE: Do not use the default connection since the Logitech Control
267     // Center (LCC) input manager steals and this would cause MacVim to
268     // never open any windows.  (This is a bug in LCC but since they are
269     // unlikely to fix it, we graciously give them the default connection.)
270     connection = [[NSConnection alloc] initWithReceivePort:[NSPort port]
271                                                   sendPort:nil];
272     [connection setRootObject:self];
273     [connection setRequestTimeout:MMRequestTimeout];
274     [connection setReplyTimeout:MMReplyTimeout];
276     // NOTE!  If the name of the connection changes here it must also be
277     // updated in MMBackend.m.
278     NSString *name = [NSString stringWithFormat:@"%@-connection",
279              [[NSBundle mainBundle] bundlePath]];
280     if (![connection registerName:name]) {
281         ASLogCrit(@"Failed to register connection with name '%@'", name);
282         [connection release];  connection = nil;
283     }
285     return self;
288 - (void)dealloc
290     ASLogDebug(@"");
292     [connection release];  connection = nil;
293     [inputQueues release];  inputQueues = nil;
294     [pidArguments release];  pidArguments = nil;
295     [vimControllers release];  vimControllers = nil;
296     [cachedVimControllers release];  cachedVimControllers = nil;
297     [openSelectionString release];  openSelectionString = nil;
298     [recentFilesMenuItem release];  recentFilesMenuItem = nil;
299     [defaultMainMenu release];  defaultMainMenu = nil;
300 #ifdef MM_ENABLE_PLUGINS
301     [plugInMenuItem release];  plugInMenuItem = nil;
302 #endif
303     [appMenuItemTemplate release];  appMenuItemTemplate = nil;
305     [super dealloc];
308 - (void)applicationWillFinishLaunching:(NSNotification *)notification
310     // Remember the default menu so that it can be restored if the user closes
311     // all editor windows.
312     defaultMainMenu = [[NSApp mainMenu] retain];
314     // Store a copy of the default app menu so we can use this as a template
315     // for all other menus.  We make a copy here because the "Services" menu
316     // will not yet have been populated at this time.  If we don't we get
317     // problems trying to set key equivalents later on because they might clash
318     // with items on the "Services" menu.
319     appMenuItemTemplate = [defaultMainMenu itemAtIndex:0];
320     appMenuItemTemplate = [appMenuItemTemplate copy];
322     // Set up the "Open Recent" menu. See
323     //   http://lapcatsoftware.com/blog/2007/07/10/
324     //     working-without-a-nib-part-5-open-recent-menu/
325     // and
326     //   http://www.cocoabuilder.com/archive/message/cocoa/2007/8/15/187793
327     // for more information.
328     //
329     // The menu itself is created in MainMenu.nib but we still seem to have to
330     // hack around a bit to get it to work.  (This has to be done in
331     // applicationWillFinishLaunching at the latest, otherwise it doesn't
332     // work.)
333     NSMenu *fileMenu = [defaultMainMenu findFileMenu];
334     if (fileMenu) {
335         int idx = [fileMenu indexOfItemWithAction:@selector(fileOpen:)];
336         if (idx >= 0 && idx+1 < [fileMenu numberOfItems])
338         recentFilesMenuItem = [fileMenu itemWithTitle:@"Open Recent"];
339         [[recentFilesMenuItem submenu] performSelector:@selector(_setMenuName:)
340                                         withObject:@"NSRecentDocumentsMenu"];
342         // Note: The "Recent Files" menu must be moved around since there is no
343         // -[NSApp setRecentFilesMenu:] method.  We keep a reference to it to
344         // facilitate this move (see setMainMenu: below).
345         [recentFilesMenuItem retain];
346     }
348 #if MM_HANDLE_XCODE_MOD_EVENT
349     [[NSAppleEventManager sharedAppleEventManager]
350             setEventHandler:self
351                 andSelector:@selector(handleXcodeModEvent:replyEvent:)
352               forEventClass:'KAHL'
353                  andEventID:'MOD '];
354 #endif
356     // Register 'mvim://' URL handler
357     [[NSAppleEventManager sharedAppleEventManager]
358             setEventHandler:self
359                 andSelector:@selector(handleGetURLEvent:replyEvent:)
360               forEventClass:kInternetEventClass
361                  andEventID:kAEGetURL];
363     // Disable the default Cocoa "Key Bindings" since they interfere with the
364     // way Vim handles keyboard input.  Cocoa reads bindings from
365     //     /System/Library/Frameworks/AppKit.framework/Resources/
366     //                                                  StandardKeyBinding.dict
367     // and
368     //     ~/Library/KeyBindings/DefaultKeyBinding.dict
369     // To avoid having the user accidentally break keyboard handling (by
370     // modifying the latter in some unexpected way) in MacVim we load our own
371     // key binding dictionary from Resource/KeyBinding.plist.  We can't disable
372     // the bindings completely since it would break keyboard handling in
373     // dialogs so the our custom dictionary contains all the entries from the
374     // former location.
375     //
376     // It is possible to disable key bindings completely by not calling
377     // interpretKeyEvents: in keyDown: but this also disables key bindings used
378     // by certain input methods.  E.g.  Ctrl-Shift-; would no longer work in
379     // the Kotoeri input manager.
380     //
381     // To solve this problem we access a private API and set the key binding
382     // dictionary to our own custom dictionary here.  At this time Cocoa will
383     // have already read the above mentioned dictionaries so it (hopefully)
384     // won't try to change the key binding dictionary again after this point.
385     NSKeyBindingManager *mgr = [NSKeyBindingManager sharedKeyBindingManager];
386     NSBundle *mainBundle = [NSBundle mainBundle];
387     NSString *path = [mainBundle pathForResource:@"KeyBinding"
388                                           ofType:@"plist"];
389     NSDictionary *dict = [NSDictionary dictionaryWithContentsOfFile:path];
390     if (mgr && dict) {
391         [mgr setDictionary:dict];
392     } else {
393         ASLogNotice(@"Failed to override the Cocoa key bindings.  Keyboard "
394                 "input may behave strangely as a result (path=%@).", path);
395     }
398 - (void)applicationDidFinishLaunching:(NSNotification *)notification
400     [NSApp setServicesProvider:self];
401 #ifdef MM_ENABLE_PLUGINS
402     [[MMPlugInManager sharedManager] loadAllPlugIns];
403 #endif
405     if ([self maxPreloadCacheSize] > 0) {
406         [self scheduleVimControllerPreloadAfterDelay:2];
407         [self startWatchingVimDir];
408     }
410     ASLogInfo(@"MacVim finished launching");
413 - (BOOL)applicationShouldOpenUntitledFile:(NSApplication *)sender
415     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
416     NSAppleEventManager *aem = [NSAppleEventManager sharedAppleEventManager];
417     NSAppleEventDescriptor *desc = [aem currentAppleEvent];
419     // The user default MMUntitledWindow can be set to control whether an
420     // untitled window should open on 'Open' and 'Reopen' events.
421     int untitledWindowFlag = [ud integerForKey:MMUntitledWindowKey];
423     BOOL isAppOpenEvent = [desc eventID] == kAEOpenApplication;
424     if (isAppOpenEvent && (untitledWindowFlag & MMUntitledWindowOnOpen) == 0)
425         return NO;
427     BOOL isAppReopenEvent = [desc eventID] == kAEReopenApplication;
428     if (isAppReopenEvent
429             && (untitledWindowFlag & MMUntitledWindowOnReopen) == 0)
430         return NO;
432     // When a process is started from the command line, the 'Open' event may
433     // contain a parameter to surpress the opening of an untitled window.
434     desc = [desc paramDescriptorForKeyword:keyAEPropData];
435     desc = [desc paramDescriptorForKeyword:keyMMUntitledWindow];
436     if (desc && ![desc booleanValue])
437         return NO;
439     // Never open an untitled window if there is at least one open window or if
440     // there are processes that are currently launching.
441     if ([vimControllers count] > 0 || [pidArguments count] > 0)
442         return NO;
444     // NOTE!  This way it possible to start the app with the command-line
445     // argument '-nowindow yes' and no window will be opened by default but
446     // this argument will only be heeded when the application is opening.
447     if (isAppOpenEvent && [ud boolForKey:MMNoWindowKey] == YES)
448         return NO;
450     return YES;
453 - (BOOL)applicationOpenUntitledFile:(NSApplication *)sender
455     ASLogDebug(@"Opening untitled window...");
456     [self newWindow:self];
457     return YES;
460 - (void)application:(NSApplication *)sender openFiles:(NSArray *)filenames
462     ASLogInfo(@"Opening files %@", filenames);
464     // Extract ODB/Xcode/Spotlight parameters from the current Apple event,
465     // sort the filenames, and then let openFiles:withArguments: do the heavy
466     // lifting.
468     if (!(filenames && [filenames count] > 0))
469         return;
471     // Sort filenames since the Finder doesn't take care in preserving the
472     // order in which files are selected anyway (and "sorted" is more
473     // predictable than "random").
474     if ([filenames count] > 1)
475         filenames = [filenames sortedArrayUsingSelector:
476                 @selector(localizedCompare:)];
478     // Extract ODB/Xcode/Spotlight parameters from the current Apple event
479     NSMutableDictionary *arguments = [self extractArgumentsFromOdocEvent:
480             [[NSAppleEventManager sharedAppleEventManager] currentAppleEvent]];
482     if ([self openFiles:filenames withArguments:arguments]) {
483         [NSApp replyToOpenOrPrint:NSApplicationDelegateReplySuccess];
484     } else {
485         // TODO: Notify user of failure?
486         [NSApp replyToOpenOrPrint:NSApplicationDelegateReplyFailure];
487     }
490 - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender
492     return (MMTerminateWhenLastWindowClosed ==
493             [[NSUserDefaults standardUserDefaults]
494                 integerForKey:MMLastWindowClosedBehaviorKey]);
497 - (NSApplicationTerminateReply)applicationShouldTerminate:
498     (NSApplication *)sender
500     // TODO: Follow Apple's guidelines for 'Graceful Application Termination'
501     // (in particular, allow user to review changes and save).
502     int reply = NSTerminateNow;
503     BOOL modifiedBuffers = NO;
505     // Go through windows, checking for modified buffers.  (Each Vim process
506     // tells MacVim when any buffer has been modified and MacVim sets the
507     // 'documentEdited' flag of the window correspondingly.)
508     NSEnumerator *e = [[NSApp windows] objectEnumerator];
509     id window;
510     while ((window = [e nextObject])) {
511         if ([window isDocumentEdited]) {
512             modifiedBuffers = YES;
513             break;
514         }
515     }
517     if (modifiedBuffers) {
518         NSAlert *alert = [[NSAlert alloc] init];
519         [alert setAlertStyle:NSWarningAlertStyle];
520         [alert addButtonWithTitle:NSLocalizedString(@"Quit",
521                 @"Dialog button")];
522         [alert addButtonWithTitle:NSLocalizedString(@"Cancel",
523                 @"Dialog button")];
524         [alert setMessageText:NSLocalizedString(@"Quit without saving?",
525                 @"Quit dialog with changed buffers, title")];
526         [alert setInformativeText:NSLocalizedString(
527                 @"There are modified buffers, "
528                 "if you quit now all changes will be lost.  Quit anyway?",
529                 @"Quit dialog with changed buffers, text")];
531         if ([alert runModal] != NSAlertFirstButtonReturn)
532             reply = NSTerminateCancel;
534         [alert release];
535     } else {
536         // No unmodified buffers, but give a warning if there are multiple
537         // windows and/or tabs open.
538         int numWindows = [vimControllers count];
539         int numTabs = 0;
541         // Count the number of open tabs
542         e = [vimControllers objectEnumerator];
543         id vc;
544         while ((vc = [e nextObject]))
545             numTabs += [[vc objectForVimStateKey:@"numTabs"] intValue];
547         if (numWindows > 1 || numTabs > 1) {
548             NSAlert *alert = [[NSAlert alloc] init];
549             [alert setAlertStyle:NSWarningAlertStyle];
550             [alert addButtonWithTitle:NSLocalizedString(@"Quit",
551                     @"Dialog button")];
552             [alert addButtonWithTitle:NSLocalizedString(@"Cancel",
553                     @"Dialog button")];
554             [alert setMessageText:NSLocalizedString(
555                     @"Are you sure you want to quit MacVim?",
556                     @"Quit dialog with no changed buffers, title")];
558             NSString *info = nil;
559             if (numWindows > 1) {
560                 if (numTabs > numWindows)
561                     info = [NSString stringWithFormat:NSLocalizedString(
562                             @"There are %d windows open in MacVim, with a "
563                             "total of %d tabs. Do you want to quit anyway?",
564                             @"Quit dialog with no changed buffers, text"),
565                          numWindows, numTabs];
566                 else
567                     info = [NSString stringWithFormat:NSLocalizedString(
568                             @"There are %d windows open in MacVim. "
569                             "Do you want to quit anyway?",
570                             @"Quit dialog with no changed buffers, text"),
571                         numWindows];
573             } else {
574                 info = [NSString stringWithFormat:NSLocalizedString(
575                         @"There are %d tabs open in MacVim. "
576                         "Do you want to quit anyway?",
577                         @"Quit dialog with no changed buffers, text"), 
578                      numTabs];
579             }
581             [alert setInformativeText:info];
583             if ([alert runModal] != NSAlertFirstButtonReturn)
584                 reply = NSTerminateCancel;
586             [alert release];
587         }
588     }
591     // Tell all Vim processes to terminate now (otherwise they'll leave swap
592     // files behind).
593     if (NSTerminateNow == reply) {
594         e = [vimControllers objectEnumerator];
595         id vc;
596         while ((vc = [e nextObject])) {
597             ASLogDebug(@"Terminate pid=%d", [vc pid]);
598             [vc sendMessage:TerminateNowMsgID data:nil];
599         }
601         e = [cachedVimControllers objectEnumerator];
602         while ((vc = [e nextObject])) {
603             ASLogDebug(@"Terminate pid=%d (cached)", [vc pid]);
604             [vc sendMessage:TerminateNowMsgID data:nil];
605         }
607         // If a Vim process is being preloaded as we quit we have to forcibly
608         // kill it since we have not established a connection yet.
609         if (preloadPid > 0) {
610             ASLogDebug(@"Kill incomplete preloaded process pid=%d", preloadPid);
611             kill(preloadPid, SIGKILL);
612         }
614         // If a Vim process was loading as we quit we also have to kill it.
615         e = [[pidArguments allKeys] objectEnumerator];
616         NSNumber *pidKey;
617         while ((pidKey = [e nextObject])) {
618             ASLogDebug(@"Kill incomplete process pid=%d", [pidKey intValue]);
619             kill([pidKey intValue], SIGKILL);
620         }
622         // Sleep a little to allow all the Vim processes to exit.
623         usleep(10000);
624     }
626     return reply;
629 - (void)applicationWillTerminate:(NSNotification *)notification
631     ASLogInfo(@"Terminating MacVim...");
633     [self stopWatchingVimDir];
635 #ifdef MM_ENABLE_PLUGINS
636     [[MMPlugInManager sharedManager] unloadAllPlugIns];
637 #endif
639 #if MM_HANDLE_XCODE_MOD_EVENT
640     [[NSAppleEventManager sharedAppleEventManager]
641             removeEventHandlerForEventClass:'KAHL'
642                                  andEventID:'MOD '];
643 #endif
645     // This will invalidate all connections (since they were spawned from this
646     // connection).
647     [connection invalidate];
649     // Deactivate the font we loaded from the app bundle.
650     // NOTE: This can take quite a while (~500 ms), so termination will be
651     // noticeably faster if loading of the default font is disabled.
652     if (fontContainerRef) {
653         ATSFontDeactivate(fontContainerRef, NULL, kATSOptionFlagsDefault);
654         fontContainerRef = 0;
655     }
657     [NSApp setDelegate:nil];
659     // Try to wait for all child processes to avoid leaving zombies behind (but
660     // don't wait around for too long).
661     NSDate *timeOutDate = [NSDate dateWithTimeIntervalSinceNow:2];
662     while ([timeOutDate timeIntervalSinceNow] > 0) {
663         [self reapChildProcesses:nil];
664         if (numChildProcesses <= 0)
665             break;
667         ASLogDebug(@"%d processes still left, hold on...", numChildProcesses);
669         // Run in NSConnectionReplyMode while waiting instead of calling e.g.
670         // usleep().  Otherwise incoming messages may clog up the DO queues and
671         // the outgoing TerminateNowMsgID sent earlier never reaches the Vim
672         // process.
673         // This has at least one side-effect, namely we may receive the
674         // annoying "dropping incoming DO message".  (E.g. this may happen if
675         // you quickly hit Cmd-n several times in a row and then immediately
676         // press Cmd-q, Enter.)
677         while (CFRunLoopRunInMode((CFStringRef)NSConnectionReplyMode,
678                 0.05, true) == kCFRunLoopRunHandledSource)
679             ;   // do nothing
680     }
682     if (numChildProcesses > 0) {
683         ASLogNotice(@"%d zombies left behind", numChildProcesses);
684     }
687 + (MMAppController *)sharedInstance
689     // Note: The app controller is a singleton which is instantiated in
690     // MainMenu.nib where it is also connected as the delegate of NSApp.
691     id delegate = [NSApp delegate];
692     return [delegate isKindOfClass:self] ? (MMAppController*)delegate : nil;
695 - (NSMenu *)defaultMainMenu
697     return defaultMainMenu;
700 - (NSMenuItem *)appMenuItemTemplate
702     return appMenuItemTemplate;
705 - (void)removeVimController:(id)controller
707     ASLogDebug(@"Remove Vim controller pid=%d id=%d (processingFlag=%d)",
708                [controller pid], [controller vimControllerId], processingFlag);
710     NSUInteger idx = [vimControllers indexOfObject:controller];
711     if (NSNotFound == idx) {
712         ASLogDebug(@"Controller not found, probably due to duplicate removal");
713         return;
714     }
716     [controller retain];
717     [vimControllers removeObjectAtIndex:idx];
718     [controller cleanup];
719     [controller release];
721     if (![vimControllers count]) {
722         // The last editor window just closed so restore the main menu back to
723         // its default state (which is defined in MainMenu.nib).
724         [self setMainMenu:defaultMainMenu];
726         BOOL hide = (MMHideWhenLastWindowClosed ==
727                     [[NSUserDefaults standardUserDefaults]
728                         integerForKey:MMLastWindowClosedBehaviorKey]);
729         if (hide)
730             [NSApp hide:self];
731     }
733     // There is a small delay before the Vim process actually exits so wait a
734     // little before trying to reap the child process.  If the process still
735     // hasn't exited after this wait it won't be reaped until the next time
736     // reapChildProcesses: is called (but this should be harmless).
737     [self performSelector:@selector(reapChildProcesses:)
738                withObject:nil
739                afterDelay:0.1];
742 - (void)windowControllerWillOpen:(MMWindowController *)windowController
744     NSPoint topLeft = NSZeroPoint;
745     NSWindow *topWin = [[[self topmostVimController] windowController] window];
746     NSWindow *win = [windowController window];
748     if (!win) return;
750     // If there is a window belonging to a Vim process, cascade from it,
751     // otherwise use the autosaved window position (if any).
752     if (topWin) {
753         NSRect frame = [topWin frame];
754         topLeft = NSMakePoint(frame.origin.x, NSMaxY(frame));
755     } else {
756         NSString *topLeftString = [[NSUserDefaults standardUserDefaults]
757             stringForKey:MMTopLeftPointKey];
758         if (topLeftString)
759             topLeft = NSPointFromString(topLeftString);
760     }
762     if (!NSEqualPoints(topLeft, NSZeroPoint)) {
763         // Try to tile from the correct screen in case the user has multiple
764         // monitors ([win screen] always seems to return the "main" screen).
765         NSScreen *screen = [self screenContainingPoint:topLeft];
766         if (!screen)
767             screen = [win screen];
769         if (topWin) {
770             // Do manual cascading instead of using
771             // -[MMWindow cascadeTopLeftFromPoint:] since it is rather
772             // unpredictable.
773             topLeft.x += MMCascadeHorizontalOffset;
774             topLeft.y -= MMCascadeVerticalOffset;
775         }
777         if (screen) {
778             // Constrain the window so that it is entirely visible on the
779             // screen.  If it sticks out on the right, move it all the way
780             // left.  If it sticks out on the bottom, move it all the way up.
781             // (Assumption: the cascading offsets are positive.)
782             NSRect screenFrame = [screen frame];
783             NSSize winSize = [win frame].size;
784             NSRect winFrame =
785                 { { topLeft.x, topLeft.y - winSize.height }, winSize };
787             if (NSMaxX(winFrame) > NSMaxX(screenFrame))
788                 topLeft.x = NSMinX(screenFrame);
789             if (NSMinY(winFrame) < NSMinY(screenFrame))
790                 topLeft.y = NSMaxY(screenFrame);
791         } else {
792             ASLogNotice(@"Window not on screen, don't constrain position");
793         }
795         [win setFrameTopLeftPoint:topLeft];
796     }
798     if (1 == [vimControllers count]) {
799         // The first window autosaves its position.  (The autosaving
800         // features of Cocoa are not used because we need more control over
801         // what is autosaved and when it is restored.)
802         [windowController setWindowAutosaveKey:MMTopLeftPointKey];
803     }
805     if (openSelectionString) {
806         // TODO: Pass this as a parameter instead!  Get rid of
807         // 'openSelectionString' etc.
808         //
809         // There is some text to paste into this window as a result of the
810         // services menu "Open selection ..." being used.
811         [[windowController vimController] dropString:openSelectionString];
812         [openSelectionString release];
813         openSelectionString = nil;
814     }
816     if (shouldActivateWhenNextWindowOpens) {
817         [NSApp activateIgnoringOtherApps:YES];
818         shouldActivateWhenNextWindowOpens = NO;
819     }
822 - (void)setMainMenu:(NSMenu *)mainMenu
824     if ([NSApp mainMenu] == mainMenu) return;
826     // If the new menu has a "Recent Files" dummy item, then swap the real item
827     // for the dummy.  We are forced to do this since Cocoa initializes the
828     // "Recent Files" menu and there is no way to simply point Cocoa to a new
829     // item each time the menus are swapped.
830     NSMenu *fileMenu = [mainMenu findFileMenu];
831     if (recentFilesMenuItem && fileMenu) {
832         int dummyIdx =
833                 [fileMenu indexOfItemWithAction:@selector(recentFilesDummy:)];
834         if (dummyIdx >= 0) {
835             NSMenuItem *dummyItem = [[fileMenu itemAtIndex:dummyIdx] retain];
836             [fileMenu removeItemAtIndex:dummyIdx];
838             NSMenu *recentFilesParentMenu = [recentFilesMenuItem menu];
839             int idx = [recentFilesParentMenu indexOfItem:recentFilesMenuItem];
840             if (idx >= 0) {
841                 [[recentFilesMenuItem retain] autorelease];
842                 [recentFilesParentMenu removeItemAtIndex:idx];
843                 [recentFilesParentMenu insertItem:dummyItem atIndex:idx];
844             }
846             [fileMenu insertItem:recentFilesMenuItem atIndex:dummyIdx];
847             [dummyItem release];
848         }
849     }
851     // Now set the new menu.  Notice that we keep one menu for each editor
852     // window since each editor can have its own set of menus.  When swapping
853     // menus we have to tell Cocoa where the new "MacVim", "Windows", and
854     // "Services" menu are.
855     [NSApp setMainMenu:mainMenu];
857     // Setting the "MacVim" (or "Application") menu ensures that it is typeset
858     // in boldface.  (The setAppleMenu: method used to be public but is now
859     // private so this will have to be considered a bit of a hack!)
860     NSMenu *appMenu = [mainMenu findApplicationMenu];
861     [NSApp performSelector:@selector(setAppleMenu:) withObject:appMenu];
863     NSMenu *servicesMenu = [mainMenu findServicesMenu];
864     [NSApp setServicesMenu:servicesMenu];
866     NSMenu *windowsMenu = [mainMenu findWindowsMenu];
867     if (windowsMenu) {
868         // Cocoa isn't clever enough to get rid of items it has added to the
869         // "Windows" menu so we have to do it ourselves otherwise there will be
870         // multiple menu items for each window in the "Windows" menu.
871         //   This code assumes that the only items Cocoa add are ones which
872         // send off the action makeKeyAndOrderFront:.  (Cocoa will not add
873         // another separator item if the last item on the "Windows" menu
874         // already is a separator, so we needen't worry about separators.)
875         int i, count = [windowsMenu numberOfItems];
876         for (i = count-1; i >= 0; --i) {
877             NSMenuItem *item = [windowsMenu itemAtIndex:i];
878             if ([item action] == @selector(makeKeyAndOrderFront:))
879                 [windowsMenu removeItem:item];
880         }
881     }
882     [NSApp setWindowsMenu:windowsMenu];
884 #ifdef MM_ENABLE_PLUGINS
885     // Move plugin menu from old to new main menu.
886     [self removePlugInMenu];
887     [self addPlugInMenuToMenu:mainMenu];
888 #endif
891 - (NSArray *)filterOpenFiles:(NSArray *)filenames
893     return [self filterOpenFiles:filenames openFilesDict:nil];
896 - (BOOL)openFiles:(NSArray *)filenames withArguments:(NSDictionary *)args
898     // Opening files works like this:
899     //  a) filter out any already open files
900     //  b) open any remaining files
901     //
902     // A file is opened in an untitled window if there is one (it may be
903     // currently launching, or it may already be visible), otherwise a new
904     // window is opened.
905     //
906     // Each launching Vim process has a dictionary of arguments that are passed
907     // to the process when in checks in (via connectBackend:pid:).  The
908     // arguments for each launching process can be looked up by its PID (in the
909     // pidArguments dictionary).
911     NSMutableDictionary *arguments = (args ? [[args mutableCopy] autorelease]
912                                            : [NSMutableDictionary dictionary]);
914     filenames = normalizeFilenames(filenames);
916     //
917     // a) Filter out any already open files
918     //
919     NSString *firstFile = [filenames objectAtIndex:0];
920     MMVimController *firstController = nil;
921     NSDictionary *openFilesDict = nil;
922     filenames = [self filterOpenFiles:filenames openFilesDict:&openFilesDict];
924     // Pass arguments to vim controllers that had files open.
925     id key;
926     NSEnumerator *e = [openFilesDict keyEnumerator];
928     // (Indicate that we do not wish to open any files at the moment.)
929     [arguments setObject:[NSNumber numberWithBool:YES] forKey:@"dontOpen"];
931     while ((key = [e nextObject])) {
932         NSArray *files = [openFilesDict objectForKey:key];
933         [arguments setObject:files forKey:@"filenames"];
935         MMVimController *vc = [key pointerValue];
936         [vc passArguments:arguments];
938         // If this controller holds the first file, then remember it for later.
939         if ([files containsObject:firstFile])
940             firstController = vc;
941     }
943     // The meaning of "layout" is defined by the WIN_* defines in main.c.
944     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
945     int layout = [ud integerForKey:MMOpenLayoutKey];
946     BOOL splitVert = [ud boolForKey:MMVerticalSplitKey];
947     BOOL openInCurrentWindow = [ud boolForKey:MMOpenInCurrentWindowKey];
949     if (splitVert && MMLayoutHorizontalSplit == layout)
950         layout = MMLayoutVerticalSplit;
951     if (layout < 0 || (layout > MMLayoutTabs && openInCurrentWindow))
952         layout = MMLayoutTabs;
954     if ([filenames count] == 0) {
955         // Raise the window containing the first file that was already open,
956         // and make sure that the tab containing that file is selected.  Only
957         // do this when there are no more files to open, otherwise sometimes
958         // the window with 'firstFile' will be raised, other times it might be
959         // the window that will open with the files in the 'filenames' array.
960         firstFile = [firstFile stringByEscapingSpecialFilenameCharacters];
962         NSString *bufCmd = @"tab sb";
963         switch (layout) {
964             case MMLayoutHorizontalSplit: bufCmd = @"sb"; break;
965             case MMLayoutVerticalSplit:   bufCmd = @"vert sb"; break;
966             case MMLayoutArglist:         bufCmd = @"b"; break;
967         }
969         NSString *input = [NSString stringWithFormat:@"<C-\\><C-N>"
970                 ":let oldswb=&swb|let &swb=\"useopen,usetab\"|"
971                 "%@ %@|let &swb=oldswb|unl oldswb|"
972                 "cal foreground()<CR>", bufCmd, firstFile];
974         [firstController addVimInput:input];
976         return YES;
977     }
979     // Add filenames to "Recent Files" menu, unless they are being edited
980     // remotely (using ODB).
981     if ([arguments objectForKey:@"remoteID"] == nil) {
982         [[NSDocumentController sharedDocumentController]
983                 noteNewRecentFilePaths:filenames];
984     }
986     //
987     // b) Open any remaining files
988     //
990     [arguments setObject:[NSNumber numberWithInt:layout] forKey:@"layout"];
991     [arguments setObject:filenames forKey:@"filenames"];
992     // (Indicate that files should be opened from now on.)
993     [arguments setObject:[NSNumber numberWithBool:NO] forKey:@"dontOpen"];
995     MMVimController *vc;
996     if (openInCurrentWindow && (vc = [self topmostVimController])) {
997         // Open files in an already open window.
998         [[[vc windowController] window] makeKeyAndOrderFront:self];
999         [vc passArguments:arguments];
1000         return YES;
1001     }
1003     BOOL openOk = YES;
1004     int numFiles = [filenames count];
1005     if (MMLayoutWindows == layout && numFiles > 1) {
1006         // Open one file at a time in a new window, but don't open too many at
1007         // once (at most cap+1 windows will open).  If the user has increased
1008         // the preload cache size we'll take that as a hint that more windows
1009         // should be able to open at once.
1010         int cap = [self maxPreloadCacheSize] - 1;
1011         if (cap < 4) cap = 4;
1012         if (cap > numFiles) cap = numFiles;
1014         int i;
1015         for (i = 0; i < cap; ++i) {
1016             NSArray *a = [NSArray arrayWithObject:[filenames objectAtIndex:i]];
1017             [arguments setObject:a forKey:@"filenames"];
1019             // NOTE: We have to copy the args since we'll mutate them in the
1020             // next loop and the below call may retain the arguments while
1021             // waiting for a process to start.
1022             NSDictionary *args = [[arguments copy] autorelease];
1024             openOk = [self openVimControllerWithArguments:args];
1025             if (!openOk) break;
1026         }
1028         // Open remaining files in tabs in a new window.
1029         if (openOk && numFiles > cap) {
1030             NSRange range = { i, numFiles-cap };
1031             NSArray *a = [filenames subarrayWithRange:range];
1032             [arguments setObject:a forKey:@"filenames"];
1033             [arguments setObject:[NSNumber numberWithInt:MMLayoutTabs]
1034                           forKey:@"layout"];
1036             openOk = [self openVimControllerWithArguments:arguments];
1037         }
1038     } else {
1039         // Open all files at once.
1040         openOk = [self openVimControllerWithArguments:arguments];
1041     }
1043     return openOk;
1046 #ifdef MM_ENABLE_PLUGINS
1047 - (void)addItemToPlugInMenu:(NSMenuItem *)item
1049     NSMenu *menu = [plugInMenuItem submenu];
1050     [menu addItem:item];
1051     if ([menu numberOfItems] == 1)
1052         [self addPlugInMenuToMenu:[NSApp mainMenu]];
1055 - (void)removeItemFromPlugInMenu:(NSMenuItem *)item
1057     NSMenu *menu = [plugInMenuItem submenu];
1058     [menu removeItem:item];
1059     if ([menu numberOfItems] == 0)
1060         [self removePlugInMenu];
1062 #endif
1064 - (IBAction)newWindow:(id)sender
1066     ASLogDebug(@"Open new window");
1068     // A cached controller requires no loading times and results in the new
1069     // window popping up instantaneously.  If the cache is empty it may take
1070     // 1-2 seconds to start a new Vim process.
1071     MMVimController *vc = [self takeVimControllerFromCache];
1072     if (vc) {
1073         [[vc backendProxy] acknowledgeConnection];
1074     } else {
1075         [self launchVimProcessWithArguments:nil];
1076     }
1079 - (IBAction)newWindowAndActivate:(id)sender
1081     [self activateWhenNextWindowOpens];
1082     [self newWindow:sender];
1085 - (IBAction)fileOpen:(id)sender
1087     ASLogDebug(@"Show file open panel");
1089     NSString *dir = nil;
1090     BOOL trackPwd = [[NSUserDefaults standardUserDefaults]
1091             boolForKey:MMDialogsTrackPwdKey];
1092     if (trackPwd) {
1093         MMVimController *vc = [self keyVimController];
1094         if (vc) dir = [vc objectForVimStateKey:@"pwd"];
1095     }
1097     NSOpenPanel *panel = [NSOpenPanel openPanel];
1098     [panel setAllowsMultipleSelection:YES];
1099     [panel setAccessoryView:showHiddenFilesView()];
1101     int result = [panel runModalForDirectory:dir file:nil types:nil];
1102     if (NSOKButton == result)
1103         [self application:NSApp openFiles:[panel filenames]];
1106 - (IBAction)selectNextWindow:(id)sender
1108     ASLogDebug(@"Select next window");
1110     unsigned i, count = [vimControllers count];
1111     if (!count) return;
1113     NSWindow *keyWindow = [NSApp keyWindow];
1114     for (i = 0; i < count; ++i) {
1115         MMVimController *vc = [vimControllers objectAtIndex:i];
1116         if ([[[vc windowController] window] isEqual:keyWindow])
1117             break;
1118     }
1120     if (i < count) {
1121         if (++i >= count)
1122             i = 0;
1123         MMVimController *vc = [vimControllers objectAtIndex:i];
1124         [[vc windowController] showWindow:self];
1125     }
1128 - (IBAction)selectPreviousWindow:(id)sender
1130     ASLogDebug(@"Select previous window");
1132     unsigned i, count = [vimControllers count];
1133     if (!count) return;
1135     NSWindow *keyWindow = [NSApp keyWindow];
1136     for (i = 0; i < count; ++i) {
1137         MMVimController *vc = [vimControllers objectAtIndex:i];
1138         if ([[[vc windowController] window] isEqual:keyWindow])
1139             break;
1140     }
1142     if (i < count) {
1143         if (i > 0) {
1144             --i;
1145         } else {
1146             i = count - 1;
1147         }
1148         MMVimController *vc = [vimControllers objectAtIndex:i];
1149         [[vc windowController] showWindow:self];
1150     }
1153 - (IBAction)orderFrontPreferencePanel:(id)sender
1155     ASLogDebug(@"Show preferences panel");
1156     [[MMPreferenceController sharedPrefsWindowController] showWindow:self];
1159 - (IBAction)openWebsite:(id)sender
1161     ASLogDebug(@"Open MacVim website");
1162     [[NSWorkspace sharedWorkspace] openURL:
1163             [NSURL URLWithString:MMWebsiteString]];
1166 - (IBAction)showVimHelp:(id)sender
1168     ASLogDebug(@"Open window with Vim help");
1169     // Open a new window with the help window maximized.
1170     [self launchVimProcessWithArguments:[NSArray arrayWithObjects:
1171             @"-c", @":h gui_mac", @"-c", @":res", nil]];
1174 - (IBAction)zoomAll:(id)sender
1176     ASLogDebug(@"Zoom all windows");
1177     [NSApp makeWindowsPerform:@selector(performZoom:) inOrder:YES];
1180 - (IBAction)atsuiButtonClicked:(id)sender
1182     ASLogDebug(@"Toggle ATSUI renderer");
1183     NSInteger renderer = MMRendererDefault;
1184     BOOL enable = ([sender state] == NSOnState);
1186     if (enable) {
1187 #if MM_ENABLE_ATSUI
1188         renderer = MMRendererATSUI;
1189 #else
1190         renderer = MMRendererCoreText;
1191 #endif
1192     }
1194     // Update the user default MMRenderer and synchronize the change so that
1195     // any new Vim process will pick up on the changed setting.
1196     CFPreferencesSetAppValue(
1197             (CFStringRef)MMRendererKey,
1198             (CFPropertyListRef)[NSNumber numberWithInt:renderer],
1199             kCFPreferencesCurrentApplication);
1200     CFPreferencesAppSynchronize(kCFPreferencesCurrentApplication);
1202     ASLogInfo(@"Use renderer=%d", renderer);
1204     // This action is called when the user clicks the "use ATSUI renderer"
1205     // button in the advanced preferences pane.
1206     [self rebuildPreloadCache];
1209 - (IBAction)loginShellButtonClicked:(id)sender
1211     ASLogDebug(@"Toggle login shell option");
1212     // This action is called when the user clicks the "use login shell" button
1213     // in the advanced preferences pane.
1214     [self rebuildPreloadCache];
1217 - (IBAction)quickstartButtonClicked:(id)sender
1219     ASLogDebug(@"Toggle Quickstart option");
1220     if ([self maxPreloadCacheSize] > 0) {
1221         [self scheduleVimControllerPreloadAfterDelay:1.0];
1222         [self startWatchingVimDir];
1223     } else {
1224         [self cancelVimControllerPreloadRequests];
1225         [self clearPreloadCacheWithCount:-1];
1226         [self stopWatchingVimDir];
1227     }
1230 - (MMVimController *)keyVimController
1232     NSWindow *keyWindow = [NSApp keyWindow];
1233     if (keyWindow) {
1234         unsigned i, count = [vimControllers count];
1235         for (i = 0; i < count; ++i) {
1236             MMVimController *vc = [vimControllers objectAtIndex:i];
1237             if ([[[vc windowController] window] isEqual:keyWindow])
1238                 return vc;
1239         }
1240     }
1242     return nil;
1245 - (unsigned)connectBackend:(byref in id <MMBackendProtocol>)proxy pid:(int)pid
1247     ASLogDebug(@"pid=%d", pid);
1249     [(NSDistantObject*)proxy setProtocolForProxy:@protocol(MMBackendProtocol)];
1251     // NOTE: Allocate the vim controller now but don't add it to the list of
1252     // controllers since this is a distributed object call and as such can
1253     // arrive at unpredictable times (e.g. while iterating the list of vim
1254     // controllers).
1255     // (What if input arrives before the vim controller is added to the list of
1256     // controllers?  This should not be a problem since the input isn't
1257     // processed immediately (see processInput:forIdentifier:).)
1258     // Also, since the app may be multithreaded (e.g. as a result of showing
1259     // the open panel) we have to ensure this call happens on the main thread,
1260     // else there is a race condition that may lead to a crash.
1261     MMVimController *vc = [[MMVimController alloc] initWithBackend:proxy
1262                                                                pid:pid];
1263     [self performSelectorOnMainThread:@selector(addVimController:)
1264                            withObject:vc
1265                         waitUntilDone:NO
1266                                 modes:[NSArray arrayWithObject:
1267                                        NSDefaultRunLoopMode]];
1269     [vc release];
1271     return [vc vimControllerId];
1274 - (oneway void)processInput:(in bycopy NSArray *)queue
1275               forIdentifier:(unsigned)identifier
1277     // NOTE: Input is not handled immediately since this is a distributed
1278     // object call and as such can arrive at unpredictable times.  Instead,
1279     // queue the input and process it when the run loop is updated.
1281     if (!(queue && identifier)) {
1282         ASLogWarn(@"Bad input for identifier=%d", identifier);
1283         return;
1284     }
1286     ASLogDebug(@"QUEUE for identifier=%d: <<< %@>>>", identifier,
1287                debugStringForMessageQueue(queue));
1289     NSNumber *key = [NSNumber numberWithUnsignedInt:identifier];
1290     NSArray *q = [inputQueues objectForKey:key];
1291     if (q) {
1292         q = [q arrayByAddingObjectsFromArray:queue];
1293         [inputQueues setObject:q forKey:key];
1294     } else {
1295         [inputQueues setObject:queue forKey:key];
1296     }
1298     // NOTE: We must use "event tracking mode" as well as "default mode",
1299     // otherwise the input queue will not be processed e.g. during live
1300     // resizing.
1301     // Also, since the app may be multithreaded (e.g. as a result of showing
1302     // the open panel) we have to ensure this call happens on the main thread,
1303     // else there is a race condition that may lead to a crash.
1304     [self performSelectorOnMainThread:@selector(processInputQueues:)
1305                            withObject:nil
1306                         waitUntilDone:NO
1307                                 modes:[NSArray arrayWithObjects:
1308                                        NSDefaultRunLoopMode,
1309                                        NSEventTrackingRunLoopMode, nil]];
1312 - (NSArray *)serverList
1314     NSMutableArray *array = [NSMutableArray array];
1316     unsigned i, count = [vimControllers count];
1317     for (i = 0; i < count; ++i) {
1318         MMVimController *controller = [vimControllers objectAtIndex:i];
1319         if ([controller serverName])
1320             [array addObject:[controller serverName]];
1321     }
1323     return array;
1326 @end // MMAppController
1331 @implementation MMAppController (MMServices)
1333 - (void)openSelection:(NSPasteboard *)pboard userData:(NSString *)userData
1334                 error:(NSString **)error
1336     if (![[pboard types] containsObject:NSStringPboardType]) {
1337         ASLogNotice(@"Pasteboard contains no NSStringPboardType");
1338         return;
1339     }
1341     ASLogInfo(@"Open new window containing current selection");
1343     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
1344     BOOL openInCurrentWindow = [ud boolForKey:MMOpenInCurrentWindowKey];
1345     MMVimController *vc;
1347     if (openInCurrentWindow && (vc = [self topmostVimController])) {
1348         [vc sendMessage:AddNewTabMsgID data:nil];
1349         [vc dropString:[pboard stringForType:NSStringPboardType]];
1350     } else {
1351         // Save the text, open a new window, and paste the text when the next
1352         // window opens.  (If this is called several times in a row, then all
1353         // but the last call may be ignored.)
1354         if (openSelectionString) [openSelectionString release];
1355         openSelectionString = [[pboard stringForType:NSStringPboardType] copy];
1357         [self newWindow:self];
1358     }
1361 - (void)openFile:(NSPasteboard *)pboard userData:(NSString *)userData
1362            error:(NSString **)error
1364     if (![[pboard types] containsObject:NSStringPboardType]) {
1365         ASLogNotice(@"Pasteboard contains no NSStringPboardType");
1366         return;
1367     }
1369     // TODO: Parse multiple filenames and create array with names.
1370     NSString *string = [pboard stringForType:NSStringPboardType];
1371     string = [string stringByTrimmingCharactersInSet:
1372             [NSCharacterSet whitespaceAndNewlineCharacterSet]];
1373     string = [string stringByStandardizingPath];
1375     ASLogInfo(@"Open new window with selected file: %@", string);
1377     NSArray *filenames = [self filterFilesAndNotify:
1378             [NSArray arrayWithObject:string]];
1379     if ([filenames count] == 0)
1380         return;
1382     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
1383     BOOL openInCurrentWindow = [ud boolForKey:MMOpenInCurrentWindowKey];
1384     MMVimController *vc;
1386     if (openInCurrentWindow && (vc = [self topmostVimController])) {
1387         [vc dropFiles:filenames forceOpen:YES];
1388     } else {
1389         [self openFiles:filenames withArguments:nil];
1390     }
1393 - (void)newFileHere:(NSPasteboard *)pboard userData:(NSString *)userData
1394               error:(NSString **)error
1396     if (![[pboard types] containsObject:NSStringPboardType]) {
1397         ASLogNotice(@"Pasteboard contains no NSStringPboardType");
1398         return;
1399     }
1401     NSString *path = [pboard stringForType:NSStringPboardType];
1402     path = [path stringByExpandingTildeInPath];
1404     BOOL dirIndicator;
1405     if (![[NSFileManager defaultManager] fileExistsAtPath:path
1406                                               isDirectory:&dirIndicator]) {
1407         ASLogNotice(@"Invalid path. Cannot open new document at: %@", path);
1408         return;
1409     }
1411     ASLogInfo(@"Open new file at path=%@", path);
1413     if (!dirIndicator)
1414         path = [path stringByDeletingLastPathComponent];
1416     path = [path stringByEscapingSpecialFilenameCharacters];
1418     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
1419     BOOL openInCurrentWindow = [ud boolForKey:MMOpenInCurrentWindowKey];
1420     MMVimController *vc;
1422     if (openInCurrentWindow && (vc = [self topmostVimController])) {
1423         NSString *input = [NSString stringWithFormat:@"<C-\\><C-N>"
1424                 ":tabe|cd %@<CR>", path];
1425         [vc addVimInput:input];
1426     } else {
1427         NSString *input = [NSString stringWithFormat:@":cd %@", path];
1428         [self launchVimProcessWithArguments:[NSArray arrayWithObjects:
1429                                              @"-c", input, nil]];
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
1459     int pid = -1;
1460     NSString *path = [[NSBundle mainBundle] pathForAuxiliaryExecutable:@"Vim"];
1462     if (!path) {
1463         ASLogCrit(@"Vim executable could not be found inside app bundle!");
1464         return -1;
1465     }
1467     NSArray *taskArgs = [NSArray arrayWithObjects:@"-g", @"-f", nil];
1468     if (args)
1469         taskArgs = [taskArgs arrayByAddingObjectsFromArray:args];
1471     BOOL useLoginShell = [[NSUserDefaults standardUserDefaults]
1472             boolForKey:MMLoginShellKey];
1473     if (useLoginShell) {
1474         // Run process with a login shell, roughly:
1475         //   echo "exec Vim -g -f args" | ARGV0=-`basename $SHELL` $SHELL [-l]
1476         pid = [self executeInLoginShell:path arguments:taskArgs];
1477     } else {
1478         // Run process directly:
1479         //   Vim -g -f args
1480         NSTask *task = [NSTask launchedTaskWithLaunchPath:path
1481                                                 arguments:taskArgs];
1482         pid = task ? [task processIdentifier] : -1;
1483     }
1485     if (-1 != pid) {
1486         // The 'pidArguments' dictionary keeps arguments to be passed to the
1487         // process when it connects (this is in contrast to arguments which are
1488         // passed on the command line, like '-f' and '-g').
1489         // If this method is called with nil arguments we take this as a hint
1490         // that this is an "untitled window" being launched and add a null
1491         // object to the 'pidArguments' dictionary.  This way we can detect if
1492         // an untitled window is being launched by looking for null objects in
1493         // this dictionary.
1494         // If this method is called with non-nil arguments then it is assumed
1495         // that the caller takes care of adding items to 'pidArguments' as
1496         // necessary (only some arguments are passed on connect, e.g. files to
1497         // open).
1498         if (!args)
1499             [pidArguments setObject:[NSNull null]
1500                              forKey:[NSNumber numberWithInt:pid]];
1501     } else {
1502         ASLogWarn(@"Failed to launch Vim process: args=%@, useLoginShell=%d",
1503                   args, useLoginShell);
1504     }
1506     return pid;
1509 - (NSArray *)filterFilesAndNotify:(NSArray *)filenames
1511     // Go trough 'filenames' array and make sure each file exists.  Present
1512     // warning dialog if some file was missing.
1514     NSString *firstMissingFile = nil;
1515     NSMutableArray *files = [NSMutableArray array];
1516     unsigned i, count = [filenames count];
1518     for (i = 0; i < count; ++i) {
1519         NSString *name = [filenames objectAtIndex:i];
1520         if ([[NSFileManager defaultManager] fileExistsAtPath:name]) {
1521             [files addObject:name];
1522         } else if (!firstMissingFile) {
1523             firstMissingFile = name;
1524         }
1525     }
1527     if (firstMissingFile) {
1528         NSAlert *alert = [[NSAlert alloc] init];
1529         [alert addButtonWithTitle:NSLocalizedString(@"OK",
1530                 @"Dialog button")];
1532         NSString *text;
1533         if ([files count] >= count-1) {
1534             [alert setMessageText:NSLocalizedString(@"File not found",
1535                     @"File not found dialog, title")];
1536             text = [NSString stringWithFormat:NSLocalizedString(
1537                     @"Could not open file with name %@.",
1538                     @"File not found dialog, text"), firstMissingFile];
1539         } else {
1540             [alert setMessageText:NSLocalizedString(@"Multiple files not found",
1541                     @"File not found dialog, title")];
1542             text = [NSString stringWithFormat:NSLocalizedString(
1543                     @"Could not open file with name %@, and %d other files.",
1544                     @"File not found dialog, text"),
1545                 firstMissingFile, count-[files count]-1];
1546         }
1548         [alert setInformativeText:text];
1549         [alert setAlertStyle:NSWarningAlertStyle];
1551         [alert runModal];
1552         [alert release];
1554         [NSApp replyToOpenOrPrint:NSApplicationDelegateReplyFailure];
1555     }
1557     return files;
1560 - (NSArray *)filterOpenFiles:(NSArray *)filenames
1561                openFilesDict:(NSDictionary **)openFiles
1563     // Filter out any files in the 'filenames' array that are open and return
1564     // all files that are not already open.  On return, the 'openFiles'
1565     // parameter (if non-nil) will point to a dictionary of open files, indexed
1566     // by Vim controller.
1568     NSMutableDictionary *dict = [NSMutableDictionary dictionary];
1569     NSMutableArray *files = [filenames mutableCopy];
1571     // TODO: Escape special characters in 'files'?
1572     NSString *expr = [NSString stringWithFormat:
1573             @"map([\"%@\"],\"bufloaded(v:val)\")",
1574             [files componentsJoinedByString:@"\",\""]];
1576     unsigned i, count = [vimControllers count];
1577     for (i = 0; i < count && [files count] > 0; ++i) {
1578         MMVimController *vc = [vimControllers objectAtIndex:i];
1580         // Query Vim for which files in the 'files' array are open.
1581         NSString *eval = [vc evaluateVimExpression:expr];
1582         if (!eval) continue;
1584         NSIndexSet *idxSet = [NSIndexSet indexSetWithVimList:eval];
1585         if ([idxSet count] > 0) {
1586             [dict setObject:[files objectsAtIndexes:idxSet]
1587                      forKey:[NSValue valueWithPointer:vc]];
1589             // Remove all the files that were open in this Vim process and
1590             // create a new expression to evaluate.
1591             [files removeObjectsAtIndexes:idxSet];
1592             expr = [NSString stringWithFormat:
1593                     @"map([\"%@\"],\"bufloaded(v:val)\")",
1594                     [files componentsJoinedByString:@"\",\""]];
1595         }
1596     }
1598     if (openFiles != nil)
1599         *openFiles = dict;
1601     return [files autorelease];
1604 #if MM_HANDLE_XCODE_MOD_EVENT
1605 - (void)handleXcodeModEvent:(NSAppleEventDescriptor *)event
1606                  replyEvent:(NSAppleEventDescriptor *)reply
1608 #if 0
1609     // Xcode sends this event to query MacVim which open files have been
1610     // modified.
1611     ASLogDebug(@"reply:%@", reply);
1612     ASLogDebug(@"event:%@", event);
1614     NSEnumerator *e = [vimControllers objectEnumerator];
1615     id vc;
1616     while ((vc = [e nextObject])) {
1617         DescType type = [reply descriptorType];
1618         unsigned len = [[type data] length];
1619         NSMutableData *data = [NSMutableData data];
1621         [data appendBytes:&type length:sizeof(DescType)];
1622         [data appendBytes:&len length:sizeof(unsigned)];
1623         [data appendBytes:[reply data] length:len];
1625         [vc sendMessage:XcodeModMsgID data:data];
1626     }
1627 #endif
1629 #endif
1631 - (void)handleGetURLEvent:(NSAppleEventDescriptor *)event
1632                replyEvent:(NSAppleEventDescriptor *)reply
1634     NSString *urlString = [[event paramDescriptorForKeyword:keyDirectObject]
1635         stringValue];
1636     NSURL *url = [NSURL URLWithString:urlString];
1638     // We try to be compatible with TextMate's URL scheme here, as documented
1639     // at http://blog.macromates.com/2007/the-textmate-url-scheme/ . Currently,
1640     // this means that:
1641     //
1642     // The format is: mvim://open?<arguments> where arguments can be:
1643     //
1644     // * url â€” the actual file to open (i.e. a file://… URL), if you leave
1645     //         out this argument, the frontmost document is implied.
1646     // * line â€” line number to go to (one based).
1647     // * column â€” column number to go to (one based).
1648     //
1649     // Example: mvim://open?url=file:///etc/profile&line=20
1651     if ([[url host] isEqualToString:@"open"]) {
1652         NSMutableDictionary *dict = [NSMutableDictionary dictionary];
1654         // Parse query ("url=file://...&line=14") into a dictionary
1655         NSArray *queries = [[url query] componentsSeparatedByString:@"&"];
1656         NSEnumerator *enumerator = [queries objectEnumerator];
1657         NSString *param;
1658         while ((param = [enumerator nextObject])) {
1659             NSArray *arr = [param componentsSeparatedByString:@"="];
1660             if ([arr count] == 2) {
1661                 [dict setValue:[[arr lastObject]
1662                             stringByReplacingPercentEscapesUsingEncoding:
1663                                 NSUTF8StringEncoding]
1664                         forKey:[[arr objectAtIndex:0]
1665                             stringByReplacingPercentEscapesUsingEncoding:
1666                                 NSUTF8StringEncoding]];
1667             }
1668         }
1670         // Actually open the file.
1671         NSString *file = [dict objectForKey:@"url"];
1672         if (file != nil) {
1673             NSURL *fileUrl= [NSURL URLWithString:file];
1674             // TextMate only opens files that already exist.
1675             if ([fileUrl isFileURL]
1676                     && [[NSFileManager defaultManager] fileExistsAtPath:
1677                            [fileUrl path]]) {
1678                 // Strip 'file://' path, else application:openFiles: might think
1679                 // the file is not yet open.
1680                 NSArray *filenames = [NSArray arrayWithObject:[fileUrl path]];
1682                 // Look for the line and column options.
1683                 NSDictionary *args = nil;
1684                 NSString *line = [dict objectForKey:@"line"];
1685                 if (line) {
1686                     NSString *column = [dict objectForKey:@"column"];
1687                     if (column)
1688                         args = [NSDictionary dictionaryWithObjectsAndKeys:
1689                                 line, @"cursorLine",
1690                                 column, @"cursorColumn",
1691                                 nil];
1692                     else
1693                         args = [NSDictionary dictionaryWithObject:line
1694                                 forKey:@"cursorLine"];
1695                 }
1697                 [self openFiles:filenames withArguments:args];
1698             }
1699         }
1700     } else {
1701         NSAlert *alert = [[NSAlert alloc] init];
1702         [alert addButtonWithTitle:NSLocalizedString(@"OK",
1703             @"Dialog button")];
1705         [alert setMessageText:NSLocalizedString(@"Unknown URL Scheme",
1706             @"Unknown URL Scheme dialog, title")];
1707         [alert setInformativeText:[NSString stringWithFormat:NSLocalizedString(
1708             @"This version of MacVim does not support \"%@\""
1709             @" in its URL scheme.",
1710             @"Unknown URL Scheme dialog, text"),
1711             [url host]]];
1713         [alert setAlertStyle:NSWarningAlertStyle];
1714         [alert runModal];
1715         [alert release];
1716     }
1720 - (int)findLaunchingProcessWithoutArguments
1722     NSArray *keys = [pidArguments allKeysForObject:[NSNull null]];
1723     if ([keys count] > 0)
1724         return [[keys objectAtIndex:0] intValue];
1726     return -1;
1729 - (MMVimController *)findUnusedEditor
1731     NSEnumerator *e = [vimControllers objectEnumerator];
1732     id vc;
1733     while ((vc = [e nextObject])) {
1734         if ([[vc objectForVimStateKey:@"unusedEditor"] boolValue])
1735             return vc;
1736     }
1738     return nil;
1741 - (NSMutableDictionary *)extractArgumentsFromOdocEvent:
1742     (NSAppleEventDescriptor *)desc
1744     NSMutableDictionary *dict = [NSMutableDictionary dictionary];
1746     // 1. Extract ODB parameters (if any)
1747     NSAppleEventDescriptor *odbdesc = desc;
1748     if (![odbdesc paramDescriptorForKeyword:keyFileSender]) {
1749         // The ODB paramaters may hide inside the 'keyAEPropData' descriptor.
1750         odbdesc = [odbdesc paramDescriptorForKeyword:keyAEPropData];
1751         if (![odbdesc paramDescriptorForKeyword:keyFileSender])
1752             odbdesc = nil;
1753     }
1755     if (odbdesc) {
1756         NSAppleEventDescriptor *p =
1757                 [odbdesc paramDescriptorForKeyword:keyFileSender];
1758         if (p)
1759             [dict setObject:[NSNumber numberWithUnsignedInt:[p typeCodeValue]]
1760                      forKey:@"remoteID"];
1762         p = [odbdesc paramDescriptorForKeyword:keyFileCustomPath];
1763         if (p)
1764             [dict setObject:[p stringValue] forKey:@"remotePath"];
1766         p = [odbdesc paramDescriptorForKeyword:keyFileSenderToken];
1767         if (p) {
1768             [dict setObject:[NSNumber numberWithUnsignedLong:[p descriptorType]]
1769                      forKey:@"remoteTokenDescType"];
1770             [dict setObject:[p data] forKey:@"remoteTokenData"];
1771         }
1772     }
1774     // 2. Extract Xcode parameters (if any)
1775     NSAppleEventDescriptor *xcodedesc =
1776             [desc paramDescriptorForKeyword:keyAEPosition];
1777     if (xcodedesc) {
1778         NSRange range;
1779         NSData *data = [xcodedesc data];
1780         NSUInteger length = [data length];
1782         if (length == sizeof(MMXcodeSelectionRange)) {
1783             MMXcodeSelectionRange *sr = (MMXcodeSelectionRange*)[data bytes];
1784             ASLogDebug(@"Xcode selection range (%d,%d,%d,%d,%d,%d)",
1785                     sr->unused1, sr->lineNum, sr->startRange, sr->endRange,
1786                     sr->unused2, sr->theDate);
1788             if (sr->lineNum < 0) {
1789                 // Should select a range of lines.
1790                 range.location = sr->startRange + 1;
1791                 range.length = sr->endRange - sr->startRange + 1;
1792             } else {
1793                 // Should only move cursor to a line.
1794                 range.location = sr->lineNum + 1;
1795                 range.length = 0;
1796             }
1798             [dict setObject:NSStringFromRange(range) forKey:@"selectionRange"];
1799         } else {
1800             ASLogErr(@"Xcode selection range size mismatch! got=%d expected=%d",
1801                     length, sizeof(MMXcodeSelectionRange));
1802         }
1803     }
1805     // 3. Extract Spotlight search text (if any)
1806     NSAppleEventDescriptor *spotlightdesc = 
1807             [desc paramDescriptorForKeyword:keyAESearchText];
1808     if (spotlightdesc)
1809         [dict setObject:[spotlightdesc stringValue] forKey:@"searchText"];
1811     return dict;
1814 #ifdef MM_ENABLE_PLUGINS
1815 - (void)removePlugInMenu
1817     if ([plugInMenuItem menu])
1818         [[plugInMenuItem menu] removeItem:plugInMenuItem];
1821 - (void)addPlugInMenuToMenu:(NSMenu *)mainMenu
1823     NSMenu *windowsMenu = [mainMenu findWindowsMenu];
1825     if ([[plugInMenuItem submenu] numberOfItems] > 0) {
1826         int idx = windowsMenu ? [mainMenu indexOfItemWithSubmenu:windowsMenu]
1827                               : -1;
1828         if (idx > 0) {
1829             [mainMenu insertItem:plugInMenuItem atIndex:idx];
1830         } else {
1831             [mainMenu addItem:plugInMenuItem];
1832         }
1833     }
1835 #endif
1837 - (void)scheduleVimControllerPreloadAfterDelay:(NSTimeInterval)delay
1839     [self performSelector:@selector(preloadVimController:)
1840                withObject:nil
1841                afterDelay:delay];
1844 - (void)cancelVimControllerPreloadRequests
1846     [NSObject cancelPreviousPerformRequestsWithTarget:self
1847             selector:@selector(preloadVimController:)
1848               object:nil];
1851 - (void)preloadVimController:(id)sender
1853     // We only allow preloading of one Vim process at a time (to avoid hogging
1854     // CPU), so schedule another preload in a little while if necessary.
1855     if (-1 != preloadPid) {
1856         [self scheduleVimControllerPreloadAfterDelay:2];
1857         return;
1858     }
1860     if ([cachedVimControllers count] >= [self maxPreloadCacheSize])
1861         return;
1863     preloadPid = [self launchVimProcessWithArguments:
1864             [NSArray arrayWithObject:@"--mmwaitforack"]];
1867 - (int)maxPreloadCacheSize
1869     // The maximum number of Vim processes to keep in the cache can be
1870     // controlled via the user default "MMPreloadCacheSize".
1871     int maxCacheSize = [[NSUserDefaults standardUserDefaults]
1872             integerForKey:MMPreloadCacheSizeKey];
1873     if (maxCacheSize < 0) maxCacheSize = 0;
1874     else if (maxCacheSize > 10) maxCacheSize = 10;
1876     return maxCacheSize;
1879 - (MMVimController *)takeVimControllerFromCache
1881     // NOTE: After calling this message the backend corresponding to the
1882     // returned vim controller must be sent an acknowledgeConnection message,
1883     // else the vim process will be stuck.
1884     //
1885     // This method may return nil even though the cache might be non-empty; the
1886     // caller should handle this by starting a new Vim process.
1888     int i, count = [cachedVimControllers count];
1889     if (0 == count) return nil;
1891     // Locate the first Vim controller with up-to-date rc-files sourced.
1892     NSDate *rcDate = [self rcFilesModificationDate];
1893     for (i = 0; i < count; ++i) {
1894         MMVimController *vc = [cachedVimControllers objectAtIndex:i];
1895         NSDate *date = [vc creationDate];
1896         if ([date compare:rcDate] != NSOrderedAscending)
1897             break;
1898     }
1900     if (i > 0) {
1901         // Clear out cache entries whose vimrc/gvimrc files were sourced before
1902         // the latest modification date for those files.  This ensures that the
1903         // latest rc-files are always sourced for new windows.
1904         [self clearPreloadCacheWithCount:i];
1905     }
1907     if ([cachedVimControllers count] == 0) {
1908         [self scheduleVimControllerPreloadAfterDelay:2.0];
1909         return nil;
1910     }
1912     MMVimController *vc = [cachedVimControllers objectAtIndex:0];
1913     [vimControllers addObject:vc];
1914     [cachedVimControllers removeObjectAtIndex:0];
1915     [vc setIsPreloading:NO];
1917     // If the Vim process has finished loading then the window will displayed
1918     // now, otherwise it will be displayed when the OpenWindowMsgID message is
1919     // received.
1920     [[vc windowController] showWindow];
1922     // Since we've taken one controller from the cache we take the opportunity
1923     // to preload another.
1924     [self scheduleVimControllerPreloadAfterDelay:1];
1926     return vc;
1929 - (void)clearPreloadCacheWithCount:(int)count
1931     // Remove the 'count' first entries in the preload cache.  It is assumed
1932     // that objects are added/removed from the cache in a FIFO manner so that
1933     // this effectively clears the 'count' oldest entries.
1934     // If 'count' is negative, then the entire cache is cleared.
1936     if ([cachedVimControllers count] == 0 || count == 0)
1937         return;
1939     if (count < 0)
1940         count = [cachedVimControllers count];
1942     // Make sure the preloaded Vim processes get killed or they'll just hang
1943     // around being useless until MacVim is terminated.
1944     NSEnumerator *e = [cachedVimControllers objectEnumerator];
1945     MMVimController *vc;
1946     int n = count;
1947     while ((vc = [e nextObject]) && n-- > 0) {
1948         [[NSNotificationCenter defaultCenter] removeObserver:vc];
1949         [vc sendMessage:TerminateNowMsgID data:nil];
1951         // Since the preloaded processes were killed "prematurely" we have to
1952         // manually tell them to cleanup (it is not enough to simply release
1953         // them since deallocation and cleanup are separated).
1954         [vc cleanup];
1955     }
1957     n = count;
1958     while (n-- > 0 && [cachedVimControllers count] > 0)
1959         [cachedVimControllers removeObjectAtIndex:0];
1961     // There is a small delay before the Vim process actually exits so wait a
1962     // little before trying to reap the child process.  If the process still
1963     // hasn't exited after this wait it won't be reaped until the next time
1964     // reapChildProcesses: is called (but this should be harmless).
1965     [self performSelector:@selector(reapChildProcesses:)
1966                withObject:nil
1967                afterDelay:0.1];
1970 - (void)rebuildPreloadCache
1972     if ([self maxPreloadCacheSize] > 0) {
1973         [self clearPreloadCacheWithCount:-1];
1974         [self cancelVimControllerPreloadRequests];
1975         [self scheduleVimControllerPreloadAfterDelay:1.0];
1976     }
1980 // HACK: fileAttributesAtPath was deprecated in 10.5
1981 #if (MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5)
1982 #define MM_fileAttributes(fm,p) [fm attributesOfItemAtPath:p error:NULL]
1983 #else
1984 #define MM_fileAttributes(fm,p) [fm fileAttributesAtPath:p traverseLink:YES]
1985 #endif
1986 - (NSDate *)rcFilesModificationDate
1988     // Check modification dates for ~/.vimrc and ~/.gvimrc and return the
1989     // latest modification date.  If ~/.vimrc does not exist, check ~/_vimrc
1990     // and similarly for gvimrc.
1991     // Returns distantPath if no rc files were found.
1993     NSDate *date = [NSDate distantPast];
1994     NSFileManager *fm = [NSFileManager defaultManager];
1996     NSString *path = [@"~/.vimrc" stringByExpandingTildeInPath];
1997     NSDictionary *attr = MM_fileAttributes(fm, path);
1998     if (!attr) {
1999         path = [@"~/_vimrc" stringByExpandingTildeInPath];
2000         attr = MM_fileAttributes(fm, path);
2001     }
2002     NSDate *modDate = [attr objectForKey:NSFileModificationDate];
2003     if (modDate)
2004         date = modDate;
2006     path = [@"~/.gvimrc" stringByExpandingTildeInPath];
2007     attr = MM_fileAttributes(fm, path);
2008     if (!attr) {
2009         path = [@"~/_gvimrc" stringByExpandingTildeInPath];
2010         attr = MM_fileAttributes(fm, path);
2011     }
2012     modDate = [attr objectForKey:NSFileModificationDate];
2013     if (modDate)
2014         date = [date laterDate:modDate];
2016     return date;
2018 #undef MM_fileAttributes
2020 - (BOOL)openVimControllerWithArguments:(NSDictionary *)arguments
2022     MMVimController *vc = [self findUnusedEditor];
2023     if (vc) {
2024         // Open files in an already open window.
2025         [[[vc windowController] window] makeKeyAndOrderFront:self];
2026         [vc passArguments:arguments];
2027     } else if ((vc = [self takeVimControllerFromCache])) {
2028         // Open files in a new window using a cached vim controller.  This
2029         // requires virtually no loading time so the new window will pop up
2030         // instantaneously.
2031         [vc passArguments:arguments];
2032         [[vc backendProxy] acknowledgeConnection];
2033     } else {
2034         // Open files in a launching Vim process or start a new process.  This
2035         // may take 1-2 seconds so there will be a visible delay before the
2036         // window appears on screen.
2037         int pid = [self findLaunchingProcessWithoutArguments];
2038         if (-1 == pid) {
2039             pid = [self launchVimProcessWithArguments:nil];
2040             if (-1 == pid)
2041                 return NO;
2042         }
2044         // TODO: If the Vim process fails to start, or if it changes PID,
2045         // then the memory allocated for these parameters will leak.
2046         // Ensure that this cannot happen or somehow detect it.
2048         if ([arguments count] > 0)
2049             [pidArguments setObject:arguments
2050                              forKey:[NSNumber numberWithInt:pid]];
2051     }
2053     return YES;
2056 - (void)activateWhenNextWindowOpens
2058     ASLogDebug(@"Activate MacVim when next window opens");
2059     shouldActivateWhenNextWindowOpens = YES;
2062 - (void)startWatchingVimDir
2064 #if (MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5)
2065     if (fsEventStream)
2066         return;
2067     if (NULL == FSEventStreamStart)
2068         return; // FSEvent functions are weakly linked
2070     NSString *path = [@"~/.vim" stringByExpandingTildeInPath];
2071     NSArray *pathsToWatch = [NSArray arrayWithObject:path];
2073     fsEventStream = FSEventStreamCreate(NULL, &fsEventCallback, NULL,
2074             (CFArrayRef)pathsToWatch, kFSEventStreamEventIdSinceNow,
2075             MMEventStreamLatency, kFSEventStreamCreateFlagNone);
2077     FSEventStreamScheduleWithRunLoop(fsEventStream,
2078             [[NSRunLoop currentRunLoop] getCFRunLoop],
2079             kCFRunLoopDefaultMode);
2081     FSEventStreamStart(fsEventStream);
2082     ASLogDebug(@"Started FS event stream");
2083 #endif
2086 - (void)stopWatchingVimDir
2088 #if (MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5)
2089     if (NULL == FSEventStreamStop)
2090         return; // FSEvent functions are weakly linked
2092     if (fsEventStream) {
2093         FSEventStreamStop(fsEventStream);
2094         FSEventStreamInvalidate(fsEventStream);
2095         FSEventStreamRelease(fsEventStream);
2096         fsEventStream = NULL;
2097         ASLogDebug(@"Stopped FS event stream");
2098     }
2099 #endif
2103 - (void)handleFSEvent
2105     [self clearPreloadCacheWithCount:-1];
2107     // Several FS events may arrive in quick succession so make sure to cancel
2108     // any previous preload requests before making a new one.
2109     [self cancelVimControllerPreloadRequests];
2110     [self scheduleVimControllerPreloadAfterDelay:0.5];
2113 - (void)loadDefaultFont
2115     // It is possible to set a user default to avoid loading the default font
2116     // (this cuts down on startup time).
2117     if (![[NSUserDefaults standardUserDefaults] boolForKey:MMLoadDefaultFontKey]
2118             || fontContainerRef) {
2119         ASLogInfo(@"Skip loading of the default font...");
2120         return;
2121     }
2123     ASLogInfo(@"Loading the default font...");
2125     // Load all fonts in the Resouces folder of the app bundle.
2126     NSString *fontsFolder = [[NSBundle mainBundle] resourcePath];
2127     if (fontsFolder) {
2128         NSURL *fontsURL = [NSURL fileURLWithPath:fontsFolder];
2129         if (fontsURL) {
2130             FSRef fsRef;
2131             CFURLGetFSRef((CFURLRef)fontsURL, &fsRef);
2133 #if (MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5)
2134             // This is the font activation API for OS X 10.5.  Only compile
2135             // this code if we're building on OS X 10.5 or later.
2136             if (NULL != ATSFontActivateFromFileReference) { // Weakly linked
2137                 ATSFontActivateFromFileReference(&fsRef, kATSFontContextLocal,
2138                                                  kATSFontFormatUnspecified,
2139                                                  NULL, kATSOptionFlagsDefault,
2140                                                  &fontContainerRef);
2141             }
2142 #endif
2143 #if (MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_5)
2144             // The following font activation API was deprecated in OS X 10.5.
2145             // Don't compile this code unless we're targeting OS X 10.4.
2146             FSSpec fsSpec;
2147             if (fontContainerRef == 0 &&
2148                     FSGetCatalogInfo(&fsRef, kFSCatInfoNone, NULL, NULL,
2149                                      &fsSpec, NULL) == noErr) {
2150                 ATSFontActivateFromFileSpecification(&fsSpec,
2151                         kATSFontContextLocal, kATSFontFormatUnspecified, NULL,
2152                         kATSOptionFlagsDefault, &fontContainerRef);
2153             }
2154 #endif
2155         }
2156     }
2158     if (!fontContainerRef) {
2159         ASLogNotice(@"Failed to activate the default font (the app bundle "
2160                     "may be incomplete)");
2161     }
2164 - (int)executeInLoginShell:(NSString *)path arguments:(NSArray *)args
2166     // Start a login shell and execute the command 'path' with arguments 'args'
2167     // in the shell.  This ensures that user environment variables are set even
2168     // when MacVim was started from the Finder.
2170     int pid = -1;
2171     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
2173     // Determine which shell to use to execute the command.  The user
2174     // may decide which shell to use by setting a user default or the
2175     // $SHELL environment variable.
2176     NSString *shell = [ud stringForKey:MMLoginShellCommandKey];
2177     if (!shell || [shell length] == 0)
2178         shell = [[[NSProcessInfo processInfo] environment]
2179             objectForKey:@"SHELL"];
2180     if (!shell)
2181         shell = @"/bin/bash";
2183     // Bash needs the '-l' flag to launch a login shell.  The user may add
2184     // flags by setting a user default.
2185     NSString *shellArgument = [ud stringForKey:MMLoginShellArgumentKey];
2186     if (!shellArgument || [shellArgument length] == 0) {
2187         if ([[shell lastPathComponent] isEqual:@"bash"])
2188             shellArgument = @"-l";
2189         else
2190             shellArgument = nil;
2191     }
2193     // Build input string to pipe to the login shell.
2194     NSMutableString *input = [NSMutableString stringWithFormat:
2195             @"exec \"%@\"", path];
2196     if (args) {
2197         // Append all arguments, making sure they are properly quoted, even
2198         // when they contain single quotes.
2199         NSEnumerator *e = [args objectEnumerator];
2200         id obj;
2202         while ((obj = [e nextObject])) {
2203             NSMutableString *arg = [NSMutableString stringWithString:obj];
2204             [arg replaceOccurrencesOfString:@"'" withString:@"'\"'\"'"
2205                                     options:NSLiteralSearch
2206                                       range:NSMakeRange(0, [arg length])];
2207             [input appendFormat:@" '%@'", arg];
2208         }
2209     }
2211     // Build the argument vector used to start the login shell.
2212     NSString *shellArg0 = [NSString stringWithFormat:@"-%@",
2213              [shell lastPathComponent]];
2214     char *shellArgv[3] = { (char *)[shellArg0 UTF8String], NULL, NULL };
2215     if (shellArgument)
2216         shellArgv[1] = (char *)[shellArgument UTF8String];
2218     // Get the C string representation of the shell path before the fork since
2219     // we must not call Foundation functions after a fork.
2220     const char *shellPath = [shell fileSystemRepresentation];
2222     // Fork and execute the process.
2223     int ds[2];
2224     if (pipe(ds)) return -1;
2226     pid = fork();
2227     if (pid == -1) {
2228         return -1;
2229     } else if (pid == 0) {
2230         // Child process
2232         if (close(ds[1]) == -1) exit(255);
2233         if (dup2(ds[0], 0) == -1) exit(255);
2235         // Without the following call warning messages like this appear on the
2236         // console:
2237         //     com.apple.launchd[69] : Stray process with PGID equal to this
2238         //                             dead job: PID 1589 PPID 1 Vim
2239         setsid();
2241         execv(shellPath, shellArgv);
2243         // Never reached unless execv fails
2244         exit(255);
2245     } else {
2246         // Parent process
2247         if (close(ds[0]) == -1) return -1;
2249         // Send input to execute to the child process
2250         [input appendString:@"\n"];
2251         int bytes = [input lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
2253         if (write(ds[1], [input UTF8String], bytes) != bytes) return -1;
2254         if (close(ds[1]) == -1) return -1;
2256         ++numChildProcesses;
2257         ASLogDebug(@"new process pid=%d (count=%d)", pid, numChildProcesses);
2258     }
2260     return pid;
2263 - (void)reapChildProcesses:(id)sender
2265     // NOTE: numChildProcesses (currently) only counts the number of Vim
2266     // processes that have been started with executeInLoginShell::.  If other
2267     // processes are spawned this code may need to be adjusted (or
2268     // numChildProcesses needs to be incremented when such a process is
2269     // started).
2270     while (numChildProcesses > 0) {
2271         int status = 0;
2272         int pid = waitpid(-1, &status, WNOHANG);
2273         if (pid <= 0)
2274             break;
2276         ASLogDebug(@"Wait for pid=%d complete", pid);
2277         --numChildProcesses;
2278     }
2281 - (void)processInputQueues:(id)sender
2283     // NOTE: Because we use distributed objects it is quite possible for this
2284     // function to be re-entered.  This can cause all sorts of unexpected
2285     // problems so we guard against it here so that the rest of the code does
2286     // not need to worry about it.
2288     // The processing flag is > 0 if this function is already on the call
2289     // stack; < 0 if this function was also re-entered.
2290     if (processingFlag != 0) {
2291         ASLogDebug(@"BUSY!");
2292         processingFlag = -1;
2293         return;
2294     }
2296     // NOTE: Be _very_ careful that no exceptions can be raised between here
2297     // and the point at which 'processingFlag' is reset.  Otherwise the above
2298     // test could end up always failing and no input queues would ever be
2299     // processed!
2300     processingFlag = 1;
2302     // NOTE: New input may arrive while we're busy processing; we deal with
2303     // this by putting the current queue aside and creating a new input queue
2304     // for future input.
2305     NSDictionary *queues = inputQueues;
2306     inputQueues = [NSMutableDictionary new];
2308     // Pass each input queue on to the vim controller with matching
2309     // identifier (and note that it could be cached).
2310     NSEnumerator *e = [queues keyEnumerator];
2311     NSNumber *key;
2312     while ((key = [e nextObject])) {
2313         unsigned ukey = [key unsignedIntValue];
2314         int i = 0, count = [vimControllers count];
2315         for (i = 0; i < count; ++i) {
2316             MMVimController *vc = [vimControllers objectAtIndex:i];
2317             if (ukey == [vc vimControllerId]) {
2318                 [vc processInputQueue:[queues objectForKey:key]]; // !exceptions
2319                 break;
2320             }
2321         }
2323         if (i < count) continue;
2325         count = [cachedVimControllers count];
2326         for (i = 0; i < count; ++i) {
2327             MMVimController *vc = [cachedVimControllers objectAtIndex:i];
2328             if (ukey == [vc vimControllerId]) {
2329                 [vc processInputQueue:[queues objectForKey:key]]; // !exceptions
2330                 break;
2331             }
2332         }
2334         if (i == count) {
2335             ASLogWarn(@"No Vim controller for identifier=%d", ukey);
2336         }
2337     }
2339     [queues release];
2341     // If new input arrived while we were processing it would have been
2342     // blocked so we have to schedule it to be processed again.
2343     if (processingFlag < 0)
2344         [self performSelectorOnMainThread:@selector(processInputQueues:)
2345                                withObject:nil
2346                             waitUntilDone:NO
2347                                     modes:[NSArray arrayWithObjects:
2348                                            NSDefaultRunLoopMode,
2349                                            NSEventTrackingRunLoopMode, nil]];
2351     processingFlag = 0;
2354 - (void)addVimController:(MMVimController *)vc
2356     ASLogDebug(@"Add Vim controller pid=%d id=%d",
2357             [vc pid], [vc vimControllerId]);
2359     int pid = [vc pid];
2360     NSNumber *pidKey = [NSNumber numberWithInt:pid];
2362     if (preloadPid == pid) {
2363         // This controller was preloaded, so add it to the cache and
2364         // schedule another vim process to be preloaded.
2365         preloadPid = -1;
2366         [vc setIsPreloading:YES];
2367         [cachedVimControllers addObject:vc];
2368         [self scheduleVimControllerPreloadAfterDelay:1];
2369     } else {
2370         [vimControllers addObject:vc];
2372         id args = [pidArguments objectForKey:pidKey];
2373         if (args && [NSNull null] != args)
2374             [vc passArguments:args];
2376         // HACK!  MacVim does not get activated if it is launched from the
2377         // terminal, so we forcibly activate here unless it is an untitled
2378         // window opening.  Untitled windows are treated differently, else
2379         // MacVim would steal the focus if another app was activated while the
2380         // untitled window was loading.
2381         if (!args || args != [NSNull null])
2382             [self activateWhenNextWindowOpens];
2384         if (args)
2385             [pidArguments removeObjectForKey:pidKey];
2386     }
2389 - (NSScreen *)screenContainingPoint:(NSPoint)pt
2391     NSArray *screens = [NSScreen screens];
2392     NSUInteger i, count = [screens count];
2393     for (i = 0; i < count; ++i) {
2394         NSScreen *screen = [screens objectAtIndex:i];
2395         NSRect frame = [screen frame];
2396         if (NSPointInRect(pt, frame))
2397             return screen;
2398     }
2400     return nil;
2403 @end // MMAppController (Private)