1 /* vi:set ts=8 sts=4 sw=4 ft=objc:
3 * VIM - Vi IMproved by Bram Moolenaar
4 * MacVim GUI port by Bjorn Winckler
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.
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.
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
24 * A Vim process started from the command line connects directly by sending the
25 * connectBackend:pid: message (launchVimProcessWithArguments: is never called
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".
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"
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_4)
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;
72 #pragma options align=mac68k
75 short unused1; // 0 (not used)
76 short lineNum; // line to select (< 0 to specify range)
77 long startRange; // start of selection range (if line < 0)
78 long endRange; // end of selection range (if line < 0)
79 long unused2; // 0 (not used)
80 long theDate; // modification date/time
82 #pragma options align=reset
85 // This is a private AppKit API gleaned from class-dump.
86 @interface NSKeyBindingManager : NSObject
87 + (id)sharedKeyBindingManager;
89 - (void)setDictionary:(id)arg1;
93 @interface MMAppController (MMServices)
94 - (void)openSelection:(NSPasteboard *)pboard userData:(NSString *)userData
95 error:(NSString **)error;
96 - (void)openFile:(NSPasteboard *)pboard userData:(NSString *)userData
97 error:(NSString **)error;
98 - (void)newFileHere:(NSPasteboard *)pboard userData:(NSString *)userData
99 error:(NSString **)error;
103 @interface MMAppController (Private)
104 - (MMVimController *)topmostVimController;
105 - (int)launchVimProcessWithArguments:(NSArray *)args;
106 - (NSArray *)filterFilesAndNotify:(NSArray *)files;
107 - (NSArray *)filterOpenFiles:(NSArray *)filenames
108 openFilesDict:(NSDictionary **)openFiles;
109 #if MM_HANDLE_XCODE_MOD_EVENT
110 - (void)handleXcodeModEvent:(NSAppleEventDescriptor *)event
111 replyEvent:(NSAppleEventDescriptor *)reply;
113 - (void)handleGetURLEvent:(NSAppleEventDescriptor *)event
114 replyEvent:(NSAppleEventDescriptor *)reply;
115 - (int)findLaunchingProcessWithoutArguments;
116 - (MMVimController *)findUnusedEditor;
117 - (NSMutableDictionary *)extractArgumentsFromOdocEvent:
118 (NSAppleEventDescriptor *)desc;
119 - (void)scheduleVimControllerPreloadAfterDelay:(NSTimeInterval)delay;
120 - (void)cancelVimControllerPreloadRequests;
121 - (void)preloadVimController:(id)sender;
122 - (int)maxPreloadCacheSize;
123 - (MMVimController *)takeVimControllerFromCache;
124 - (void)clearPreloadCacheWithCount:(int)count;
125 - (void)rebuildPreloadCache;
126 - (NSDate *)rcFilesModificationDate;
127 - (BOOL)openVimControllerWithArguments:(NSDictionary *)arguments;
128 - (void)activateWhenNextWindowOpens;
129 - (void)startWatchingVimDir;
130 - (void)stopWatchingVimDir;
131 - (void)handleFSEvent;
132 - (void)loadDefaultFont;
133 - (int)executeInLoginShell:(NSString *)path arguments:(NSArray *)args;
134 - (void)reapChildProcesses:(id)sender;
135 - (void)processInputQueues:(id)sender;
136 - (void)addVimController:(MMVimController *)vc;
138 #ifdef MM_ENABLE_PLUGINS
139 - (void)removePlugInMenu;
140 - (void)addPlugInMenuToMenu:(NSMenu *)mainMenu;
146 #if (MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_4)
148 fsEventCallback(ConstFSEventStreamRef streamRef,
149 void *clientCallBackInfo,
152 const FSEventStreamEventFlags eventFlags[],
153 const FSEventStreamEventId eventIds[])
155 [[MMAppController sharedInstance] handleFSEvent];
159 @implementation MMAppController
163 static BOOL initDone = NO;
164 if (initDone) return;
169 // HACK! The following user default must be reset, else Ctrl-q (or
170 // whichever key is specified by the default) will be blocked by the input
171 // manager (interpretKeyEvents: swallows that key). (We can't use
172 // NSUserDefaults since it only allows us to write to the registration
173 // domain and this preference has "higher precedence" than that so such a
174 // change would have no effect.)
175 CFPreferencesSetAppValue(CFSTR("NSQuotedKeystrokeBinding"),
177 kCFPreferencesCurrentApplication);
179 NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:
180 [NSNumber numberWithBool:NO], MMNoWindowKey,
181 [NSNumber numberWithInt:64], MMTabMinWidthKey,
182 [NSNumber numberWithInt:6*64], MMTabMaxWidthKey,
183 [NSNumber numberWithInt:132], MMTabOptimumWidthKey,
184 [NSNumber numberWithBool:YES], MMShowAddTabButtonKey,
185 [NSNumber numberWithInt:2], MMTextInsetLeftKey,
186 [NSNumber numberWithInt:1], MMTextInsetRightKey,
187 [NSNumber numberWithInt:1], MMTextInsetTopKey,
188 [NSNumber numberWithInt:1], MMTextInsetBottomKey,
189 @"MMTypesetter", MMTypesetterKey,
190 [NSNumber numberWithFloat:1], MMCellWidthMultiplierKey,
191 [NSNumber numberWithFloat:-1], MMBaselineOffsetKey,
192 [NSNumber numberWithBool:YES], MMTranslateCtrlClickKey,
193 [NSNumber numberWithInt:0], MMOpenInCurrentWindowKey,
194 [NSNumber numberWithBool:NO], MMNoFontSubstitutionKey,
195 [NSNumber numberWithBool:YES], MMLoginShellKey,
196 [NSNumber numberWithBool:NO], MMAtsuiRendererKey,
197 [NSNumber numberWithInt:MMUntitledWindowAlways],
199 [NSNumber numberWithBool:NO], MMTexturedWindowKey,
200 [NSNumber numberWithBool:NO], MMZoomBothKey,
201 @"", MMLoginShellCommandKey,
202 @"", MMLoginShellArgumentKey,
203 [NSNumber numberWithBool:YES], MMDialogsTrackPwdKey,
204 #ifdef MM_ENABLE_PLUGINS
205 [NSNumber numberWithBool:YES], MMShowLeftPlugInContainerKey,
207 [NSNumber numberWithInt:3], MMOpenLayoutKey,
208 [NSNumber numberWithBool:NO], MMVerticalSplitKey,
209 [NSNumber numberWithInt:0], MMPreloadCacheSizeKey,
210 [NSNumber numberWithInt:0], MMLastWindowClosedBehaviorKey,
211 [NSNumber numberWithBool:YES], MMLoadDefaultFontKey,
212 #ifdef INCLUDE_OLD_IM_CODE
213 [NSNumber numberWithBool:YES], MMUseInlineImKey,
214 #endif // INCLUDE_OLD_IM_CODE
217 [[NSUserDefaults standardUserDefaults] registerDefaults:dict];
219 NSArray *types = [NSArray arrayWithObject:NSStringPboardType];
220 [NSApp registerServicesMenuSendTypes:types returnTypes:types];
222 // NOTE: Set the current directory to user's home directory, otherwise it
223 // will default to the root directory. (This matters since new Vim
224 // processes inherit MacVim's environment variables.)
225 [[NSFileManager defaultManager] changeCurrentDirectoryPath:
231 if (!(self = [super init])) return nil;
233 [self loadDefaultFont];
235 vimControllers = [NSMutableArray new];
236 cachedVimControllers = [NSMutableArray new];
238 pidArguments = [NSMutableDictionary new];
239 inputQueues = [NSMutableDictionary new];
241 #ifdef MM_ENABLE_PLUGINS
242 NSString *plugInTitle = NSLocalizedString(@"Plug-In",
243 @"Plug-In menu title");
244 plugInMenuItem = [[NSMenuItem alloc] initWithTitle:plugInTitle
247 NSMenu *submenu = [[NSMenu alloc] initWithTitle:plugInTitle];
248 [plugInMenuItem setSubmenu:submenu];
252 // NOTE: Do not use the default connection since the Logitech Control
253 // Center (LCC) input manager steals and this would cause MacVim to
254 // never open any windows. (This is a bug in LCC but since they are
255 // unlikely to fix it, we graciously give them the default connection.)
256 connection = [[NSConnection alloc] initWithReceivePort:[NSPort port]
258 [connection setRootObject:self];
259 [connection setRequestTimeout:MMRequestTimeout];
260 [connection setReplyTimeout:MMReplyTimeout];
262 // NOTE! If the name of the connection changes here it must also be
263 // updated in MMBackend.m.
264 NSString *name = [NSString stringWithFormat:@"%@-connection",
265 [[NSBundle mainBundle] bundlePath]];
266 if (![connection registerName:name]) {
267 ASLogCrit(@"Failed to register connection with name '%@'", name);
268 [connection release]; connection = nil;
278 [connection release]; connection = nil;
279 [inputQueues release]; inputQueues = nil;
280 [pidArguments release]; pidArguments = nil;
281 [vimControllers release]; vimControllers = nil;
282 [cachedVimControllers release]; cachedVimControllers = nil;
283 [openSelectionString release]; openSelectionString = nil;
284 [recentFilesMenuItem release]; recentFilesMenuItem = nil;
285 [defaultMainMenu release]; defaultMainMenu = nil;
286 #ifdef MM_ENABLE_PLUGINS
287 [plugInMenuItem release]; plugInMenuItem = nil;
289 [appMenuItemTemplate release]; appMenuItemTemplate = nil;
294 - (void)applicationWillFinishLaunching:(NSNotification *)notification
296 // Remember the default menu so that it can be restored if the user closes
297 // all editor windows.
298 defaultMainMenu = [[NSApp mainMenu] retain];
300 // Store a copy of the default app menu so we can use this as a template
301 // for all other menus. We make a copy here because the "Services" menu
302 // will not yet have been populated at this time. If we don't we get
303 // problems trying to set key equivalents later on because they might clash
304 // with items on the "Services" menu.
305 appMenuItemTemplate = [defaultMainMenu itemAtIndex:0];
306 appMenuItemTemplate = [appMenuItemTemplate copy];
308 // Set up the "Open Recent" menu. See
309 // http://lapcatsoftware.com/blog/2007/07/10/
310 // working-without-a-nib-part-5-open-recent-menu/
312 // http://www.cocoabuilder.com/archive/message/cocoa/2007/8/15/187793
313 // for more information.
315 // The menu itself is created in MainMenu.nib but we still seem to have to
316 // hack around a bit to get it to work. (This has to be done in
317 // applicationWillFinishLaunching at the latest, otherwise it doesn't
319 NSMenu *fileMenu = [defaultMainMenu findFileMenu];
321 int idx = [fileMenu indexOfItemWithAction:@selector(fileOpen:)];
322 if (idx >= 0 && idx+1 < [fileMenu numberOfItems])
324 recentFilesMenuItem = [fileMenu itemWithTitle:@"Open Recent"];
325 [[recentFilesMenuItem submenu] performSelector:@selector(_setMenuName:)
326 withObject:@"NSRecentDocumentsMenu"];
328 // Note: The "Recent Files" menu must be moved around since there is no
329 // -[NSApp setRecentFilesMenu:] method. We keep a reference to it to
330 // facilitate this move (see setMainMenu: below).
331 [recentFilesMenuItem retain];
334 #if MM_HANDLE_XCODE_MOD_EVENT
335 [[NSAppleEventManager sharedAppleEventManager]
337 andSelector:@selector(handleXcodeModEvent:replyEvent:)
342 // Register 'mvim://' URL handler
343 [[NSAppleEventManager sharedAppleEventManager]
345 andSelector:@selector(handleGetURLEvent:replyEvent:)
346 forEventClass:kInternetEventClass
347 andEventID:kAEGetURL];
349 // Disable the default Cocoa "Key Bindings" since they interfere with the
350 // way Vim handles keyboard input. Cocoa reads bindings from
351 // /System/Library/Frameworks/AppKit.framework/Resources/
352 // StandardKeyBinding.dict
354 // ~/Library/KeyBindings/DefaultKeyBinding.dict
355 // To avoid having the user accidentally break keyboard handling (by
356 // modifying the latter in some unexpected way) in MacVim we load our own
357 // key binding dictionary from Resource/KeyBinding.plist. We can't disable
358 // the bindings completely since it would break keyboard handling in
359 // dialogs so the our custom dictionary contains all the entries from the
362 // It is possible to disable key bindings completely by not calling
363 // interpretKeyEvents: in keyDown: but this also disables key bindings used
364 // by certain input methods. E.g. Ctrl-Shift-; would no longer work in
365 // the Kotoeri input manager.
367 // To solve this problem we access a private API and set the key binding
368 // dictionary to our own custom dictionary here. At this time Cocoa will
369 // have already read the above mentioned dictionaries so it (hopefully)
370 // won't try to change the key binding dictionary again after this point.
371 NSKeyBindingManager *mgr = [NSKeyBindingManager sharedKeyBindingManager];
372 NSBundle *mainBundle = [NSBundle mainBundle];
373 NSString *path = [mainBundle pathForResource:@"KeyBinding"
375 NSDictionary *dict = [NSDictionary dictionaryWithContentsOfFile:path];
377 [mgr setDictionary:dict];
379 ASLogNotice(@"Failed to override the Cocoa key bindings. Keyboard "
380 "input may behave strangely as a result (path=%@).", path);
384 - (void)applicationDidFinishLaunching:(NSNotification *)notification
386 [NSApp setServicesProvider:self];
387 #ifdef MM_ENABLE_PLUGINS
388 [[MMPlugInManager sharedManager] loadAllPlugIns];
391 if ([self maxPreloadCacheSize] > 0) {
392 [self scheduleVimControllerPreloadAfterDelay:2];
393 [self startWatchingVimDir];
396 ASLogInfo(@"MacVim finished launching");
399 - (BOOL)applicationShouldOpenUntitledFile:(NSApplication *)sender
401 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
402 NSAppleEventManager *aem = [NSAppleEventManager sharedAppleEventManager];
403 NSAppleEventDescriptor *desc = [aem currentAppleEvent];
405 // The user default MMUntitledWindow can be set to control whether an
406 // untitled window should open on 'Open' and 'Reopen' events.
407 int untitledWindowFlag = [ud integerForKey:MMUntitledWindowKey];
409 BOOL isAppOpenEvent = [desc eventID] == kAEOpenApplication;
410 if (isAppOpenEvent && (untitledWindowFlag & MMUntitledWindowOnOpen) == 0)
413 BOOL isAppReopenEvent = [desc eventID] == kAEReopenApplication;
415 && (untitledWindowFlag & MMUntitledWindowOnReopen) == 0)
418 // When a process is started from the command line, the 'Open' event may
419 // contain a parameter to surpress the opening of an untitled window.
420 desc = [desc paramDescriptorForKeyword:keyAEPropData];
421 desc = [desc paramDescriptorForKeyword:keyMMUntitledWindow];
422 if (desc && ![desc booleanValue])
425 // Never open an untitled window if there is at least one open window or if
426 // there are processes that are currently launching.
427 if ([vimControllers count] > 0 || [pidArguments count] > 0)
430 // NOTE! This way it possible to start the app with the command-line
431 // argument '-nowindow yes' and no window will be opened by default but
432 // this argument will only be heeded when the application is opening.
433 if (isAppOpenEvent && [ud boolForKey:MMNoWindowKey] == YES)
439 - (BOOL)applicationOpenUntitledFile:(NSApplication *)sender
441 ASLogDebug(@"Opening untitled window...");
442 [self newWindow:self];
446 - (void)application:(NSApplication *)sender openFiles:(NSArray *)filenames
448 ASLogInfo(@"Opening files %@", filenames);
450 // Extract ODB/Xcode/Spotlight parameters from the current Apple event,
451 // sort the filenames, and then let openFiles:withArguments: do the heavy
454 if (!(filenames && [filenames count] > 0))
457 // Sort filenames since the Finder doesn't take care in preserving the
458 // order in which files are selected anyway (and "sorted" is more
459 // predictable than "random").
460 if ([filenames count] > 1)
461 filenames = [filenames sortedArrayUsingSelector:
462 @selector(localizedCompare:)];
464 // Extract ODB/Xcode/Spotlight parameters from the current Apple event
465 NSMutableDictionary *arguments = [self extractArgumentsFromOdocEvent:
466 [[NSAppleEventManager sharedAppleEventManager] currentAppleEvent]];
468 if ([self openFiles:filenames withArguments:arguments]) {
469 [NSApp replyToOpenOrPrint:NSApplicationDelegateReplySuccess];
471 // TODO: Notify user of failure?
472 [NSApp replyToOpenOrPrint:NSApplicationDelegateReplyFailure];
476 - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender
478 return (MMTerminateWhenLastWindowClosed ==
479 [[NSUserDefaults standardUserDefaults]
480 integerForKey:MMLastWindowClosedBehaviorKey]);
483 - (NSApplicationTerminateReply)applicationShouldTerminate:
484 (NSApplication *)sender
486 // TODO: Follow Apple's guidelines for 'Graceful Application Termination'
487 // (in particular, allow user to review changes and save).
488 int reply = NSTerminateNow;
489 BOOL modifiedBuffers = NO;
491 // Go through windows, checking for modified buffers. (Each Vim process
492 // tells MacVim when any buffer has been modified and MacVim sets the
493 // 'documentEdited' flag of the window correspondingly.)
494 NSEnumerator *e = [[NSApp windows] objectEnumerator];
496 while ((window = [e nextObject])) {
497 if ([window isDocumentEdited]) {
498 modifiedBuffers = YES;
503 if (modifiedBuffers) {
504 NSAlert *alert = [[NSAlert alloc] init];
505 [alert setAlertStyle:NSWarningAlertStyle];
506 [alert addButtonWithTitle:NSLocalizedString(@"Quit",
508 [alert addButtonWithTitle:NSLocalizedString(@"Cancel",
510 [alert setMessageText:NSLocalizedString(@"Quit without saving?",
511 @"Quit dialog with changed buffers, title")];
512 [alert setInformativeText:NSLocalizedString(
513 @"There are modified buffers, "
514 "if you quit now all changes will be lost. Quit anyway?",
515 @"Quit dialog with changed buffers, text")];
517 if ([alert runModal] != NSAlertFirstButtonReturn)
518 reply = NSTerminateCancel;
522 // No unmodified buffers, but give a warning if there are multiple
523 // windows and/or tabs open.
524 int numWindows = [vimControllers count];
527 // Count the number of open tabs
528 e = [vimControllers objectEnumerator];
530 while ((vc = [e nextObject]))
531 numTabs += [[vc objectForVimStateKey:@"numTabs"] intValue];
533 if (numWindows > 1 || numTabs > 1) {
534 NSAlert *alert = [[NSAlert alloc] init];
535 [alert setAlertStyle:NSWarningAlertStyle];
536 [alert addButtonWithTitle:NSLocalizedString(@"Quit",
538 [alert addButtonWithTitle:NSLocalizedString(@"Cancel",
540 [alert setMessageText:NSLocalizedString(
541 @"Are you sure you want to quit MacVim?",
542 @"Quit dialog with no changed buffers, title")];
544 NSString *info = nil;
545 if (numWindows > 1) {
546 if (numTabs > numWindows)
547 info = [NSString stringWithFormat:NSLocalizedString(
548 @"There are %d windows open in MacVim, with a "
549 "total of %d tabs. Do you want to quit anyway?",
550 @"Quit dialog with no changed buffers, text"),
551 numWindows, numTabs];
553 info = [NSString stringWithFormat:NSLocalizedString(
554 @"There are %d windows open in MacVim. "
555 "Do you want to quit anyway?",
556 @"Quit dialog with no changed buffers, text"),
560 info = [NSString stringWithFormat:NSLocalizedString(
561 @"There are %d tabs open in MacVim. "
562 "Do you want to quit anyway?",
563 @"Quit dialog with no changed buffers, text"),
567 [alert setInformativeText:info];
569 if ([alert runModal] != NSAlertFirstButtonReturn)
570 reply = NSTerminateCancel;
577 // Tell all Vim processes to terminate now (otherwise they'll leave swap
579 if (NSTerminateNow == reply) {
580 e = [vimControllers objectEnumerator];
582 while ((vc = [e nextObject])) {
583 ASLogDebug(@"Terminate pid=%d", [vc pid]);
584 [vc sendMessage:TerminateNowMsgID data:nil];
587 e = [cachedVimControllers objectEnumerator];
588 while ((vc = [e nextObject])) {
589 ASLogDebug(@"Terminate pid=%d (cached)", [vc pid]);
590 [vc sendMessage:TerminateNowMsgID data:nil];
593 // If a Vim process is being preloaded as we quit we have to forcibly
594 // kill it since we have not established a connection yet.
595 if (preloadPid > 0) {
596 ASLogDebug(@"Kill incomplete preloaded process pid=%d", preloadPid);
597 kill(preloadPid, SIGKILL);
600 // If a Vim process was loading as we quit we also have to kill it.
601 e = [[pidArguments allKeys] objectEnumerator];
603 while ((pidKey = [e nextObject])) {
604 ASLogDebug(@"Kill incomplete process pid=%d", [pidKey intValue]);
605 kill([pidKey intValue], SIGKILL);
608 // Sleep a little to allow all the Vim processes to exit.
615 - (void)applicationWillTerminate:(NSNotification *)notification
617 ASLogInfo(@"Terminating MacVim...");
619 [self stopWatchingVimDir];
621 #ifdef MM_ENABLE_PLUGINS
622 [[MMPlugInManager sharedManager] unloadAllPlugIns];
625 #if MM_HANDLE_XCODE_MOD_EVENT
626 [[NSAppleEventManager sharedAppleEventManager]
627 removeEventHandlerForEventClass:'KAHL'
631 // This will invalidate all connections (since they were spawned from this
633 [connection invalidate];
635 // Deactivate the font we loaded from the app bundle.
636 // NOTE: This can take quite a while (~500 ms), so termination will be
637 // noticeably faster if loading of the default font is disabled.
638 if (fontContainerRef) {
639 ATSFontDeactivate(fontContainerRef, NULL, kATSOptionFlagsDefault);
640 fontContainerRef = 0;
643 [NSApp setDelegate:nil];
645 // Try to wait for all child processes to avoid leaving zombies behind (but
646 // don't wait around for too long).
647 NSDate *timeOutDate = [NSDate dateWithTimeIntervalSinceNow:2];
648 while ([timeOutDate timeIntervalSinceNow] > 0) {
649 [self reapChildProcesses:nil];
650 if (numChildProcesses <= 0)
653 ASLogDebug(@"%d processes still left, hold on...", numChildProcesses);
655 // Run in NSConnectionReplyMode while waiting instead of calling e.g.
656 // usleep(). Otherwise incoming messages may clog up the DO queues and
657 // the outgoing TerminateNowMsgID sent earlier never reaches the Vim
659 // This has at least one side-effect, namely we may receive the
660 // annoying "dropping incoming DO message". (E.g. this may happen if
661 // you quickly hit Cmd-n several times in a row and then immediately
662 // press Cmd-q, Enter.)
663 while (CFRunLoopRunInMode((CFStringRef)NSConnectionReplyMode,
664 0.05, true) == kCFRunLoopRunHandledSource)
668 if (numChildProcesses > 0) {
669 ASLogNotice(@"%d zombies left behind", numChildProcesses);
673 + (MMAppController *)sharedInstance
675 // Note: The app controller is a singleton which is instantiated in
676 // MainMenu.nib where it is also connected as the delegate of NSApp.
677 id delegate = [NSApp delegate];
678 return [delegate isKindOfClass:self] ? (MMAppController*)delegate : nil;
681 - (NSMenu *)defaultMainMenu
683 return defaultMainMenu;
686 - (NSMenuItem *)appMenuItemTemplate
688 return appMenuItemTemplate;
691 - (void)removeVimController:(id)controller
693 ASLogDebug(@"Remove Vim controller pid=%d id=%d (processingFlag=%d)",
694 [controller pid], [controller identifier], processingFlag);
696 int idx = [vimControllers indexOfObject:controller];
697 if (NSNotFound == idx) {
698 ASLogDebug(@"Controller not found, probably due to duplicate removal");
703 [vimControllers removeObjectAtIndex:idx];
704 [controller cleanup];
705 [controller release];
707 if (![vimControllers count]) {
708 // The last editor window just closed so restore the main menu back to
709 // its default state (which is defined in MainMenu.nib).
710 [self setMainMenu:defaultMainMenu];
712 BOOL hide = (MMHideWhenLastWindowClosed ==
713 [[NSUserDefaults standardUserDefaults]
714 integerForKey:MMLastWindowClosedBehaviorKey]);
719 // There is a small delay before the Vim process actually exits so wait a
720 // little before trying to reap the child process. If the process still
721 // hasn't exited after this wait it won't be reaped until the next time
722 // reapChildProcesses: is called (but this should be harmless).
723 [self performSelector:@selector(reapChildProcesses:)
728 - (void)windowControllerWillOpen:(MMWindowController *)windowController
730 NSPoint topLeft = NSZeroPoint;
731 NSWindow *topWin = [[[self topmostVimController] windowController] window];
732 NSWindow *win = [windowController window];
736 // If there is a window belonging to a Vim process, cascade from it,
737 // otherwise use the autosaved window position (if any).
739 NSRect frame = [topWin frame];
740 topLeft = NSMakePoint(frame.origin.x, NSMaxY(frame));
742 NSString *topLeftString = [[NSUserDefaults standardUserDefaults]
743 stringForKey:MMTopLeftPointKey];
745 topLeft = NSPointFromString(topLeftString);
748 if (!NSEqualPoints(topLeft, NSZeroPoint)) {
749 NSPoint oldTopLeft = topLeft;
751 topLeft = [win cascadeTopLeftFromPoint:topLeft];
753 [win setFrameTopLeftPoint:topLeft];
756 NSPoint screenOrigin = [[win screen] frame].origin;
757 if ([win frame].origin.y < screenOrigin.y) {
758 // Try to avoid shifting the new window downwards if it means
759 // that the bottom of the window will be off the screen. E.g.
760 // if the user has set windows to open maximized in the
761 // vertical direction then the new window will cascade
762 // horizontally only.
763 topLeft.y = oldTopLeft.y;
764 [win setFrameTopLeftPoint:topLeft];
767 if ([win frame].origin.y < screenOrigin.y) {
768 // Move the window to the top of the screen if the bottom of
769 // the window is still obscured.
770 topLeft.y = NSMaxY([[win screen] frame]);
771 [win setFrameTopLeftPoint:topLeft];
774 ASLogNotice(@"Window not on screen, don't constrain position");
778 if (1 == [vimControllers count]) {
779 // The first window autosaves its position. (The autosaving
780 // features of Cocoa are not used because we need more control over
781 // what is autosaved and when it is restored.)
782 [windowController setWindowAutosaveKey:MMTopLeftPointKey];
785 if (openSelectionString) {
786 // TODO: Pass this as a parameter instead! Get rid of
787 // 'openSelectionString' etc.
789 // There is some text to paste into this window as a result of the
790 // services menu "Open selection ..." being used.
791 [[windowController vimController] dropString:openSelectionString];
792 [openSelectionString release];
793 openSelectionString = nil;
796 if (shouldActivateWhenNextWindowOpens) {
797 [NSApp activateIgnoringOtherApps:YES];
798 shouldActivateWhenNextWindowOpens = NO;
802 - (void)setMainMenu:(NSMenu *)mainMenu
804 if ([NSApp mainMenu] == mainMenu) return;
806 // If the new menu has a "Recent Files" dummy item, then swap the real item
807 // for the dummy. We are forced to do this since Cocoa initializes the
808 // "Recent Files" menu and there is no way to simply point Cocoa to a new
809 // item each time the menus are swapped.
810 NSMenu *fileMenu = [mainMenu findFileMenu];
811 if (recentFilesMenuItem && fileMenu) {
813 [fileMenu indexOfItemWithAction:@selector(recentFilesDummy:)];
815 NSMenuItem *dummyItem = [[fileMenu itemAtIndex:dummyIdx] retain];
816 [fileMenu removeItemAtIndex:dummyIdx];
818 NSMenu *recentFilesParentMenu = [recentFilesMenuItem menu];
819 int idx = [recentFilesParentMenu indexOfItem:recentFilesMenuItem];
821 [[recentFilesMenuItem retain] autorelease];
822 [recentFilesParentMenu removeItemAtIndex:idx];
823 [recentFilesParentMenu insertItem:dummyItem atIndex:idx];
826 [fileMenu insertItem:recentFilesMenuItem atIndex:dummyIdx];
831 // Now set the new menu. Notice that we keep one menu for each editor
832 // window since each editor can have its own set of menus. When swapping
833 // menus we have to tell Cocoa where the new "MacVim", "Windows", and
834 // "Services" menu are.
835 [NSApp setMainMenu:mainMenu];
837 // Setting the "MacVim" (or "Application") menu ensures that it is typeset
838 // in boldface. (The setAppleMenu: method used to be public but is now
839 // private so this will have to be considered a bit of a hack!)
840 NSMenu *appMenu = [mainMenu findApplicationMenu];
841 [NSApp performSelector:@selector(setAppleMenu:) withObject:appMenu];
843 NSMenu *servicesMenu = [mainMenu findServicesMenu];
844 [NSApp setServicesMenu:servicesMenu];
846 NSMenu *windowsMenu = [mainMenu findWindowsMenu];
848 // Cocoa isn't clever enough to get rid of items it has added to the
849 // "Windows" menu so we have to do it ourselves otherwise there will be
850 // multiple menu items for each window in the "Windows" menu.
851 // This code assumes that the only items Cocoa add are ones which
852 // send off the action makeKeyAndOrderFront:. (Cocoa will not add
853 // another separator item if the last item on the "Windows" menu
854 // already is a separator, so we needen't worry about separators.)
855 int i, count = [windowsMenu numberOfItems];
856 for (i = count-1; i >= 0; --i) {
857 NSMenuItem *item = [windowsMenu itemAtIndex:i];
858 if ([item action] == @selector(makeKeyAndOrderFront:))
859 [windowsMenu removeItem:item];
862 [NSApp setWindowsMenu:windowsMenu];
864 #ifdef MM_ENABLE_PLUGINS
865 // Move plugin menu from old to new main menu.
866 [self removePlugInMenu];
867 [self addPlugInMenuToMenu:mainMenu];
871 - (NSArray *)filterOpenFiles:(NSArray *)filenames
873 return [self filterOpenFiles:filenames openFilesDict:nil];
876 - (BOOL)openFiles:(NSArray *)filenames withArguments:(NSDictionary *)args
878 // Opening files works like this:
879 // a) filter out any already open files
880 // b) open any remaining files
882 // A file is opened in an untitled window if there is one (it may be
883 // currently launching, or it may already be visible), otherwise a new
886 // Each launching Vim process has a dictionary of arguments that are passed
887 // to the process when in checks in (via connectBackend:pid:). The
888 // arguments for each launching process can be looked up by its PID (in the
889 // pidArguments dictionary).
891 NSMutableDictionary *arguments = (args ? [[args mutableCopy] autorelease]
892 : [NSMutableDictionary dictionary]);
894 filenames = normalizeFilenames(filenames);
897 // a) Filter out any already open files
899 NSString *firstFile = [filenames objectAtIndex:0];
900 MMVimController *firstController = nil;
901 NSDictionary *openFilesDict = nil;
902 filenames = [self filterOpenFiles:filenames openFilesDict:&openFilesDict];
904 // Pass arguments to vim controllers that had files open.
906 NSEnumerator *e = [openFilesDict keyEnumerator];
908 // (Indicate that we do not wish to open any files at the moment.)
909 [arguments setObject:[NSNumber numberWithBool:YES] forKey:@"dontOpen"];
911 while ((key = [e nextObject])) {
912 NSArray *files = [openFilesDict objectForKey:key];
913 [arguments setObject:files forKey:@"filenames"];
915 MMVimController *vc = [key pointerValue];
916 [vc passArguments:arguments];
918 // If this controller holds the first file, then remember it for later.
919 if ([files containsObject:firstFile])
920 firstController = vc;
923 // The meaning of "layout" is defined by the WIN_* defines in main.c.
924 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
925 int layout = [ud integerForKey:MMOpenLayoutKey];
926 BOOL splitVert = [ud boolForKey:MMVerticalSplitKey];
927 BOOL openInCurrentWindow = [ud boolForKey:MMOpenInCurrentWindowKey];
929 if (splitVert && MMLayoutHorizontalSplit == layout)
930 layout = MMLayoutVerticalSplit;
931 if (layout < 0 || (layout > MMLayoutTabs && openInCurrentWindow))
932 layout = MMLayoutTabs;
934 if ([filenames count] == 0) {
935 // Raise the window containing the first file that was already open,
936 // and make sure that the tab containing that file is selected. Only
937 // do this when there are no more files to open, otherwise sometimes
938 // the window with 'firstFile' will be raised, other times it might be
939 // the window that will open with the files in the 'filenames' array.
940 firstFile = [firstFile stringByEscapingSpecialFilenameCharacters];
942 NSString *bufCmd = @"tab sb";
944 case MMLayoutHorizontalSplit: bufCmd = @"sb"; break;
945 case MMLayoutVerticalSplit: bufCmd = @"vert sb"; break;
946 case MMLayoutArglist: bufCmd = @"b"; break;
949 NSString *input = [NSString stringWithFormat:@"<C-\\><C-N>"
950 ":let oldswb=&swb|let &swb=\"useopen,usetab\"|"
951 "%@ %@|let &swb=oldswb|unl oldswb|"
952 "cal foreground()<CR>", bufCmd, firstFile];
954 [firstController addVimInput:input];
959 // Add filenames to "Recent Files" menu, unless they are being edited
960 // remotely (using ODB).
961 if ([arguments objectForKey:@"remoteID"] == nil) {
962 [[NSDocumentController sharedDocumentController]
963 noteNewRecentFilePaths:filenames];
967 // b) Open any remaining files
970 [arguments setObject:[NSNumber numberWithInt:layout] forKey:@"layout"];
971 [arguments setObject:filenames forKey:@"filenames"];
972 // (Indicate that files should be opened from now on.)
973 [arguments setObject:[NSNumber numberWithBool:NO] forKey:@"dontOpen"];
976 if (openInCurrentWindow && (vc = [self topmostVimController])) {
977 // Open files in an already open window.
978 [[[vc windowController] window] makeKeyAndOrderFront:self];
979 [vc passArguments:arguments];
984 int numFiles = [filenames count];
985 if (MMLayoutWindows == layout && numFiles > 1) {
986 // Open one file at a time in a new window, but don't open too many at
987 // once (at most cap+1 windows will open). If the user has increased
988 // the preload cache size we'll take that as a hint that more windows
989 // should be able to open at once.
990 int cap = [self maxPreloadCacheSize] - 1;
991 if (cap < 4) cap = 4;
992 if (cap > numFiles) cap = numFiles;
995 for (i = 0; i < cap; ++i) {
996 NSArray *a = [NSArray arrayWithObject:[filenames objectAtIndex:i]];
997 [arguments setObject:a forKey:@"filenames"];
999 // NOTE: We have to copy the args since we'll mutate them in the
1000 // next loop and the below call may retain the arguments while
1001 // waiting for a process to start.
1002 NSDictionary *args = [[arguments copy] autorelease];
1004 openOk = [self openVimControllerWithArguments:args];
1008 // Open remaining files in tabs in a new window.
1009 if (openOk && numFiles > cap) {
1010 NSRange range = { i, numFiles-cap };
1011 NSArray *a = [filenames subarrayWithRange:range];
1012 [arguments setObject:a forKey:@"filenames"];
1013 [arguments setObject:[NSNumber numberWithInt:MMLayoutTabs]
1016 openOk = [self openVimControllerWithArguments:arguments];
1019 // Open all files at once.
1020 openOk = [self openVimControllerWithArguments:arguments];
1026 #ifdef MM_ENABLE_PLUGINS
1027 - (void)addItemToPlugInMenu:(NSMenuItem *)item
1029 NSMenu *menu = [plugInMenuItem submenu];
1030 [menu addItem:item];
1031 if ([menu numberOfItems] == 1)
1032 [self addPlugInMenuToMenu:[NSApp mainMenu]];
1035 - (void)removeItemFromPlugInMenu:(NSMenuItem *)item
1037 NSMenu *menu = [plugInMenuItem submenu];
1038 [menu removeItem:item];
1039 if ([menu numberOfItems] == 0)
1040 [self removePlugInMenu];
1044 - (IBAction)newWindow:(id)sender
1046 ASLogDebug(@"Open new window");
1048 // A cached controller requires no loading times and results in the new
1049 // window popping up instantaneously. If the cache is empty it may take
1050 // 1-2 seconds to start a new Vim process.
1051 MMVimController *vc = [self takeVimControllerFromCache];
1053 [[vc backendProxy] acknowledgeConnection];
1055 [self launchVimProcessWithArguments:nil];
1059 - (IBAction)newWindowAndActivate:(id)sender
1061 [self activateWhenNextWindowOpens];
1062 [self newWindow:sender];
1065 - (IBAction)fileOpen:(id)sender
1067 ASLogDebug(@"Show file open panel");
1069 NSString *dir = nil;
1070 BOOL trackPwd = [[NSUserDefaults standardUserDefaults]
1071 boolForKey:MMDialogsTrackPwdKey];
1073 MMVimController *vc = [self keyVimController];
1074 if (vc) dir = [vc objectForVimStateKey:@"pwd"];
1077 NSOpenPanel *panel = [NSOpenPanel openPanel];
1078 [panel setAllowsMultipleSelection:YES];
1079 [panel setAccessoryView:showHiddenFilesView()];
1081 int result = [panel runModalForDirectory:dir file:nil types:nil];
1082 if (NSOKButton == result)
1083 [self application:NSApp openFiles:[panel filenames]];
1086 - (IBAction)selectNextWindow:(id)sender
1088 ASLogDebug(@"Select next window");
1090 unsigned i, count = [vimControllers count];
1093 NSWindow *keyWindow = [NSApp keyWindow];
1094 for (i = 0; i < count; ++i) {
1095 MMVimController *vc = [vimControllers objectAtIndex:i];
1096 if ([[[vc windowController] window] isEqual:keyWindow])
1103 MMVimController *vc = [vimControllers objectAtIndex:i];
1104 [[vc windowController] showWindow:self];
1108 - (IBAction)selectPreviousWindow:(id)sender
1110 ASLogDebug(@"Select previous window");
1112 unsigned i, count = [vimControllers count];
1115 NSWindow *keyWindow = [NSApp keyWindow];
1116 for (i = 0; i < count; ++i) {
1117 MMVimController *vc = [vimControllers objectAtIndex:i];
1118 if ([[[vc windowController] window] isEqual:keyWindow])
1128 MMVimController *vc = [vimControllers objectAtIndex:i];
1129 [[vc windowController] showWindow:self];
1133 - (IBAction)orderFrontPreferencePanel:(id)sender
1135 ASLogDebug(@"Show preferences panel");
1136 [[MMPreferenceController sharedPrefsWindowController] showWindow:self];
1139 - (IBAction)openWebsite:(id)sender
1141 ASLogDebug(@"Open MacVim website");
1142 [[NSWorkspace sharedWorkspace] openURL:
1143 [NSURL URLWithString:MMWebsiteString]];
1146 - (IBAction)showVimHelp:(id)sender
1148 ASLogDebug(@"Open window with Vim help");
1149 // Open a new window with the help window maximized.
1150 [self launchVimProcessWithArguments:[NSArray arrayWithObjects:
1151 @"-c", @":h gui_mac", @"-c", @":res", nil]];
1154 - (IBAction)zoomAll:(id)sender
1156 ASLogDebug(@"Zoom all windows");
1157 [NSApp makeWindowsPerform:@selector(performZoom:) inOrder:YES];
1160 - (IBAction)atsuiButtonClicked:(id)sender
1162 ASLogDebug(@"Toggle ATSUI renderer");
1163 // This action is called when the user clicks the "use ATSUI renderer"
1164 // button in the advanced preferences pane.
1165 [self rebuildPreloadCache];
1168 - (IBAction)loginShellButtonClicked:(id)sender
1170 ASLogDebug(@"Toggle login shell option");
1171 // This action is called when the user clicks the "use login shell" button
1172 // in the advanced preferences pane.
1173 [self rebuildPreloadCache];
1176 - (IBAction)quickstartButtonClicked:(id)sender
1178 ASLogDebug(@"Toggle Quickstart option");
1179 if ([self maxPreloadCacheSize] > 0) {
1180 [self scheduleVimControllerPreloadAfterDelay:1.0];
1181 [self startWatchingVimDir];
1183 [self cancelVimControllerPreloadRequests];
1184 [self clearPreloadCacheWithCount:-1];
1185 [self stopWatchingVimDir];
1189 - (MMVimController *)keyVimController
1191 NSWindow *keyWindow = [NSApp keyWindow];
1193 unsigned i, count = [vimControllers count];
1194 for (i = 0; i < count; ++i) {
1195 MMVimController *vc = [vimControllers objectAtIndex:i];
1196 if ([[[vc windowController] window] isEqual:keyWindow])
1204 - (unsigned)connectBackend:(byref in id <MMBackendProtocol>)proxy pid:(int)pid
1206 ASLogDebug(@"pid=%d", pid);
1208 [(NSDistantObject*)proxy setProtocolForProxy:@protocol(MMBackendProtocol)];
1210 // NOTE: Allocate the vim controller now but don't add it to the list of
1211 // controllers since this is a distributed object call and as such can
1212 // arrive at unpredictable times (e.g. while iterating the list of vim
1214 // (What if input arrives before the vim controller is added to the list of
1215 // controllers? This should not be a problem since the input isn't
1216 // processed immediately (see processInput:forIdentifier:).)
1217 // Also, since the app may be multithreaded (e.g. as a result of showing
1218 // the open panel) we have to ensure this call happens on the main thread,
1219 // else there is a race condition that may lead to a crash.
1220 MMVimController *vc = [[MMVimController alloc] initWithBackend:proxy
1222 [self performSelectorOnMainThread:@selector(addVimController:)
1225 modes:[NSArray arrayWithObject:
1226 NSDefaultRunLoopMode]];
1230 return [vc identifier];
1233 - (oneway void)processInput:(in bycopy NSArray *)queue
1234 forIdentifier:(unsigned)identifier
1236 // NOTE: Input is not handled immediately since this is a distributed
1237 // object call and as such can arrive at unpredictable times. Instead,
1238 // queue the input and process it when the run loop is updated.
1240 if (!(queue && identifier)) {
1241 ASLogWarn(@"Bad input for identifier=%d", identifier);
1245 ASLogDebug(@"QUEUE for identifier=%d: <<< %@>>>", identifier,
1246 debugStringForMessageQueue(queue));
1248 NSNumber *key = [NSNumber numberWithUnsignedInt:identifier];
1249 NSArray *q = [inputQueues objectForKey:key];
1251 q = [q arrayByAddingObjectsFromArray:queue];
1252 [inputQueues setObject:q forKey:key];
1254 [inputQueues setObject:queue forKey:key];
1257 // NOTE: We must use "event tracking mode" as well as "default mode",
1258 // otherwise the input queue will not be processed e.g. during live
1260 // Also, since the app may be multithreaded (e.g. as a result of showing
1261 // the open panel) we have to ensure this call happens on the main thread,
1262 // else there is a race condition that may lead to a crash.
1263 [self performSelectorOnMainThread:@selector(processInputQueues:)
1266 modes:[NSArray arrayWithObjects:
1267 NSDefaultRunLoopMode,
1268 NSEventTrackingRunLoopMode, nil]];
1271 - (NSArray *)serverList
1273 NSMutableArray *array = [NSMutableArray array];
1275 unsigned i, count = [vimControllers count];
1276 for (i = 0; i < count; ++i) {
1277 MMVimController *controller = [vimControllers objectAtIndex:i];
1278 if ([controller serverName])
1279 [array addObject:[controller serverName]];
1285 @end // MMAppController
1290 @implementation MMAppController (MMServices)
1292 - (void)openSelection:(NSPasteboard *)pboard userData:(NSString *)userData
1293 error:(NSString **)error
1295 if (![[pboard types] containsObject:NSStringPboardType]) {
1296 ASLogNotice(@"Pasteboard contains no NSStringPboardType");
1300 ASLogInfo(@"Open new window containing current selection");
1302 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
1303 BOOL openInCurrentWindow = [ud boolForKey:MMOpenInCurrentWindowKey];
1304 MMVimController *vc;
1306 if (openInCurrentWindow && (vc = [self topmostVimController])) {
1307 [vc sendMessage:AddNewTabMsgID data:nil];
1308 [vc dropString:[pboard stringForType:NSStringPboardType]];
1310 // Save the text, open a new window, and paste the text when the next
1311 // window opens. (If this is called several times in a row, then all
1312 // but the last call may be ignored.)
1313 if (openSelectionString) [openSelectionString release];
1314 openSelectionString = [[pboard stringForType:NSStringPboardType] copy];
1316 [self newWindow:self];
1320 - (void)openFile:(NSPasteboard *)pboard userData:(NSString *)userData
1321 error:(NSString **)error
1323 if (![[pboard types] containsObject:NSStringPboardType]) {
1324 ASLogNotice(@"Pasteboard contains no NSStringPboardType");
1328 // TODO: Parse multiple filenames and create array with names.
1329 NSString *string = [pboard stringForType:NSStringPboardType];
1330 string = [string stringByTrimmingCharactersInSet:
1331 [NSCharacterSet whitespaceAndNewlineCharacterSet]];
1332 string = [string stringByStandardizingPath];
1334 ASLogInfo(@"Open new window with selected file: %@", string);
1336 NSArray *filenames = [self filterFilesAndNotify:
1337 [NSArray arrayWithObject:string]];
1338 if ([filenames count] == 0)
1341 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
1342 BOOL openInCurrentWindow = [ud boolForKey:MMOpenInCurrentWindowKey];
1343 MMVimController *vc;
1345 if (openInCurrentWindow && (vc = [self topmostVimController])) {
1346 [vc dropFiles:filenames forceOpen:YES];
1348 [self openFiles:filenames withArguments:nil];
1352 - (void)newFileHere:(NSPasteboard *)pboard userData:(NSString *)userData
1353 error:(NSString **)error
1355 if (![[pboard types] containsObject:NSStringPboardType]) {
1356 ASLogNotice(@"Pasteboard contains no NSStringPboardType");
1360 NSString *path = [pboard stringForType:NSStringPboardType];
1363 if (![[NSFileManager defaultManager] fileExistsAtPath:path
1364 isDirectory:&dirIndicator]) {
1365 ASLogNotice(@"Invalid path. Cannot open new document at: %@", path);
1369 ASLogInfo(@"Open new file at path=%@", path);
1372 path = [path stringByDeletingLastPathComponent];
1374 path = [path stringByEscapingSpecialFilenameCharacters];
1376 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
1377 BOOL openInCurrentWindow = [ud boolForKey:MMOpenInCurrentWindowKey];
1378 MMVimController *vc;
1380 if (openInCurrentWindow && (vc = [self topmostVimController])) {
1381 NSString *input = [NSString stringWithFormat:@"<C-\\><C-N>"
1382 ":tabe|cd %@<CR>", path];
1383 [vc addVimInput:input];
1385 NSString *input = [NSString stringWithFormat:@":cd %@", path];
1386 [self launchVimProcessWithArguments:[NSArray arrayWithObjects:
1387 @"-c", input, nil]];
1391 @end // MMAppController (MMServices)
1396 @implementation MMAppController (Private)
1398 - (MMVimController *)topmostVimController
1400 // Find the topmost visible window which has an associated vim controller.
1401 NSEnumerator *e = [[NSApp orderedWindows] objectEnumerator];
1403 while ((window = [e nextObject]) && [window isVisible]) {
1404 unsigned i, count = [vimControllers count];
1405 for (i = 0; i < count; ++i) {
1406 MMVimController *vc = [vimControllers objectAtIndex:i];
1407 if ([[[vc windowController] window] isEqual:window])
1415 - (int)launchVimProcessWithArguments:(NSArray *)args
1418 NSString *path = [[NSBundle mainBundle] pathForAuxiliaryExecutable:@"Vim"];
1421 ASLogCrit(@"Vim executable could not be found inside app bundle!");
1425 NSArray *taskArgs = [NSArray arrayWithObjects:@"-g", @"-f", nil];
1427 taskArgs = [taskArgs arrayByAddingObjectsFromArray:args];
1429 BOOL useLoginShell = [[NSUserDefaults standardUserDefaults]
1430 boolForKey:MMLoginShellKey];
1431 if (useLoginShell) {
1432 // Run process with a login shell, roughly:
1433 // echo "exec Vim -g -f args" | ARGV0=-`basename $SHELL` $SHELL [-l]
1434 pid = [self executeInLoginShell:path arguments:taskArgs];
1436 // Run process directly:
1438 NSTask *task = [NSTask launchedTaskWithLaunchPath:path
1439 arguments:taskArgs];
1440 pid = task ? [task processIdentifier] : -1;
1444 // The 'pidArguments' dictionary keeps arguments to be passed to the
1445 // process when it connects (this is in contrast to arguments which are
1446 // passed on the command line, like '-f' and '-g').
1447 // If this method is called with nil arguments we take this as a hint
1448 // that this is an "untitled window" being launched and add a null
1449 // object to the 'pidArguments' dictionary. This way we can detect if
1450 // an untitled window is being launched by looking for null objects in
1452 // If this method is called with non-nil arguments then it is assumed
1453 // that the caller takes care of adding items to 'pidArguments' as
1454 // necessary (only some arguments are passed on connect, e.g. files to
1457 [pidArguments setObject:[NSNull null]
1458 forKey:[NSNumber numberWithInt:pid]];
1460 ASLogWarn(@"Failed to launch Vim process: args=%@, useLoginShell=%d",
1461 args, useLoginShell);
1467 - (NSArray *)filterFilesAndNotify:(NSArray *)filenames
1469 // Go trough 'filenames' array and make sure each file exists. Present
1470 // warning dialog if some file was missing.
1472 NSString *firstMissingFile = nil;
1473 NSMutableArray *files = [NSMutableArray array];
1474 unsigned i, count = [filenames count];
1476 for (i = 0; i < count; ++i) {
1477 NSString *name = [filenames objectAtIndex:i];
1478 if ([[NSFileManager defaultManager] fileExistsAtPath:name]) {
1479 [files addObject:name];
1480 } else if (!firstMissingFile) {
1481 firstMissingFile = name;
1485 if (firstMissingFile) {
1486 NSAlert *alert = [[NSAlert alloc] init];
1487 [alert addButtonWithTitle:NSLocalizedString(@"OK",
1491 if ([files count] >= count-1) {
1492 [alert setMessageText:NSLocalizedString(@"File not found",
1493 @"File not found dialog, title")];
1494 text = [NSString stringWithFormat:NSLocalizedString(
1495 @"Could not open file with name %@.",
1496 @"File not found dialog, text"), firstMissingFile];
1498 [alert setMessageText:NSLocalizedString(@"Multiple files not found",
1499 @"File not found dialog, title")];
1500 text = [NSString stringWithFormat:NSLocalizedString(
1501 @"Could not open file with name %@, and %d other files.",
1502 @"File not found dialog, text"),
1503 firstMissingFile, count-[files count]-1];
1506 [alert setInformativeText:text];
1507 [alert setAlertStyle:NSWarningAlertStyle];
1512 [NSApp replyToOpenOrPrint:NSApplicationDelegateReplyFailure];
1518 - (NSArray *)filterOpenFiles:(NSArray *)filenames
1519 openFilesDict:(NSDictionary **)openFiles
1521 // Filter out any files in the 'filenames' array that are open and return
1522 // all files that are not already open. On return, the 'openFiles'
1523 // parameter (if non-nil) will point to a dictionary of open files, indexed
1524 // by Vim controller.
1526 NSMutableDictionary *dict = [NSMutableDictionary dictionary];
1527 NSMutableArray *files = [filenames mutableCopy];
1529 // TODO: Escape special characters in 'files'?
1530 NSString *expr = [NSString stringWithFormat:
1531 @"map([\"%@\"],\"bufloaded(v:val)\")",
1532 [files componentsJoinedByString:@"\",\""]];
1534 unsigned i, count = [vimControllers count];
1535 for (i = 0; i < count && [files count] > 0; ++i) {
1536 MMVimController *vc = [vimControllers objectAtIndex:i];
1538 // Query Vim for which files in the 'files' array are open.
1539 NSString *eval = [vc evaluateVimExpression:expr];
1540 if (!eval) continue;
1542 NSIndexSet *idxSet = [NSIndexSet indexSetWithVimList:eval];
1543 if ([idxSet count] > 0) {
1544 [dict setObject:[files objectsAtIndexes:idxSet]
1545 forKey:[NSValue valueWithPointer:vc]];
1547 // Remove all the files that were open in this Vim process and
1548 // create a new expression to evaluate.
1549 [files removeObjectsAtIndexes:idxSet];
1550 expr = [NSString stringWithFormat:
1551 @"map([\"%@\"],\"bufloaded(v:val)\")",
1552 [files componentsJoinedByString:@"\",\""]];
1556 if (openFiles != nil)
1562 #if MM_HANDLE_XCODE_MOD_EVENT
1563 - (void)handleXcodeModEvent:(NSAppleEventDescriptor *)event
1564 replyEvent:(NSAppleEventDescriptor *)reply
1567 // Xcode sends this event to query MacVim which open files have been
1569 ASLogDebug(@"reply:%@", reply);
1570 ASLogDebug(@"event:%@", event);
1572 NSEnumerator *e = [vimControllers objectEnumerator];
1574 while ((vc = [e nextObject])) {
1575 DescType type = [reply descriptorType];
1576 unsigned len = [[type data] length];
1577 NSMutableData *data = [NSMutableData data];
1579 [data appendBytes:&type length:sizeof(DescType)];
1580 [data appendBytes:&len length:sizeof(unsigned)];
1581 [data appendBytes:[reply data] length:len];
1583 [vc sendMessage:XcodeModMsgID data:data];
1589 - (void)handleGetURLEvent:(NSAppleEventDescriptor *)event
1590 replyEvent:(NSAppleEventDescriptor *)reply
1592 NSString *urlString = [[event paramDescriptorForKeyword:keyDirectObject]
1594 NSURL *url = [NSURL URLWithString:urlString];
1596 // We try to be compatible with TextMate's URL scheme here, as documented
1597 // at http://blog.macromates.com/2007/the-textmate-url-scheme/ . Currently,
1600 // The format is: mvim://open?<arguments> where arguments can be:
1602 // * url — the actual file to open (i.e. a file://… URL), if you leave
1603 // out this argument, the frontmost document is implied.
1604 // * line — line number to go to (one based).
1605 // * column — column number to go to (one based).
1607 // Example: mvim://open?url=file:///etc/profile&line=20
1609 if ([[url host] isEqualToString:@"open"]) {
1610 NSMutableDictionary *dict = [NSMutableDictionary dictionary];
1612 // Parse query ("url=file://...&line=14") into a dictionary
1613 NSArray *queries = [[url query] componentsSeparatedByString:@"&"];
1614 NSEnumerator *enumerator = [queries objectEnumerator];
1616 while( param = [enumerator nextObject] ) {
1617 NSArray *arr = [param componentsSeparatedByString:@"="];
1618 if ([arr count] == 2) {
1619 [dict setValue:[[arr lastObject]
1620 stringByReplacingPercentEscapesUsingEncoding:
1621 NSUTF8StringEncoding]
1622 forKey:[[arr objectAtIndex:0]
1623 stringByReplacingPercentEscapesUsingEncoding:
1624 NSUTF8StringEncoding]];
1628 // Actually open the file.
1629 NSString *file = [dict objectForKey:@"url"];
1631 NSURL *fileUrl= [NSURL URLWithString:file];
1632 // TextMate only opens files that already exist.
1633 if ([fileUrl isFileURL]
1634 && [[NSFileManager defaultManager] fileExistsAtPath:
1636 // Strip 'file://' path, else application:openFiles: might think
1637 // the file is not yet open.
1638 NSArray *filenames = [NSArray arrayWithObject:[fileUrl path]];
1640 // Look for the line and column options.
1641 NSDictionary *args = nil;
1642 NSString *line = [dict objectForKey:@"line"];
1644 NSString *column = [dict objectForKey:@"column"];
1646 args = [NSDictionary dictionaryWithObjectsAndKeys:
1647 line, @"cursorLine",
1648 column, @"cursorColumn",
1651 args = [NSDictionary dictionaryWithObject:line
1652 forKey:@"cursorLine"];
1655 [self openFiles:filenames withArguments:args];
1659 NSAlert *alert = [[NSAlert alloc] init];
1660 [alert addButtonWithTitle:NSLocalizedString(@"OK",
1663 [alert setMessageText:NSLocalizedString(@"Unknown URL Scheme",
1664 @"Unknown URL Scheme dialog, title")];
1665 [alert setInformativeText:[NSString stringWithFormat:NSLocalizedString(
1666 @"This version of MacVim does not support \"%@\""
1667 @" in its URL scheme.",
1668 @"Unknown URL Scheme dialog, text"),
1671 [alert setAlertStyle:NSWarningAlertStyle];
1678 - (int)findLaunchingProcessWithoutArguments
1680 NSArray *keys = [pidArguments allKeysForObject:[NSNull null]];
1681 if ([keys count] > 0)
1682 return [[keys objectAtIndex:0] intValue];
1687 - (MMVimController *)findUnusedEditor
1689 NSEnumerator *e = [vimControllers objectEnumerator];
1691 while ((vc = [e nextObject])) {
1692 if ([[vc objectForVimStateKey:@"unusedEditor"] boolValue])
1699 - (NSMutableDictionary *)extractArgumentsFromOdocEvent:
1700 (NSAppleEventDescriptor *)desc
1702 NSMutableDictionary *dict = [NSMutableDictionary dictionary];
1704 // 1. Extract ODB parameters (if any)
1705 NSAppleEventDescriptor *odbdesc = desc;
1706 if (![odbdesc paramDescriptorForKeyword:keyFileSender]) {
1707 // The ODB paramaters may hide inside the 'keyAEPropData' descriptor.
1708 odbdesc = [odbdesc paramDescriptorForKeyword:keyAEPropData];
1709 if (![odbdesc paramDescriptorForKeyword:keyFileSender])
1714 NSAppleEventDescriptor *p =
1715 [odbdesc paramDescriptorForKeyword:keyFileSender];
1717 [dict setObject:[NSNumber numberWithUnsignedInt:[p typeCodeValue]]
1718 forKey:@"remoteID"];
1720 p = [odbdesc paramDescriptorForKeyword:keyFileCustomPath];
1722 [dict setObject:[p stringValue] forKey:@"remotePath"];
1724 p = [odbdesc paramDescriptorForKeyword:keyFileSenderToken];
1726 [dict setObject:[NSNumber numberWithUnsignedLong:[p descriptorType]]
1727 forKey:@"remoteTokenDescType"];
1728 [dict setObject:[p data] forKey:@"remoteTokenData"];
1732 // 2. Extract Xcode parameters (if any)
1733 NSAppleEventDescriptor *xcodedesc =
1734 [desc paramDescriptorForKeyword:keyAEPosition];
1737 MMSelectionRange *sr = (MMSelectionRange*)[[xcodedesc data] bytes];
1739 if (sr->lineNum < 0) {
1740 // Should select a range of lines.
1741 range.location = sr->startRange + 1;
1742 range.length = sr->endRange - sr->startRange + 1;
1744 // Should only move cursor to a line.
1745 range.location = sr->lineNum + 1;
1749 [dict setObject:NSStringFromRange(range) forKey:@"selectionRange"];
1752 // 3. Extract Spotlight search text (if any)
1753 NSAppleEventDescriptor *spotlightdesc =
1754 [desc paramDescriptorForKeyword:keyAESearchText];
1756 [dict setObject:[spotlightdesc stringValue] forKey:@"searchText"];
1761 #ifdef MM_ENABLE_PLUGINS
1762 - (void)removePlugInMenu
1764 if ([plugInMenuItem menu])
1765 [[plugInMenuItem menu] removeItem:plugInMenuItem];
1768 - (void)addPlugInMenuToMenu:(NSMenu *)mainMenu
1770 NSMenu *windowsMenu = [mainMenu findWindowsMenu];
1772 if ([[plugInMenuItem submenu] numberOfItems] > 0) {
1773 int idx = windowsMenu ? [mainMenu indexOfItemWithSubmenu:windowsMenu]
1776 [mainMenu insertItem:plugInMenuItem atIndex:idx];
1778 [mainMenu addItem:plugInMenuItem];
1784 - (void)scheduleVimControllerPreloadAfterDelay:(NSTimeInterval)delay
1786 [self performSelector:@selector(preloadVimController:)
1791 - (void)cancelVimControllerPreloadRequests
1793 [NSObject cancelPreviousPerformRequestsWithTarget:self
1794 selector:@selector(preloadVimController:)
1798 - (void)preloadVimController:(id)sender
1800 // We only allow preloading of one Vim process at a time (to avoid hogging
1801 // CPU), so schedule another preload in a little while if necessary.
1802 if (-1 != preloadPid) {
1803 [self scheduleVimControllerPreloadAfterDelay:2];
1807 if ([cachedVimControllers count] >= [self maxPreloadCacheSize])
1810 preloadPid = [self launchVimProcessWithArguments:
1811 [NSArray arrayWithObject:@"--mmwaitforack"]];
1814 - (int)maxPreloadCacheSize
1816 // The maximum number of Vim processes to keep in the cache can be
1817 // controlled via the user default "MMPreloadCacheSize".
1818 int maxCacheSize = [[NSUserDefaults standardUserDefaults]
1819 integerForKey:MMPreloadCacheSizeKey];
1820 if (maxCacheSize < 0) maxCacheSize = 0;
1821 else if (maxCacheSize > 10) maxCacheSize = 10;
1823 return maxCacheSize;
1826 - (MMVimController *)takeVimControllerFromCache
1828 // NOTE: After calling this message the backend corresponding to the
1829 // returned vim controller must be sent an acknowledgeConnection message,
1830 // else the vim process will be stuck.
1832 // This method may return nil even though the cache might be non-empty; the
1833 // caller should handle this by starting a new Vim process.
1835 int i, count = [cachedVimControllers count];
1836 if (0 == count) return nil;
1838 // Locate the first Vim controller with up-to-date rc-files sourced.
1839 NSDate *rcDate = [self rcFilesModificationDate];
1840 for (i = 0; i < count; ++i) {
1841 MMVimController *vc = [cachedVimControllers objectAtIndex:i];
1842 NSDate *date = [vc creationDate];
1843 if ([date compare:rcDate] != NSOrderedAscending)
1848 // Clear out cache entries whose vimrc/gvimrc files were sourced before
1849 // the latest modification date for those files. This ensures that the
1850 // latest rc-files are always sourced for new windows.
1851 [self clearPreloadCacheWithCount:i];
1854 if ([cachedVimControllers count] == 0) {
1855 [self scheduleVimControllerPreloadAfterDelay:2.0];
1859 MMVimController *vc = [cachedVimControllers objectAtIndex:0];
1860 [vimControllers addObject:vc];
1861 [cachedVimControllers removeObjectAtIndex:0];
1862 [vc setIsPreloading:NO];
1864 // If the Vim process has finished loading then the window will displayed
1865 // now, otherwise it will be displayed when the OpenWindowMsgID message is
1867 [[vc windowController] showWindow];
1869 // Since we've taken one controller from the cache we take the opportunity
1870 // to preload another.
1871 [self scheduleVimControllerPreloadAfterDelay:1];
1876 - (void)clearPreloadCacheWithCount:(int)count
1878 // Remove the 'count' first entries in the preload cache. It is assumed
1879 // that objects are added/removed from the cache in a FIFO manner so that
1880 // this effectively clears the 'count' oldest entries.
1881 // If 'count' is negative, then the entire cache is cleared.
1883 if ([cachedVimControllers count] == 0 || count == 0)
1887 count = [cachedVimControllers count];
1889 // Make sure the preloaded Vim processes get killed or they'll just hang
1890 // around being useless until MacVim is terminated.
1891 NSEnumerator *e = [cachedVimControllers objectEnumerator];
1892 MMVimController *vc;
1894 while ((vc = [e nextObject]) && n-- > 0) {
1895 [[NSNotificationCenter defaultCenter] removeObserver:vc];
1896 [vc sendMessage:TerminateNowMsgID data:nil];
1898 // Since the preloaded processes were killed "prematurely" we have to
1899 // manually tell them to cleanup (it is not enough to simply release
1900 // them since deallocation and cleanup are separated).
1905 while (n-- > 0 && [cachedVimControllers count] > 0)
1906 [cachedVimControllers removeObjectAtIndex:0];
1908 // There is a small delay before the Vim process actually exits so wait a
1909 // little before trying to reap the child process. If the process still
1910 // hasn't exited after this wait it won't be reaped until the next time
1911 // reapChildProcesses: is called (but this should be harmless).
1912 [self performSelector:@selector(reapChildProcesses:)
1917 - (void)rebuildPreloadCache
1919 if ([self maxPreloadCacheSize] > 0) {
1920 [self clearPreloadCacheWithCount:-1];
1921 [self cancelVimControllerPreloadRequests];
1922 [self scheduleVimControllerPreloadAfterDelay:1.0];
1926 - (NSDate *)rcFilesModificationDate
1928 // Check modification dates for ~/.vimrc and ~/.gvimrc and return the
1929 // latest modification date. If ~/.vimrc does not exist, check ~/_vimrc
1930 // and similarly for gvimrc.
1931 // Returns distantPath if no rc files were found.
1933 NSDate *date = [NSDate distantPast];
1934 NSFileManager *fm = [NSFileManager defaultManager];
1936 NSString *path = [@"~/.vimrc" stringByExpandingTildeInPath];
1937 NSDictionary *attr = [fm fileAttributesAtPath:path traverseLink:YES];
1939 path = [@"~/_vimrc" stringByExpandingTildeInPath];
1940 attr = [fm fileAttributesAtPath:path traverseLink:YES];
1942 NSDate *modDate = [attr objectForKey:NSFileModificationDate];
1946 path = [@"~/.gvimrc" stringByExpandingTildeInPath];
1947 attr = [fm fileAttributesAtPath:path traverseLink:YES];
1949 path = [@"~/_gvimrc" stringByExpandingTildeInPath];
1950 attr = [fm fileAttributesAtPath:path traverseLink:YES];
1952 modDate = [attr objectForKey:NSFileModificationDate];
1954 date = [date laterDate:modDate];
1959 - (BOOL)openVimControllerWithArguments:(NSDictionary *)arguments
1961 MMVimController *vc = [self findUnusedEditor];
1963 // Open files in an already open window.
1964 [[[vc windowController] window] makeKeyAndOrderFront:self];
1965 [vc passArguments:arguments];
1966 } else if ((vc = [self takeVimControllerFromCache])) {
1967 // Open files in a new window using a cached vim controller. This
1968 // requires virtually no loading time so the new window will pop up
1970 [vc passArguments:arguments];
1971 [[vc backendProxy] acknowledgeConnection];
1973 // Open files in a launching Vim process or start a new process. This
1974 // may take 1-2 seconds so there will be a visible delay before the
1975 // window appears on screen.
1976 int pid = [self findLaunchingProcessWithoutArguments];
1978 pid = [self launchVimProcessWithArguments:nil];
1983 // TODO: If the Vim process fails to start, or if it changes PID,
1984 // then the memory allocated for these parameters will leak.
1985 // Ensure that this cannot happen or somehow detect it.
1987 if ([arguments count] > 0)
1988 [pidArguments setObject:arguments
1989 forKey:[NSNumber numberWithInt:pid]];
1995 - (void)activateWhenNextWindowOpens
1997 ASLogDebug(@"Activate MacVim when next window opens");
1998 shouldActivateWhenNextWindowOpens = YES;
2001 - (void)startWatchingVimDir
2003 #if (MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_4)
2006 if (NULL == FSEventStreamStart)
2007 return; // FSEvent functions are weakly linked
2009 NSString *path = [@"~/.vim" stringByExpandingTildeInPath];
2010 NSArray *pathsToWatch = [NSArray arrayWithObject:path];
2012 fsEventStream = FSEventStreamCreate(NULL, &fsEventCallback, NULL,
2013 (CFArrayRef)pathsToWatch, kFSEventStreamEventIdSinceNow,
2014 MMEventStreamLatency, kFSEventStreamCreateFlagNone);
2016 FSEventStreamScheduleWithRunLoop(fsEventStream,
2017 [[NSRunLoop currentRunLoop] getCFRunLoop],
2018 kCFRunLoopDefaultMode);
2020 FSEventStreamStart(fsEventStream);
2021 ASLogDebug(@"Started FS event stream");
2025 - (void)stopWatchingVimDir
2027 #if (MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_4)
2028 if (NULL == FSEventStreamStop)
2029 return; // FSEvent functions are weakly linked
2031 if (fsEventStream) {
2032 FSEventStreamStop(fsEventStream);
2033 FSEventStreamInvalidate(fsEventStream);
2034 FSEventStreamRelease(fsEventStream);
2035 fsEventStream = NULL;
2036 ASLogDebug(@"Stopped FS event stream");
2042 - (void)handleFSEvent
2044 [self clearPreloadCacheWithCount:-1];
2046 // Several FS events may arrive in quick succession so make sure to cancel
2047 // any previous preload requests before making a new one.
2048 [self cancelVimControllerPreloadRequests];
2049 [self scheduleVimControllerPreloadAfterDelay:0.5];
2052 - (void)loadDefaultFont
2054 // It is possible to set a user default to avoid loading the default font
2055 // (this cuts down on startup time).
2056 if (![[NSUserDefaults standardUserDefaults] boolForKey:MMLoadDefaultFontKey]
2057 || fontContainerRef) {
2058 ASLogInfo(@"Skip loading of the default font...");
2062 ASLogInfo(@"Loading the default font...");
2064 // Load all fonts in the Resouces folder of the app bundle.
2065 NSString *fontsFolder = [[NSBundle mainBundle] resourcePath];
2067 NSURL *fontsURL = [NSURL fileURLWithPath:fontsFolder];
2070 CFURLGetFSRef((CFURLRef)fontsURL, &fsRef);
2072 #if (MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_4)
2073 // This is the font activation API for OS X 10.5. Only compile
2074 // this code if we're building on OS X 10.5 or later.
2075 if (NULL != ATSFontActivateFromFileReference) { // Weakly linked
2076 ATSFontActivateFromFileReference(&fsRef, kATSFontContextLocal,
2077 kATSFontFormatUnspecified,
2078 NULL, kATSOptionFlagsDefault,
2082 #if (MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_4)
2083 // The following font activation API was deprecated in OS X 10.5.
2084 // Don't compile this code unless we're targeting OS X 10.4.
2086 if (fontContainerRef == 0 &&
2087 FSGetCatalogInfo(&fsRef, kFSCatInfoNone, NULL, NULL,
2088 &fsSpec, NULL) == noErr) {
2089 ATSFontActivateFromFileSpecification(&fsSpec,
2090 kATSFontContextLocal, kATSFontFormatUnspecified, NULL,
2091 kATSOptionFlagsDefault, &fontContainerRef);
2097 if (!fontContainerRef) {
2098 ASLogNotice(@"Failed to activate the default font (the app bundle "
2099 "may be incomplete)");
2103 - (int)executeInLoginShell:(NSString *)path arguments:(NSArray *)args
2105 // Start a login shell and execute the command 'path' with arguments 'args'
2106 // in the shell. This ensures that user environment variables are set even
2107 // when MacVim was started from the Finder.
2110 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
2112 // Determine which shell to use to execute the command. The user
2113 // may decide which shell to use by setting a user default or the
2114 // $SHELL environment variable.
2115 NSString *shell = [ud stringForKey:MMLoginShellCommandKey];
2116 if (!shell || [shell length] == 0)
2117 shell = [[[NSProcessInfo processInfo] environment]
2118 objectForKey:@"SHELL"];
2120 shell = @"/bin/bash";
2122 // Bash needs the '-l' flag to launch a login shell. The user may add
2123 // flags by setting a user default.
2124 NSString *shellArgument = [ud stringForKey:MMLoginShellArgumentKey];
2125 if (!shellArgument || [shellArgument length] == 0) {
2126 if ([[shell lastPathComponent] isEqual:@"bash"])
2127 shellArgument = @"-l";
2129 shellArgument = nil;
2132 // Build input string to pipe to the login shell.
2133 NSMutableString *input = [NSMutableString stringWithFormat:
2134 @"exec \"%@\"", path];
2136 // Append all arguments, making sure they are properly quoted, even
2137 // when they contain single quotes.
2138 NSEnumerator *e = [args objectEnumerator];
2141 while ((obj = [e nextObject])) {
2142 NSMutableString *arg = [NSMutableString stringWithString:obj];
2143 [arg replaceOccurrencesOfString:@"'" withString:@"'\"'\"'"
2144 options:NSLiteralSearch
2145 range:NSMakeRange(0, [arg length])];
2146 [input appendFormat:@" '%@'", arg];
2150 // Build the argument vector used to start the login shell.
2151 NSString *shellArg0 = [NSString stringWithFormat:@"-%@",
2152 [shell lastPathComponent]];
2153 char *shellArgv[3] = { (char *)[shellArg0 UTF8String], NULL, NULL };
2155 shellArgv[1] = (char *)[shellArgument UTF8String];
2157 // Get the C string representation of the shell path before the fork since
2158 // we must not call Foundation functions after a fork.
2159 const char *shellPath = [shell fileSystemRepresentation];
2161 // Fork and execute the process.
2163 if (pipe(ds)) return -1;
2168 } else if (pid == 0) {
2171 if (close(ds[1]) == -1) exit(255);
2172 if (dup2(ds[0], 0) == -1) exit(255);
2174 // Without the following call warning messages like this appear on the
2176 // com.apple.launchd[69] : Stray process with PGID equal to this
2177 // dead job: PID 1589 PPID 1 Vim
2180 execv(shellPath, shellArgv);
2182 // Never reached unless execv fails
2186 if (close(ds[0]) == -1) return -1;
2188 // Send input to execute to the child process
2189 [input appendString:@"\n"];
2190 int bytes = [input lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
2192 if (write(ds[1], [input UTF8String], bytes) != bytes) return -1;
2193 if (close(ds[1]) == -1) return -1;
2195 ++numChildProcesses;
2196 ASLogDebug(@"new process pid=%d (count=%d)", pid, numChildProcesses);
2202 - (void)reapChildProcesses:(id)sender
2204 // NOTE: numChildProcesses (currently) only counts the number of Vim
2205 // processes that have been started with executeInLoginShell::. If other
2206 // processes are spawned this code may need to be adjusted (or
2207 // numChildProcesses needs to be incremented when such a process is
2209 while (numChildProcesses > 0) {
2211 int pid = waitpid(-1, &status, WNOHANG);
2215 ASLogDebug(@"Wait for pid=%d complete", pid);
2216 --numChildProcesses;
2220 - (void)processInputQueues:(id)sender
2222 // NOTE: Because we use distributed objects it is quite possible for this
2223 // function to be re-entered. This can cause all sorts of unexpected
2224 // problems so we guard against it here so that the rest of the code does
2225 // not need to worry about it.
2227 // The processing flag is > 0 if this function is already on the call
2228 // stack; < 0 if this function was also re-entered.
2229 if (processingFlag != 0) {
2230 ASLogDebug(@"BUSY!");
2231 processingFlag = -1;
2235 // NOTE: Be _very_ careful that no exceptions can be raised between here
2236 // and the point at which 'processingFlag' is reset. Otherwise the above
2237 // test could end up always failing and no input queues would ever be
2241 // NOTE: New input may arrive while we're busy processing; we deal with
2242 // this by putting the current queue aside and creating a new input queue
2243 // for future input.
2244 NSDictionary *queues = inputQueues;
2245 inputQueues = [NSMutableDictionary new];
2247 // Pass each input queue on to the vim controller with matching
2248 // identifier (and note that it could be cached).
2249 NSEnumerator *e = [queues keyEnumerator];
2251 while ((key = [e nextObject])) {
2252 unsigned ukey = [key unsignedIntValue];
2253 int i = 0, count = [vimControllers count];
2254 for (i = 0; i < count; ++i) {
2255 MMVimController *vc = [vimControllers objectAtIndex:i];
2256 if (ukey == [vc identifier]) {
2257 [vc processInputQueue:[queues objectForKey:key]]; // !exceptions
2262 if (i < count) continue;
2264 count = [cachedVimControllers count];
2265 for (i = 0; i < count; ++i) {
2266 MMVimController *vc = [cachedVimControllers objectAtIndex:i];
2267 if (ukey == [vc identifier]) {
2268 [vc processInputQueue:[queues objectForKey:key]]; // !exceptions
2274 ASLogWarn(@"No Vim controller for identifier=%d", ukey);
2280 // If new input arrived while we were processing it would have been
2281 // blocked so we have to schedule it to be processed again.
2282 if (processingFlag < 0)
2283 [self performSelectorOnMainThread:@selector(processInputQueues:)
2286 modes:[NSArray arrayWithObjects:
2287 NSDefaultRunLoopMode,
2288 NSEventTrackingRunLoopMode, nil]];
2293 - (void)addVimController:(MMVimController *)vc
2295 ASLogDebug(@"Add Vim controller pid=%d id=%d", [vc pid], [vc identifier]);
2298 NSNumber *pidKey = [NSNumber numberWithInt:pid];
2300 if (preloadPid == pid) {
2301 // This controller was preloaded, so add it to the cache and
2302 // schedule another vim process to be preloaded.
2304 [vc setIsPreloading:YES];
2305 [cachedVimControllers addObject:vc];
2306 [self scheduleVimControllerPreloadAfterDelay:1];
2308 [vimControllers addObject:vc];
2310 id args = [pidArguments objectForKey:pidKey];
2311 if (args && [NSNull null] != args)
2312 [vc passArguments:args];
2314 // HACK! MacVim does not get activated if it is launched from the
2315 // terminal, so we forcibly activate here unless it is an untitled
2316 // window opening. Untitled windows are treated differently, else
2317 // MacVim would steal the focus if another app was activated while the
2318 // untitled window was loading.
2319 if (!args || args != [NSNull null])
2320 [self activateWhenNextWindowOpens];
2323 [pidArguments removeObjectForKey:pidKey];
2327 @end // MMAppController (Private)