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