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