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 workingDirectory:(NSString *)cwd;
112 - (NSArray *)filterFilesAndNotify:(NSArray *)files;
113 - (NSArray *)filterOpenFiles:(NSArray *)filenames
114 openFilesDict:(NSDictionary **)openFiles;
115 #if MM_HANDLE_XCODE_MOD_EVENT
116 - (void)handleXcodeModEvent:(NSAppleEventDescriptor *)event
117 replyEvent:(NSAppleEventDescriptor *)reply;
119 - (void)handleGetURLEvent:(NSAppleEventDescriptor *)event
120 replyEvent:(NSAppleEventDescriptor *)reply;
121 - (NSMutableDictionary *)extractArgumentsFromOdocEvent:
122 (NSAppleEventDescriptor *)desc;
123 - (void)scheduleVimControllerPreloadAfterDelay:(NSTimeInterval)delay;
124 - (void)cancelVimControllerPreloadRequests;
125 - (void)preloadVimController:(id)sender;
126 - (int)maxPreloadCacheSize;
127 - (MMVimController *)takeVimControllerFromCache;
128 - (void)clearPreloadCacheWithCount:(int)count;
129 - (void)rebuildPreloadCache;
130 - (NSDate *)rcFilesModificationDate;
131 - (BOOL)openVimControllerWithArguments:(NSDictionary *)arguments;
132 - (void)activateWhenNextWindowOpens;
133 - (void)startWatchingVimDir;
134 - (void)stopWatchingVimDir;
135 - (void)handleFSEvent;
136 - (void)loadDefaultFont;
137 - (int)executeInLoginShell:(NSString *)path arguments:(NSArray *)args;
138 - (void)reapChildProcesses:(id)sender;
139 - (void)processInputQueues:(id)sender;
140 - (void)addVimController:(MMVimController *)vc;
141 - (NSDictionary *)convertVimControllerArguments:(NSDictionary *)args
142 toCommandLine:(NSArray **)cmdline;
143 - (NSString *)workingDirectoryForArguments:(NSDictionary *)args;
144 - (NSScreen *)screenContainingPoint:(NSPoint)pt;
146 #ifdef MM_ENABLE_PLUGINS
147 - (void)removePlugInMenu;
148 - (void)addPlugInMenuToMenu:(NSMenu *)mainMenu;
154 #if (MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5)
156 fsEventCallback(ConstFSEventStreamRef streamRef,
157 void *clientCallBackInfo,
160 const FSEventStreamEventFlags eventFlags[],
161 const FSEventStreamEventId eventIds[])
163 [[MMAppController sharedInstance] handleFSEvent];
167 @implementation MMAppController
171 static BOOL initDone = NO;
172 if (initDone) return;
177 // HACK! The following user default must be reset, else Ctrl-q (or
178 // whichever key is specified by the default) will be blocked by the input
179 // manager (interpretKeyEvents: swallows that key). (We can't use
180 // NSUserDefaults since it only allows us to write to the registration
181 // domain and this preference has "higher precedence" than that so such a
182 // change would have no effect.)
183 CFPreferencesSetAppValue(CFSTR("NSQuotedKeystrokeBinding"),
185 kCFPreferencesCurrentApplication);
187 // Also disable NSRepeatCountBinding -- it is not enabled by default, but
188 // it does not make much sense to support it since Vim has its own way of
189 // dealing with repeat counts.
190 CFPreferencesSetAppValue(CFSTR("NSRepeatCountBinding"),
192 kCFPreferencesCurrentApplication);
194 NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:
195 [NSNumber numberWithBool:NO], MMNoWindowKey,
196 [NSNumber numberWithInt:64], MMTabMinWidthKey,
197 [NSNumber numberWithInt:6*64], MMTabMaxWidthKey,
198 [NSNumber numberWithInt:132], MMTabOptimumWidthKey,
199 [NSNumber numberWithBool:YES], MMShowAddTabButtonKey,
200 [NSNumber numberWithInt:2], MMTextInsetLeftKey,
201 [NSNumber numberWithInt:1], MMTextInsetRightKey,
202 [NSNumber numberWithInt:1], MMTextInsetTopKey,
203 [NSNumber numberWithInt:1], MMTextInsetBottomKey,
204 @"MMTypesetter", MMTypesetterKey,
205 [NSNumber numberWithFloat:1], MMCellWidthMultiplierKey,
206 [NSNumber numberWithFloat:-1], MMBaselineOffsetKey,
207 [NSNumber numberWithBool:YES], MMTranslateCtrlClickKey,
208 [NSNumber numberWithInt:0], MMOpenInCurrentWindowKey,
209 [NSNumber numberWithBool:NO], MMNoFontSubstitutionKey,
210 [NSNumber numberWithBool:YES], MMLoginShellKey,
211 [NSNumber numberWithInt:0], MMRendererKey,
212 [NSNumber numberWithInt:MMUntitledWindowAlways],
214 [NSNumber numberWithBool:NO], MMTexturedWindowKey,
215 [NSNumber numberWithBool:NO], MMZoomBothKey,
216 @"", MMLoginShellCommandKey,
217 @"", MMLoginShellArgumentKey,
218 [NSNumber numberWithBool:YES], MMDialogsTrackPwdKey,
219 #ifdef MM_ENABLE_PLUGINS
220 [NSNumber numberWithBool:YES], MMShowLeftPlugInContainerKey,
222 [NSNumber numberWithInt:3], MMOpenLayoutKey,
223 [NSNumber numberWithBool:NO], MMVerticalSplitKey,
224 [NSNumber numberWithInt:0], MMPreloadCacheSizeKey,
225 [NSNumber numberWithInt:0], MMLastWindowClosedBehaviorKey,
226 [NSNumber numberWithBool:YES], MMLoadDefaultFontKey,
227 #ifdef INCLUDE_OLD_IM_CODE
228 [NSNumber numberWithBool:YES], MMUseInlineImKey,
229 #endif // INCLUDE_OLD_IM_CODE
232 [[NSUserDefaults standardUserDefaults] registerDefaults:dict];
234 NSArray *types = [NSArray arrayWithObject:NSStringPboardType];
235 [NSApp registerServicesMenuSendTypes:types returnTypes:types];
237 // NOTE: Set the current directory to user's home directory, otherwise it
238 // will default to the root directory. (This matters since new Vim
239 // processes inherit MacVim's environment variables.)
240 [[NSFileManager defaultManager] changeCurrentDirectoryPath:
246 if (!(self = [super init])) return nil;
248 [self loadDefaultFont];
250 vimControllers = [NSMutableArray new];
251 cachedVimControllers = [NSMutableArray new];
253 pidArguments = [NSMutableDictionary new];
254 inputQueues = [NSMutableDictionary new];
256 #ifdef MM_ENABLE_PLUGINS
257 NSString *plugInTitle = NSLocalizedString(@"Plug-In",
258 @"Plug-In menu title");
259 plugInMenuItem = [[NSMenuItem alloc] initWithTitle:plugInTitle
262 NSMenu *submenu = [[NSMenu alloc] initWithTitle:plugInTitle];
263 [plugInMenuItem setSubmenu:submenu];
267 // NOTE: Do not use the default connection since the Logitech Control
268 // Center (LCC) input manager steals and this would cause MacVim to
269 // never open any windows. (This is a bug in LCC but since they are
270 // unlikely to fix it, we graciously give them the default connection.)
271 connection = [[NSConnection alloc] initWithReceivePort:[NSPort port]
273 [connection setRootObject:self];
274 [connection setRequestTimeout:MMRequestTimeout];
275 [connection setReplyTimeout:MMReplyTimeout];
277 // NOTE! If the name of the connection changes here it must also be
278 // updated in MMBackend.m.
279 NSString *name = [NSString stringWithFormat:@"%@-connection",
280 [[NSBundle mainBundle] bundlePath]];
281 if (![connection registerName:name]) {
282 ASLogCrit(@"Failed to register connection with name '%@'", name);
283 [connection release]; connection = nil;
293 [connection release]; connection = nil;
294 [inputQueues release]; inputQueues = nil;
295 [pidArguments release]; pidArguments = nil;
296 [vimControllers release]; vimControllers = nil;
297 [cachedVimControllers release]; cachedVimControllers = nil;
298 [openSelectionString release]; openSelectionString = nil;
299 [recentFilesMenuItem release]; recentFilesMenuItem = nil;
300 [defaultMainMenu release]; defaultMainMenu = nil;
301 #ifdef MM_ENABLE_PLUGINS
302 [plugInMenuItem release]; plugInMenuItem = nil;
304 [appMenuItemTemplate release]; appMenuItemTemplate = nil;
309 - (void)applicationWillFinishLaunching:(NSNotification *)notification
311 // Remember the default menu so that it can be restored if the user closes
312 // all editor windows.
313 defaultMainMenu = [[NSApp mainMenu] retain];
315 // Store a copy of the default app menu so we can use this as a template
316 // for all other menus. We make a copy here because the "Services" menu
317 // will not yet have been populated at this time. If we don't we get
318 // problems trying to set key equivalents later on because they might clash
319 // with items on the "Services" menu.
320 appMenuItemTemplate = [defaultMainMenu itemAtIndex:0];
321 appMenuItemTemplate = [appMenuItemTemplate copy];
323 // Set up the "Open Recent" menu. See
324 // http://lapcatsoftware.com/blog/2007/07/10/
325 // working-without-a-nib-part-5-open-recent-menu/
327 // http://www.cocoabuilder.com/archive/message/cocoa/2007/8/15/187793
328 // for more information.
330 // The menu itself is created in MainMenu.nib but we still seem to have to
331 // hack around a bit to get it to work. (This has to be done in
332 // applicationWillFinishLaunching at the latest, otherwise it doesn't
334 NSMenu *fileMenu = [defaultMainMenu findFileMenu];
336 int idx = [fileMenu indexOfItemWithAction:@selector(fileOpen:)];
337 if (idx >= 0 && idx+1 < [fileMenu numberOfItems])
339 recentFilesMenuItem = [fileMenu itemWithTitle:@"Open Recent"];
340 [[recentFilesMenuItem submenu] performSelector:@selector(_setMenuName:)
341 withObject:@"NSRecentDocumentsMenu"];
343 // Note: The "Recent Files" menu must be moved around since there is no
344 // -[NSApp setRecentFilesMenu:] method. We keep a reference to it to
345 // facilitate this move (see setMainMenu: below).
346 [recentFilesMenuItem retain];
349 #if MM_HANDLE_XCODE_MOD_EVENT
350 [[NSAppleEventManager sharedAppleEventManager]
352 andSelector:@selector(handleXcodeModEvent:replyEvent:)
357 // Register 'mvim://' URL handler
358 [[NSAppleEventManager sharedAppleEventManager]
360 andSelector:@selector(handleGetURLEvent:replyEvent:)
361 forEventClass:kInternetEventClass
362 andEventID:kAEGetURL];
364 // Disable the default Cocoa "Key Bindings" since they interfere with the
365 // way Vim handles keyboard input. Cocoa reads bindings from
366 // /System/Library/Frameworks/AppKit.framework/Resources/
367 // StandardKeyBinding.dict
369 // ~/Library/KeyBindings/DefaultKeyBinding.dict
370 // To avoid having the user accidentally break keyboard handling (by
371 // modifying the latter in some unexpected way) in MacVim we load our own
372 // key binding dictionary from Resource/KeyBinding.plist. We can't disable
373 // the bindings completely since it would break keyboard handling in
374 // dialogs so the our custom dictionary contains all the entries from the
377 // It is possible to disable key bindings completely by not calling
378 // interpretKeyEvents: in keyDown: but this also disables key bindings used
379 // by certain input methods. E.g. Ctrl-Shift-; would no longer work in
380 // the Kotoeri input manager.
382 // To solve this problem we access a private API and set the key binding
383 // dictionary to our own custom dictionary here. At this time Cocoa will
384 // have already read the above mentioned dictionaries so it (hopefully)
385 // won't try to change the key binding dictionary again after this point.
386 NSKeyBindingManager *mgr = [NSKeyBindingManager sharedKeyBindingManager];
387 NSBundle *mainBundle = [NSBundle mainBundle];
388 NSString *path = [mainBundle pathForResource:@"KeyBinding"
390 NSDictionary *dict = [NSDictionary dictionaryWithContentsOfFile:path];
392 [mgr setDictionary:dict];
394 ASLogNotice(@"Failed to override the Cocoa key bindings. Keyboard "
395 "input may behave strangely as a result (path=%@).", path);
399 - (void)applicationDidFinishLaunching:(NSNotification *)notification
401 [NSApp setServicesProvider:self];
402 #ifdef MM_ENABLE_PLUGINS
403 [[MMPlugInManager sharedManager] loadAllPlugIns];
406 if ([self maxPreloadCacheSize] > 0) {
407 [self scheduleVimControllerPreloadAfterDelay:2];
408 [self startWatchingVimDir];
411 ASLogInfo(@"MacVim finished launching");
414 - (BOOL)applicationShouldOpenUntitledFile:(NSApplication *)sender
416 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
417 NSAppleEventManager *aem = [NSAppleEventManager sharedAppleEventManager];
418 NSAppleEventDescriptor *desc = [aem currentAppleEvent];
420 // The user default MMUntitledWindow can be set to control whether an
421 // untitled window should open on 'Open' and 'Reopen' events.
422 int untitledWindowFlag = [ud integerForKey:MMUntitledWindowKey];
424 BOOL isAppOpenEvent = [desc eventID] == kAEOpenApplication;
425 if (isAppOpenEvent && (untitledWindowFlag & MMUntitledWindowOnOpen) == 0)
428 BOOL isAppReopenEvent = [desc eventID] == kAEReopenApplication;
430 && (untitledWindowFlag & MMUntitledWindowOnReopen) == 0)
433 // When a process is started from the command line, the 'Open' event may
434 // contain a parameter to surpress the opening of an untitled window.
435 desc = [desc paramDescriptorForKeyword:keyAEPropData];
436 desc = [desc paramDescriptorForKeyword:keyMMUntitledWindow];
437 if (desc && ![desc booleanValue])
440 // Never open an untitled window if there is at least one open window or if
441 // there are processes that are currently launching.
442 if ([vimControllers count] > 0 || [pidArguments count] > 0)
445 // NOTE! This way it possible to start the app with the command-line
446 // argument '-nowindow yes' and no window will be opened by default but
447 // this argument will only be heeded when the application is opening.
448 if (isAppOpenEvent && [ud boolForKey:MMNoWindowKey] == YES)
454 - (BOOL)applicationOpenUntitledFile:(NSApplication *)sender
456 ASLogDebug(@"Opening untitled window...");
457 [self newWindow:self];
461 - (void)application:(NSApplication *)sender openFiles:(NSArray *)filenames
463 ASLogInfo(@"Opening files %@", filenames);
465 // Extract ODB/Xcode/Spotlight parameters from the current Apple event,
466 // sort the filenames, and then let openFiles:withArguments: do the heavy
469 if (!(filenames && [filenames count] > 0))
472 // Sort filenames since the Finder doesn't take care in preserving the
473 // order in which files are selected anyway (and "sorted" is more
474 // predictable than "random").
475 if ([filenames count] > 1)
476 filenames = [filenames sortedArrayUsingSelector:
477 @selector(localizedCompare:)];
479 // Extract ODB/Xcode/Spotlight parameters from the current Apple event
480 NSMutableDictionary *arguments = [self extractArgumentsFromOdocEvent:
481 [[NSAppleEventManager sharedAppleEventManager] currentAppleEvent]];
483 if ([self openFiles:filenames withArguments:arguments]) {
484 [NSApp replyToOpenOrPrint:NSApplicationDelegateReplySuccess];
486 // TODO: Notify user of failure?
487 [NSApp replyToOpenOrPrint:NSApplicationDelegateReplyFailure];
491 - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender
493 return (MMTerminateWhenLastWindowClosed ==
494 [[NSUserDefaults standardUserDefaults]
495 integerForKey:MMLastWindowClosedBehaviorKey]);
498 - (NSApplicationTerminateReply)applicationShouldTerminate:
499 (NSApplication *)sender
501 // TODO: Follow Apple's guidelines for 'Graceful Application Termination'
502 // (in particular, allow user to review changes and save).
503 int reply = NSTerminateNow;
504 BOOL modifiedBuffers = NO;
506 // Go through windows, checking for modified buffers. (Each Vim process
507 // tells MacVim when any buffer has been modified and MacVim sets the
508 // 'documentEdited' flag of the window correspondingly.)
509 NSEnumerator *e = [[NSApp windows] objectEnumerator];
511 while ((window = [e nextObject])) {
512 if ([window isDocumentEdited]) {
513 modifiedBuffers = YES;
518 if (modifiedBuffers) {
519 NSAlert *alert = [[NSAlert alloc] init];
520 [alert setAlertStyle:NSWarningAlertStyle];
521 [alert addButtonWithTitle:NSLocalizedString(@"Quit",
523 [alert addButtonWithTitle:NSLocalizedString(@"Cancel",
525 [alert setMessageText:NSLocalizedString(@"Quit without saving?",
526 @"Quit dialog with changed buffers, title")];
527 [alert setInformativeText:NSLocalizedString(
528 @"There are modified buffers, "
529 "if you quit now all changes will be lost. Quit anyway?",
530 @"Quit dialog with changed buffers, text")];
532 if ([alert runModal] != NSAlertFirstButtonReturn)
533 reply = NSTerminateCancel;
537 // No unmodified buffers, but give a warning if there are multiple
538 // windows and/or tabs open.
539 int numWindows = [vimControllers count];
542 // Count the number of open tabs
543 e = [vimControllers objectEnumerator];
545 while ((vc = [e nextObject]))
546 numTabs += [[vc objectForVimStateKey:@"numTabs"] intValue];
548 if (numWindows > 1 || numTabs > 1) {
549 NSAlert *alert = [[NSAlert alloc] init];
550 [alert setAlertStyle:NSWarningAlertStyle];
551 [alert addButtonWithTitle:NSLocalizedString(@"Quit",
553 [alert addButtonWithTitle:NSLocalizedString(@"Cancel",
555 [alert setMessageText:NSLocalizedString(
556 @"Are you sure you want to quit MacVim?",
557 @"Quit dialog with no changed buffers, title")];
559 NSString *info = nil;
560 if (numWindows > 1) {
561 if (numTabs > numWindows)
562 info = [NSString stringWithFormat:NSLocalizedString(
563 @"There are %d windows open in MacVim, with a "
564 "total of %d tabs. Do you want to quit anyway?",
565 @"Quit dialog with no changed buffers, text"),
566 numWindows, numTabs];
568 info = [NSString stringWithFormat:NSLocalizedString(
569 @"There are %d windows open in MacVim. "
570 "Do you want to quit anyway?",
571 @"Quit dialog with no changed buffers, text"),
575 info = [NSString stringWithFormat:NSLocalizedString(
576 @"There are %d tabs open in MacVim. "
577 "Do you want to quit anyway?",
578 @"Quit dialog with no changed buffers, text"),
582 [alert setInformativeText:info];
584 if ([alert runModal] != NSAlertFirstButtonReturn)
585 reply = NSTerminateCancel;
592 // Tell all Vim processes to terminate now (otherwise they'll leave swap
594 if (NSTerminateNow == reply) {
595 e = [vimControllers objectEnumerator];
597 while ((vc = [e nextObject])) {
598 ASLogDebug(@"Terminate pid=%d", [vc pid]);
599 [vc sendMessage:TerminateNowMsgID data:nil];
602 e = [cachedVimControllers objectEnumerator];
603 while ((vc = [e nextObject])) {
604 ASLogDebug(@"Terminate pid=%d (cached)", [vc pid]);
605 [vc sendMessage:TerminateNowMsgID data:nil];
608 // If a Vim process is being preloaded as we quit we have to forcibly
609 // kill it since we have not established a connection yet.
610 if (preloadPid > 0) {
611 ASLogDebug(@"Kill incomplete preloaded process pid=%d", preloadPid);
612 kill(preloadPid, SIGKILL);
615 // If a Vim process was loading as we quit we also have to kill it.
616 e = [[pidArguments allKeys] objectEnumerator];
618 while ((pidKey = [e nextObject])) {
619 ASLogDebug(@"Kill incomplete process pid=%d", [pidKey intValue]);
620 kill([pidKey intValue], SIGKILL);
623 // Sleep a little to allow all the Vim processes to exit.
630 - (void)applicationWillTerminate:(NSNotification *)notification
632 ASLogInfo(@"Terminating MacVim...");
634 [self stopWatchingVimDir];
636 #ifdef MM_ENABLE_PLUGINS
637 [[MMPlugInManager sharedManager] unloadAllPlugIns];
640 #if MM_HANDLE_XCODE_MOD_EVENT
641 [[NSAppleEventManager sharedAppleEventManager]
642 removeEventHandlerForEventClass:'KAHL'
646 // This will invalidate all connections (since they were spawned from this
648 [connection invalidate];
650 // Deactivate the font we loaded from the app bundle.
651 // NOTE: This can take quite a while (~500 ms), so termination will be
652 // noticeably faster if loading of the default font is disabled.
653 if (fontContainerRef) {
654 ATSFontDeactivate(fontContainerRef, NULL, kATSOptionFlagsDefault);
655 fontContainerRef = 0;
658 [NSApp setDelegate:nil];
660 // Try to wait for all child processes to avoid leaving zombies behind (but
661 // don't wait around for too long).
662 NSDate *timeOutDate = [NSDate dateWithTimeIntervalSinceNow:2];
663 while ([timeOutDate timeIntervalSinceNow] > 0) {
664 [self reapChildProcesses:nil];
665 if (numChildProcesses <= 0)
668 ASLogDebug(@"%d processes still left, hold on...", numChildProcesses);
670 // Run in NSConnectionReplyMode while waiting instead of calling e.g.
671 // usleep(). Otherwise incoming messages may clog up the DO queues and
672 // the outgoing TerminateNowMsgID sent earlier never reaches the Vim
674 // This has at least one side-effect, namely we may receive the
675 // annoying "dropping incoming DO message". (E.g. this may happen if
676 // you quickly hit Cmd-n several times in a row and then immediately
677 // press Cmd-q, Enter.)
678 while (CFRunLoopRunInMode((CFStringRef)NSConnectionReplyMode,
679 0.05, true) == kCFRunLoopRunHandledSource)
683 if (numChildProcesses > 0) {
684 ASLogNotice(@"%d zombies left behind", numChildProcesses);
688 + (MMAppController *)sharedInstance
690 // Note: The app controller is a singleton which is instantiated in
691 // MainMenu.nib where it is also connected as the delegate of NSApp.
692 id delegate = [NSApp delegate];
693 return [delegate isKindOfClass:self] ? (MMAppController*)delegate : nil;
696 - (NSMenu *)defaultMainMenu
698 return defaultMainMenu;
701 - (NSMenuItem *)appMenuItemTemplate
703 return appMenuItemTemplate;
706 - (void)removeVimController:(id)controller
708 ASLogDebug(@"Remove Vim controller pid=%d id=%d (processingFlag=%d)",
709 [controller pid], [controller vimControllerId], processingFlag);
711 NSUInteger idx = [vimControllers indexOfObject:controller];
712 if (NSNotFound == idx) {
713 ASLogDebug(@"Controller not found, probably due to duplicate removal");
718 [vimControllers removeObjectAtIndex:idx];
719 [controller cleanup];
720 [controller release];
722 if (![vimControllers count]) {
723 // The last editor window just closed so restore the main menu back to
724 // its default state (which is defined in MainMenu.nib).
725 [self setMainMenu:defaultMainMenu];
727 BOOL hide = (MMHideWhenLastWindowClosed ==
728 [[NSUserDefaults standardUserDefaults]
729 integerForKey:MMLastWindowClosedBehaviorKey]);
734 // There is a small delay before the Vim process actually exits so wait a
735 // little before trying to reap the child process. If the process still
736 // hasn't exited after this wait it won't be reaped until the next time
737 // reapChildProcesses: is called (but this should be harmless).
738 [self performSelector:@selector(reapChildProcesses:)
743 - (void)windowControllerWillOpen:(MMWindowController *)windowController
745 NSPoint topLeft = NSZeroPoint;
746 NSWindow *cascadeFrom = [[[self topmostVimController] windowController]
748 NSWindow *win = [windowController window];
752 // Heuristic to determine where to position the window:
753 // 1. Use the default top left position (set using :winpos in .[g]vimrc)
754 // 2. Cascade from an existing window
755 // 3. Use autosaved position
756 // If all of the above fail, then the window position is not changed.
757 if ([windowController getDefaultTopLeft:&topLeft]) {
758 // Make sure the window is not cascaded (note that topLeft was set in
761 } else if (cascadeFrom) {
762 NSRect frame = [cascadeFrom frame];
763 topLeft = NSMakePoint(frame.origin.x, NSMaxY(frame));
765 NSString *topLeftString = [[NSUserDefaults standardUserDefaults]
766 stringForKey:MMTopLeftPointKey];
768 topLeft = NSPointFromString(topLeftString);
771 if (!NSEqualPoints(topLeft, NSZeroPoint)) {
772 // Try to tile from the correct screen in case the user has multiple
773 // monitors ([win screen] always seems to return the "main" screen).
774 NSScreen *screen = [self screenContainingPoint:topLeft];
776 screen = [win screen];
779 // Do manual cascading instead of using
780 // -[MMWindow cascadeTopLeftFromPoint:] since it is rather
782 topLeft.x += MMCascadeHorizontalOffset;
783 topLeft.y -= MMCascadeVerticalOffset;
787 // Constrain the window so that it is entirely visible on the
788 // screen. If it sticks out on the right, move it all the way
789 // left. If it sticks out on the bottom, move it all the way up.
790 // (Assumption: the cascading offsets are positive.)
791 NSRect screenFrame = [screen frame];
792 NSSize winSize = [win frame].size;
794 { { topLeft.x, topLeft.y - winSize.height }, winSize };
796 if (NSMaxX(winFrame) > NSMaxX(screenFrame))
797 topLeft.x = NSMinX(screenFrame);
798 if (NSMinY(winFrame) < NSMinY(screenFrame))
799 topLeft.y = NSMaxY(screenFrame);
801 ASLogNotice(@"Window not on screen, don't constrain position");
804 [win setFrameTopLeftPoint:topLeft];
807 if (1 == [vimControllers count]) {
808 // The first window autosaves its position. (The autosaving
809 // features of Cocoa are not used because we need more control over
810 // what is autosaved and when it is restored.)
811 [windowController setWindowAutosaveKey:MMTopLeftPointKey];
814 if (openSelectionString) {
815 // TODO: Pass this as a parameter instead! Get rid of
816 // 'openSelectionString' etc.
818 // There is some text to paste into this window as a result of the
819 // services menu "Open selection ..." being used.
820 [[windowController vimController] dropString:openSelectionString];
821 [openSelectionString release];
822 openSelectionString = nil;
825 if (shouldActivateWhenNextWindowOpens) {
826 [NSApp activateIgnoringOtherApps:YES];
827 shouldActivateWhenNextWindowOpens = NO;
831 - (void)setMainMenu:(NSMenu *)mainMenu
833 if ([NSApp mainMenu] == mainMenu) return;
835 // If the new menu has a "Recent Files" dummy item, then swap the real item
836 // for the dummy. We are forced to do this since Cocoa initializes the
837 // "Recent Files" menu and there is no way to simply point Cocoa to a new
838 // item each time the menus are swapped.
839 NSMenu *fileMenu = [mainMenu findFileMenu];
840 if (recentFilesMenuItem && fileMenu) {
842 [fileMenu indexOfItemWithAction:@selector(recentFilesDummy:)];
844 NSMenuItem *dummyItem = [[fileMenu itemAtIndex:dummyIdx] retain];
845 [fileMenu removeItemAtIndex:dummyIdx];
847 NSMenu *recentFilesParentMenu = [recentFilesMenuItem menu];
848 int idx = [recentFilesParentMenu indexOfItem:recentFilesMenuItem];
850 [[recentFilesMenuItem retain] autorelease];
851 [recentFilesParentMenu removeItemAtIndex:idx];
852 [recentFilesParentMenu insertItem:dummyItem atIndex:idx];
855 [fileMenu insertItem:recentFilesMenuItem atIndex:dummyIdx];
860 // Now set the new menu. Notice that we keep one menu for each editor
861 // window since each editor can have its own set of menus. When swapping
862 // menus we have to tell Cocoa where the new "MacVim", "Windows", and
863 // "Services" menu are.
864 [NSApp setMainMenu:mainMenu];
866 // Setting the "MacVim" (or "Application") menu ensures that it is typeset
867 // in boldface. (The setAppleMenu: method used to be public but is now
868 // private so this will have to be considered a bit of a hack!)
869 NSMenu *appMenu = [mainMenu findApplicationMenu];
870 [NSApp performSelector:@selector(setAppleMenu:) withObject:appMenu];
872 NSMenu *servicesMenu = [mainMenu findServicesMenu];
873 [NSApp setServicesMenu:servicesMenu];
875 NSMenu *windowsMenu = [mainMenu findWindowsMenu];
877 // Cocoa isn't clever enough to get rid of items it has added to the
878 // "Windows" menu so we have to do it ourselves otherwise there will be
879 // multiple menu items for each window in the "Windows" menu.
880 // This code assumes that the only items Cocoa add are ones which
881 // send off the action makeKeyAndOrderFront:. (Cocoa will not add
882 // another separator item if the last item on the "Windows" menu
883 // already is a separator, so we needen't worry about separators.)
884 int i, count = [windowsMenu numberOfItems];
885 for (i = count-1; i >= 0; --i) {
886 NSMenuItem *item = [windowsMenu itemAtIndex:i];
887 if ([item action] == @selector(makeKeyAndOrderFront:))
888 [windowsMenu removeItem:item];
891 [NSApp setWindowsMenu:windowsMenu];
893 #ifdef MM_ENABLE_PLUGINS
894 // Move plugin menu from old to new main menu.
895 [self removePlugInMenu];
896 [self addPlugInMenuToMenu:mainMenu];
900 - (NSArray *)filterOpenFiles:(NSArray *)filenames
902 return [self filterOpenFiles:filenames openFilesDict:nil];
905 - (BOOL)openFiles:(NSArray *)filenames withArguments:(NSDictionary *)args
907 // Opening files works like this:
908 // a) filter out any already open files
909 // b) open any remaining files
911 // A file is opened in an untitled window if there is one (it may be
912 // currently launching, or it may already be visible), otherwise a new
915 // Each launching Vim process has a dictionary of arguments that are passed
916 // to the process when in checks in (via connectBackend:pid:). The
917 // arguments for each launching process can be looked up by its PID (in the
918 // pidArguments dictionary).
920 NSMutableDictionary *arguments = (args ? [[args mutableCopy] autorelease]
921 : [NSMutableDictionary dictionary]);
923 filenames = normalizeFilenames(filenames);
926 // a) Filter out any already open files
928 NSString *firstFile = [filenames objectAtIndex:0];
929 MMVimController *firstController = nil;
930 NSDictionary *openFilesDict = nil;
931 filenames = [self filterOpenFiles:filenames openFilesDict:&openFilesDict];
933 // Pass arguments to vim controllers that had files open.
935 NSEnumerator *e = [openFilesDict keyEnumerator];
937 // (Indicate that we do not wish to open any files at the moment.)
938 [arguments setObject:[NSNumber numberWithBool:YES] forKey:@"dontOpen"];
940 while ((key = [e nextObject])) {
941 NSArray *files = [openFilesDict objectForKey:key];
942 [arguments setObject:files forKey:@"filenames"];
944 MMVimController *vc = [key pointerValue];
945 [vc passArguments:arguments];
947 // If this controller holds the first file, then remember it for later.
948 if ([files containsObject:firstFile])
949 firstController = vc;
952 // The meaning of "layout" is defined by the WIN_* defines in main.c.
953 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
954 int layout = [ud integerForKey:MMOpenLayoutKey];
955 BOOL splitVert = [ud boolForKey:MMVerticalSplitKey];
956 BOOL openInCurrentWindow = [ud boolForKey:MMOpenInCurrentWindowKey];
958 if (splitVert && MMLayoutHorizontalSplit == layout)
959 layout = MMLayoutVerticalSplit;
960 if (layout < 0 || (layout > MMLayoutTabs && openInCurrentWindow))
961 layout = MMLayoutTabs;
963 if ([filenames count] == 0) {
964 // Raise the window containing the first file that was already open,
965 // and make sure that the tab containing that file is selected. Only
966 // do this when there are no more files to open, otherwise sometimes
967 // the window with 'firstFile' will be raised, other times it might be
968 // the window that will open with the files in the 'filenames' array.
969 firstFile = [firstFile stringByEscapingSpecialFilenameCharacters];
971 NSString *bufCmd = @"tab sb";
973 case MMLayoutHorizontalSplit: bufCmd = @"sb"; break;
974 case MMLayoutVerticalSplit: bufCmd = @"vert sb"; break;
975 case MMLayoutArglist: bufCmd = @"b"; break;
978 NSString *input = [NSString stringWithFormat:@"<C-\\><C-N>"
979 ":let oldswb=&swb|let &swb=\"useopen,usetab\"|"
980 "%@ %@|let &swb=oldswb|unl oldswb|"
981 "cal foreground()<CR>", bufCmd, firstFile];
983 [firstController addVimInput:input];
988 // Add filenames to "Recent Files" menu, unless they are being edited
989 // remotely (using ODB).
990 if ([arguments objectForKey:@"remoteID"] == nil) {
991 [[NSDocumentController sharedDocumentController]
992 noteNewRecentFilePaths:filenames];
996 // b) Open any remaining files
999 [arguments setObject:[NSNumber numberWithInt:layout] forKey:@"layout"];
1000 [arguments setObject:filenames forKey:@"filenames"];
1001 // (Indicate that files should be opened from now on.)
1002 [arguments setObject:[NSNumber numberWithBool:NO] forKey:@"dontOpen"];
1004 MMVimController *vc;
1005 if (openInCurrentWindow && (vc = [self topmostVimController])) {
1006 // Open files in an already open window.
1007 [[[vc windowController] window] makeKeyAndOrderFront:self];
1008 [vc passArguments:arguments];
1013 int numFiles = [filenames count];
1014 if (MMLayoutWindows == layout && numFiles > 1) {
1015 // Open one file at a time in a new window, but don't open too many at
1016 // once (at most cap+1 windows will open). If the user has increased
1017 // the preload cache size we'll take that as a hint that more windows
1018 // should be able to open at once.
1019 int cap = [self maxPreloadCacheSize] - 1;
1020 if (cap < 4) cap = 4;
1021 if (cap > numFiles) cap = numFiles;
1024 for (i = 0; i < cap; ++i) {
1025 NSArray *a = [NSArray arrayWithObject:[filenames objectAtIndex:i]];
1026 [arguments setObject:a forKey:@"filenames"];
1028 // NOTE: We have to copy the args since we'll mutate them in the
1029 // next loop and the below call may retain the arguments while
1030 // waiting for a process to start.
1031 NSDictionary *args = [[arguments copy] autorelease];
1033 openOk = [self openVimControllerWithArguments:args];
1037 // Open remaining files in tabs in a new window.
1038 if (openOk && numFiles > cap) {
1039 NSRange range = { i, numFiles-cap };
1040 NSArray *a = [filenames subarrayWithRange:range];
1041 [arguments setObject:a forKey:@"filenames"];
1042 [arguments setObject:[NSNumber numberWithInt:MMLayoutTabs]
1045 openOk = [self openVimControllerWithArguments:arguments];
1048 // Open all files at once.
1049 openOk = [self openVimControllerWithArguments:arguments];
1055 #ifdef MM_ENABLE_PLUGINS
1056 - (void)addItemToPlugInMenu:(NSMenuItem *)item
1058 NSMenu *menu = [plugInMenuItem submenu];
1059 [menu addItem:item];
1060 if ([menu numberOfItems] == 1)
1061 [self addPlugInMenuToMenu:[NSApp mainMenu]];
1064 - (void)removeItemFromPlugInMenu:(NSMenuItem *)item
1066 NSMenu *menu = [plugInMenuItem submenu];
1067 [menu removeItem:item];
1068 if ([menu numberOfItems] == 0)
1069 [self removePlugInMenu];
1073 - (IBAction)newWindow:(id)sender
1075 ASLogDebug(@"Open new window");
1077 // A cached controller requires no loading times and results in the new
1078 // window popping up instantaneously. If the cache is empty it may take
1079 // 1-2 seconds to start a new Vim process.
1080 MMVimController *vc = [self takeVimControllerFromCache];
1082 [[vc backendProxy] acknowledgeConnection];
1084 [self launchVimProcessWithArguments:nil workingDirectory:nil];
1088 - (IBAction)newWindowAndActivate:(id)sender
1090 [self activateWhenNextWindowOpens];
1091 [self newWindow:sender];
1094 - (IBAction)fileOpen:(id)sender
1096 ASLogDebug(@"Show file open panel");
1098 NSString *dir = nil;
1099 BOOL trackPwd = [[NSUserDefaults standardUserDefaults]
1100 boolForKey:MMDialogsTrackPwdKey];
1102 MMVimController *vc = [self keyVimController];
1103 if (vc) dir = [vc objectForVimStateKey:@"pwd"];
1106 NSOpenPanel *panel = [NSOpenPanel openPanel];
1107 [panel setAllowsMultipleSelection:YES];
1108 [panel setAccessoryView:showHiddenFilesView()];
1110 int result = [panel runModalForDirectory:dir file:nil types:nil];
1111 if (NSOKButton == result)
1112 [self application:NSApp openFiles:[panel filenames]];
1115 - (IBAction)selectNextWindow:(id)sender
1117 ASLogDebug(@"Select next window");
1119 unsigned i, count = [vimControllers count];
1122 NSWindow *keyWindow = [NSApp keyWindow];
1123 for (i = 0; i < count; ++i) {
1124 MMVimController *vc = [vimControllers objectAtIndex:i];
1125 if ([[[vc windowController] window] isEqual:keyWindow])
1132 MMVimController *vc = [vimControllers objectAtIndex:i];
1133 [[vc windowController] showWindow:self];
1137 - (IBAction)selectPreviousWindow:(id)sender
1139 ASLogDebug(@"Select previous window");
1141 unsigned i, count = [vimControllers count];
1144 NSWindow *keyWindow = [NSApp keyWindow];
1145 for (i = 0; i < count; ++i) {
1146 MMVimController *vc = [vimControllers objectAtIndex:i];
1147 if ([[[vc windowController] window] isEqual:keyWindow])
1157 MMVimController *vc = [vimControllers objectAtIndex:i];
1158 [[vc windowController] showWindow:self];
1162 - (IBAction)orderFrontPreferencePanel:(id)sender
1164 ASLogDebug(@"Show preferences panel");
1165 [[MMPreferenceController sharedPrefsWindowController] showWindow:self];
1168 - (IBAction)openWebsite:(id)sender
1170 ASLogDebug(@"Open MacVim website");
1171 [[NSWorkspace sharedWorkspace] openURL:
1172 [NSURL URLWithString:MMWebsiteString]];
1175 - (IBAction)showVimHelp:(id)sender
1177 ASLogDebug(@"Open window with Vim help");
1178 // Open a new window with the help window maximized.
1179 [self launchVimProcessWithArguments:[NSArray arrayWithObjects:
1180 @"-c", @":h gui_mac", @"-c", @":res", nil]
1181 workingDirectory:nil];
1184 - (IBAction)zoomAll:(id)sender
1186 ASLogDebug(@"Zoom all windows");
1187 [NSApp makeWindowsPerform:@selector(performZoom:) inOrder:YES];
1190 - (IBAction)atsuiButtonClicked:(id)sender
1192 ASLogDebug(@"Toggle ATSUI renderer");
1193 NSInteger renderer = MMRendererDefault;
1194 BOOL enable = ([sender state] == NSOnState);
1198 renderer = MMRendererATSUI;
1200 renderer = MMRendererCoreText;
1204 // Update the user default MMRenderer and synchronize the change so that
1205 // any new Vim process will pick up on the changed setting.
1206 CFPreferencesSetAppValue(
1207 (CFStringRef)MMRendererKey,
1208 (CFPropertyListRef)[NSNumber numberWithInt:renderer],
1209 kCFPreferencesCurrentApplication);
1210 CFPreferencesAppSynchronize(kCFPreferencesCurrentApplication);
1212 ASLogInfo(@"Use renderer=%d", renderer);
1214 // This action is called when the user clicks the "use ATSUI renderer"
1215 // button in the advanced preferences pane.
1216 [self rebuildPreloadCache];
1219 - (IBAction)loginShellButtonClicked:(id)sender
1221 ASLogDebug(@"Toggle login shell option");
1222 // This action is called when the user clicks the "use login shell" button
1223 // in the advanced preferences pane.
1224 [self rebuildPreloadCache];
1227 - (IBAction)quickstartButtonClicked:(id)sender
1229 ASLogDebug(@"Toggle Quickstart option");
1230 if ([self maxPreloadCacheSize] > 0) {
1231 [self scheduleVimControllerPreloadAfterDelay:1.0];
1232 [self startWatchingVimDir];
1234 [self cancelVimControllerPreloadRequests];
1235 [self clearPreloadCacheWithCount:-1];
1236 [self stopWatchingVimDir];
1240 - (MMVimController *)keyVimController
1242 NSWindow *keyWindow = [NSApp keyWindow];
1244 unsigned i, count = [vimControllers count];
1245 for (i = 0; i < count; ++i) {
1246 MMVimController *vc = [vimControllers objectAtIndex:i];
1247 if ([[[vc windowController] window] isEqual:keyWindow])
1255 - (unsigned)connectBackend:(byref in id <MMBackendProtocol>)proxy pid:(int)pid
1257 ASLogDebug(@"pid=%d", pid);
1259 [(NSDistantObject*)proxy setProtocolForProxy:@protocol(MMBackendProtocol)];
1261 // NOTE: Allocate the vim controller now but don't add it to the list of
1262 // controllers since this is a distributed object call and as such can
1263 // arrive at unpredictable times (e.g. while iterating the list of vim
1265 // (What if input arrives before the vim controller is added to the list of
1266 // controllers? This should not be a problem since the input isn't
1267 // processed immediately (see processInput:forIdentifier:).)
1268 // Also, since the app may be multithreaded (e.g. as a result of showing
1269 // the open panel) we have to ensure this call happens on the main thread,
1270 // else there is a race condition that may lead to a crash.
1271 MMVimController *vc = [[MMVimController alloc] initWithBackend:proxy
1273 [self performSelectorOnMainThread:@selector(addVimController:)
1276 modes:[NSArray arrayWithObject:
1277 NSDefaultRunLoopMode]];
1281 return [vc vimControllerId];
1284 - (oneway void)processInput:(in bycopy NSArray *)queue
1285 forIdentifier:(unsigned)identifier
1287 // NOTE: Input is not handled immediately since this is a distributed
1288 // object call and as such can arrive at unpredictable times. Instead,
1289 // queue the input and process it when the run loop is updated.
1291 if (!(queue && identifier)) {
1292 ASLogWarn(@"Bad input for identifier=%d", identifier);
1296 ASLogDebug(@"QUEUE for identifier=%d: <<< %@>>>", identifier,
1297 debugStringForMessageQueue(queue));
1299 NSNumber *key = [NSNumber numberWithUnsignedInt:identifier];
1300 NSArray *q = [inputQueues objectForKey:key];
1302 q = [q arrayByAddingObjectsFromArray:queue];
1303 [inputQueues setObject:q forKey:key];
1305 [inputQueues setObject:queue forKey:key];
1308 // NOTE: We must use "event tracking mode" as well as "default mode",
1309 // otherwise the input queue will not be processed e.g. during live
1311 // Also, since the app may be multithreaded (e.g. as a result of showing
1312 // the open panel) we have to ensure this call happens on the main thread,
1313 // else there is a race condition that may lead to a crash.
1314 [self performSelectorOnMainThread:@selector(processInputQueues:)
1317 modes:[NSArray arrayWithObjects:
1318 NSDefaultRunLoopMode,
1319 NSEventTrackingRunLoopMode, nil]];
1322 - (NSArray *)serverList
1324 NSMutableArray *array = [NSMutableArray array];
1326 unsigned i, count = [vimControllers count];
1327 for (i = 0; i < count; ++i) {
1328 MMVimController *controller = [vimControllers objectAtIndex:i];
1329 if ([controller serverName])
1330 [array addObject:[controller serverName]];
1336 @end // MMAppController
1341 @implementation MMAppController (MMServices)
1343 - (void)openSelection:(NSPasteboard *)pboard userData:(NSString *)userData
1344 error:(NSString **)error
1346 if (![[pboard types] containsObject:NSStringPboardType]) {
1347 ASLogNotice(@"Pasteboard contains no NSStringPboardType");
1351 ASLogInfo(@"Open new window containing current selection");
1353 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
1354 BOOL openInCurrentWindow = [ud boolForKey:MMOpenInCurrentWindowKey];
1355 MMVimController *vc;
1357 if (openInCurrentWindow && (vc = [self topmostVimController])) {
1358 [vc sendMessage:AddNewTabMsgID data:nil];
1359 [vc dropString:[pboard stringForType:NSStringPboardType]];
1361 // Save the text, open a new window, and paste the text when the next
1362 // window opens. (If this is called several times in a row, then all
1363 // but the last call may be ignored.)
1364 if (openSelectionString) [openSelectionString release];
1365 openSelectionString = [[pboard stringForType:NSStringPboardType] copy];
1367 [self newWindow:self];
1371 - (void)openFile:(NSPasteboard *)pboard userData:(NSString *)userData
1372 error:(NSString **)error
1374 if (![[pboard types] containsObject:NSStringPboardType]) {
1375 ASLogNotice(@"Pasteboard contains no NSStringPboardType");
1379 // TODO: Parse multiple filenames and create array with names.
1380 NSString *string = [pboard stringForType:NSStringPboardType];
1381 string = [string stringByTrimmingCharactersInSet:
1382 [NSCharacterSet whitespaceAndNewlineCharacterSet]];
1383 string = [string stringByStandardizingPath];
1385 ASLogInfo(@"Open new window with selected file: %@", string);
1387 NSArray *filenames = [self filterFilesAndNotify:
1388 [NSArray arrayWithObject:string]];
1389 if ([filenames count] == 0)
1392 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
1393 BOOL openInCurrentWindow = [ud boolForKey:MMOpenInCurrentWindowKey];
1394 MMVimController *vc;
1396 if (openInCurrentWindow && (vc = [self topmostVimController])) {
1397 [vc dropFiles:filenames forceOpen:YES];
1399 [self openFiles:filenames withArguments:nil];
1403 - (void)newFileHere:(NSPasteboard *)pboard userData:(NSString *)userData
1404 error:(NSString **)error
1406 if (![[pboard types] containsObject:NSFilenamesPboardType]) {
1407 ASLogNotice(@"Pasteboard contains no NSFilenamesPboardType");
1411 NSArray *filenames = [pboard propertyListForType:NSFilenamesPboardType];
1412 NSString *path = [filenames lastObject];
1415 if (![[NSFileManager defaultManager] fileExistsAtPath:path
1416 isDirectory:&dirIndicator]) {
1417 ASLogNotice(@"Invalid path. Cannot open new document at: %@", path);
1421 ASLogInfo(@"Open new file at path=%@", path);
1424 path = [path stringByDeletingLastPathComponent];
1426 path = [path stringByEscapingSpecialFilenameCharacters];
1428 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
1429 BOOL openInCurrentWindow = [ud boolForKey:MMOpenInCurrentWindowKey];
1430 MMVimController *vc;
1432 if (openInCurrentWindow && (vc = [self topmostVimController])) {
1433 NSString *input = [NSString stringWithFormat:@"<C-\\><C-N>"
1434 ":tabe|cd %@<CR>", path];
1435 [vc addVimInput:input];
1437 [self launchVimProcessWithArguments:nil workingDirectory:path];
1441 @end // MMAppController (MMServices)
1446 @implementation MMAppController (Private)
1448 - (MMVimController *)topmostVimController
1450 // Find the topmost visible window which has an associated vim controller.
1451 NSEnumerator *e = [[NSApp orderedWindows] objectEnumerator];
1453 while ((window = [e nextObject]) && [window isVisible]) {
1454 unsigned i, count = [vimControllers count];
1455 for (i = 0; i < count; ++i) {
1456 MMVimController *vc = [vimControllers objectAtIndex:i];
1457 if ([[[vc windowController] window] isEqual:window])
1465 - (int)launchVimProcessWithArguments:(NSArray *)args
1466 workingDirectory:(NSString *)cwd
1469 NSString *path = [[NSBundle mainBundle] pathForAuxiliaryExecutable:@"Vim"];
1472 ASLogCrit(@"Vim executable could not be found inside app bundle!");
1476 // Change current working directory so that the child process picks it up.
1477 NSFileManager *fm = [NSFileManager defaultManager];
1478 NSString *restoreCwd = nil;
1480 restoreCwd = [fm currentDirectoryPath];
1481 [fm changeCurrentDirectoryPath:cwd];
1484 NSArray *taskArgs = [NSArray arrayWithObjects:@"-g", @"-f", nil];
1486 taskArgs = [taskArgs arrayByAddingObjectsFromArray:args];
1488 BOOL useLoginShell = [[NSUserDefaults standardUserDefaults]
1489 boolForKey:MMLoginShellKey];
1490 if (useLoginShell) {
1491 // Run process with a login shell, roughly:
1492 // echo "exec Vim -g -f args" | ARGV0=-`basename $SHELL` $SHELL [-l]
1493 pid = [self executeInLoginShell:path arguments:taskArgs];
1495 // Run process directly:
1497 NSTask *task = [NSTask launchedTaskWithLaunchPath:path
1498 arguments:taskArgs];
1499 pid = task ? [task processIdentifier] : -1;
1503 // The 'pidArguments' dictionary keeps arguments to be passed to the
1504 // process when it connects (this is in contrast to arguments which are
1505 // passed on the command line, like '-f' and '-g').
1506 // If this method is called with nil arguments we take this as a hint
1507 // that this is an "untitled window" being launched and add a null
1508 // object to the 'pidArguments' dictionary. This way we can detect if
1509 // an untitled window is being launched by looking for null objects in
1511 // If this method is called with non-nil arguments then it is assumed
1512 // that the caller takes care of adding items to 'pidArguments' as
1513 // necessary (only some arguments are passed on connect, e.g. files to
1516 [pidArguments setObject:[NSNull null]
1517 forKey:[NSNumber numberWithInt:pid]];
1519 ASLogWarn(@"Failed to launch Vim process: args=%@, useLoginShell=%d",
1520 args, useLoginShell);
1523 // Now that child has launched, restore the current working directory.
1525 [fm changeCurrentDirectoryPath:restoreCwd];
1530 - (NSArray *)filterFilesAndNotify:(NSArray *)filenames
1532 // Go trough 'filenames' array and make sure each file exists. Present
1533 // warning dialog if some file was missing.
1535 NSString *firstMissingFile = nil;
1536 NSMutableArray *files = [NSMutableArray array];
1537 unsigned i, count = [filenames count];
1539 for (i = 0; i < count; ++i) {
1540 NSString *name = [filenames objectAtIndex:i];
1541 if ([[NSFileManager defaultManager] fileExistsAtPath:name]) {
1542 [files addObject:name];
1543 } else if (!firstMissingFile) {
1544 firstMissingFile = name;
1548 if (firstMissingFile) {
1549 NSAlert *alert = [[NSAlert alloc] init];
1550 [alert addButtonWithTitle:NSLocalizedString(@"OK",
1554 if ([files count] >= count-1) {
1555 [alert setMessageText:NSLocalizedString(@"File not found",
1556 @"File not found dialog, title")];
1557 text = [NSString stringWithFormat:NSLocalizedString(
1558 @"Could not open file with name %@.",
1559 @"File not found dialog, text"), firstMissingFile];
1561 [alert setMessageText:NSLocalizedString(@"Multiple files not found",
1562 @"File not found dialog, title")];
1563 text = [NSString stringWithFormat:NSLocalizedString(
1564 @"Could not open file with name %@, and %d other files.",
1565 @"File not found dialog, text"),
1566 firstMissingFile, count-[files count]-1];
1569 [alert setInformativeText:text];
1570 [alert setAlertStyle:NSWarningAlertStyle];
1575 [NSApp replyToOpenOrPrint:NSApplicationDelegateReplyFailure];
1581 - (NSArray *)filterOpenFiles:(NSArray *)filenames
1582 openFilesDict:(NSDictionary **)openFiles
1584 // Filter out any files in the 'filenames' array that are open and return
1585 // all files that are not already open. On return, the 'openFiles'
1586 // parameter (if non-nil) will point to a dictionary of open files, indexed
1587 // by Vim controller.
1589 NSMutableDictionary *dict = [NSMutableDictionary dictionary];
1590 NSMutableArray *files = [filenames mutableCopy];
1592 // TODO: Escape special characters in 'files'?
1593 NSString *expr = [NSString stringWithFormat:
1594 @"map([\"%@\"],\"bufloaded(v:val)\")",
1595 [files componentsJoinedByString:@"\",\""]];
1597 unsigned i, count = [vimControllers count];
1598 for (i = 0; i < count && [files count] > 0; ++i) {
1599 MMVimController *vc = [vimControllers objectAtIndex:i];
1601 // Query Vim for which files in the 'files' array are open.
1602 NSString *eval = [vc evaluateVimExpression:expr];
1603 if (!eval) continue;
1605 NSIndexSet *idxSet = [NSIndexSet indexSetWithVimList:eval];
1606 if ([idxSet count] > 0) {
1607 [dict setObject:[files objectsAtIndexes:idxSet]
1608 forKey:[NSValue valueWithPointer:vc]];
1610 // Remove all the files that were open in this Vim process and
1611 // create a new expression to evaluate.
1612 [files removeObjectsAtIndexes:idxSet];
1613 expr = [NSString stringWithFormat:
1614 @"map([\"%@\"],\"bufloaded(v:val)\")",
1615 [files componentsJoinedByString:@"\",\""]];
1619 if (openFiles != nil)
1622 return [files autorelease];
1625 #if MM_HANDLE_XCODE_MOD_EVENT
1626 - (void)handleXcodeModEvent:(NSAppleEventDescriptor *)event
1627 replyEvent:(NSAppleEventDescriptor *)reply
1630 // Xcode sends this event to query MacVim which open files have been
1632 ASLogDebug(@"reply:%@", reply);
1633 ASLogDebug(@"event:%@", event);
1635 NSEnumerator *e = [vimControllers objectEnumerator];
1637 while ((vc = [e nextObject])) {
1638 DescType type = [reply descriptorType];
1639 unsigned len = [[type data] length];
1640 NSMutableData *data = [NSMutableData data];
1642 [data appendBytes:&type length:sizeof(DescType)];
1643 [data appendBytes:&len length:sizeof(unsigned)];
1644 [data appendBytes:[reply data] length:len];
1646 [vc sendMessage:XcodeModMsgID data:data];
1652 - (void)handleGetURLEvent:(NSAppleEventDescriptor *)event
1653 replyEvent:(NSAppleEventDescriptor *)reply
1655 NSString *urlString = [[event paramDescriptorForKeyword:keyDirectObject]
1657 NSURL *url = [NSURL URLWithString:urlString];
1659 // We try to be compatible with TextMate's URL scheme here, as documented
1660 // at http://blog.macromates.com/2007/the-textmate-url-scheme/ . Currently,
1663 // The format is: mvim://open?<arguments> where arguments can be:
1665 // * url — the actual file to open (i.e. a file://… URL), if you leave
1666 // out this argument, the frontmost document is implied.
1667 // * line — line number to go to (one based).
1668 // * column — column number to go to (one based).
1670 // Example: mvim://open?url=file:///etc/profile&line=20
1672 if ([[url host] isEqualToString:@"open"]) {
1673 NSMutableDictionary *dict = [NSMutableDictionary dictionary];
1675 // Parse query ("url=file://...&line=14") into a dictionary
1676 NSArray *queries = [[url query] componentsSeparatedByString:@"&"];
1677 NSEnumerator *enumerator = [queries objectEnumerator];
1679 while ((param = [enumerator nextObject])) {
1680 NSArray *arr = [param componentsSeparatedByString:@"="];
1681 if ([arr count] == 2) {
1682 [dict setValue:[[arr lastObject]
1683 stringByReplacingPercentEscapesUsingEncoding:
1684 NSUTF8StringEncoding]
1685 forKey:[[arr objectAtIndex:0]
1686 stringByReplacingPercentEscapesUsingEncoding:
1687 NSUTF8StringEncoding]];
1691 // Actually open the file.
1692 NSString *file = [dict objectForKey:@"url"];
1694 NSURL *fileUrl= [NSURL URLWithString:file];
1695 // TextMate only opens files that already exist.
1696 if ([fileUrl isFileURL]
1697 && [[NSFileManager defaultManager] fileExistsAtPath:
1699 // Strip 'file://' path, else application:openFiles: might think
1700 // the file is not yet open.
1701 NSArray *filenames = [NSArray arrayWithObject:[fileUrl path]];
1703 // Look for the line and column options.
1704 NSDictionary *args = nil;
1705 NSString *line = [dict objectForKey:@"line"];
1707 NSString *column = [dict objectForKey:@"column"];
1709 args = [NSDictionary dictionaryWithObjectsAndKeys:
1710 line, @"cursorLine",
1711 column, @"cursorColumn",
1714 args = [NSDictionary dictionaryWithObject:line
1715 forKey:@"cursorLine"];
1718 [self openFiles:filenames withArguments:args];
1722 NSAlert *alert = [[NSAlert alloc] init];
1723 [alert addButtonWithTitle:NSLocalizedString(@"OK",
1726 [alert setMessageText:NSLocalizedString(@"Unknown URL Scheme",
1727 @"Unknown URL Scheme dialog, title")];
1728 [alert setInformativeText:[NSString stringWithFormat:NSLocalizedString(
1729 @"This version of MacVim does not support \"%@\""
1730 @" in its URL scheme.",
1731 @"Unknown URL Scheme dialog, text"),
1734 [alert setAlertStyle:NSWarningAlertStyle];
1740 - (NSMutableDictionary *)extractArgumentsFromOdocEvent:
1741 (NSAppleEventDescriptor *)desc
1743 NSMutableDictionary *dict = [NSMutableDictionary dictionary];
1745 // 1. Extract ODB parameters (if any)
1746 NSAppleEventDescriptor *odbdesc = desc;
1747 if (![odbdesc paramDescriptorForKeyword:keyFileSender]) {
1748 // The ODB paramaters may hide inside the 'keyAEPropData' descriptor.
1749 odbdesc = [odbdesc paramDescriptorForKeyword:keyAEPropData];
1750 if (![odbdesc paramDescriptorForKeyword:keyFileSender])
1755 NSAppleEventDescriptor *p =
1756 [odbdesc paramDescriptorForKeyword:keyFileSender];
1758 [dict setObject:[NSNumber numberWithUnsignedInt:[p typeCodeValue]]
1759 forKey:@"remoteID"];
1761 p = [odbdesc paramDescriptorForKeyword:keyFileCustomPath];
1763 [dict setObject:[p stringValue] forKey:@"remotePath"];
1765 p = [odbdesc paramDescriptorForKeyword:keyFileSenderToken];
1767 [dict setObject:[NSNumber numberWithUnsignedLong:[p descriptorType]]
1768 forKey:@"remoteTokenDescType"];
1769 [dict setObject:[p data] forKey:@"remoteTokenData"];
1773 // 2. Extract Xcode parameters (if any)
1774 NSAppleEventDescriptor *xcodedesc =
1775 [desc paramDescriptorForKeyword:keyAEPosition];
1778 NSData *data = [xcodedesc data];
1779 NSUInteger length = [data length];
1781 if (length == sizeof(MMXcodeSelectionRange)) {
1782 MMXcodeSelectionRange *sr = (MMXcodeSelectionRange*)[data bytes];
1783 ASLogDebug(@"Xcode selection range (%d,%d,%d,%d,%d,%d)",
1784 sr->unused1, sr->lineNum, sr->startRange, sr->endRange,
1785 sr->unused2, sr->theDate);
1787 if (sr->lineNum < 0) {
1788 // Should select a range of lines.
1789 range.location = sr->startRange + 1;
1790 range.length = sr->endRange - sr->startRange + 1;
1792 // Should only move cursor to a line.
1793 range.location = sr->lineNum + 1;
1797 [dict setObject:NSStringFromRange(range) forKey:@"selectionRange"];
1799 ASLogErr(@"Xcode selection range size mismatch! got=%d expected=%d",
1800 length, sizeof(MMXcodeSelectionRange));
1804 // 3. Extract Spotlight search text (if any)
1805 NSAppleEventDescriptor *spotlightdesc =
1806 [desc paramDescriptorForKeyword:keyAESearchText];
1808 [dict setObject:[spotlightdesc stringValue] forKey:@"searchText"];
1813 #ifdef MM_ENABLE_PLUGINS
1814 - (void)removePlugInMenu
1816 if ([plugInMenuItem menu])
1817 [[plugInMenuItem menu] removeItem:plugInMenuItem];
1820 - (void)addPlugInMenuToMenu:(NSMenu *)mainMenu
1822 NSMenu *windowsMenu = [mainMenu findWindowsMenu];
1824 if ([[plugInMenuItem submenu] numberOfItems] > 0) {
1825 int idx = windowsMenu ? [mainMenu indexOfItemWithSubmenu:windowsMenu]
1828 [mainMenu insertItem:plugInMenuItem atIndex:idx];
1830 [mainMenu addItem:plugInMenuItem];
1836 - (void)scheduleVimControllerPreloadAfterDelay:(NSTimeInterval)delay
1838 [self performSelector:@selector(preloadVimController:)
1843 - (void)cancelVimControllerPreloadRequests
1845 [NSObject cancelPreviousPerformRequestsWithTarget:self
1846 selector:@selector(preloadVimController:)
1850 - (void)preloadVimController:(id)sender
1852 // We only allow preloading of one Vim process at a time (to avoid hogging
1853 // CPU), so schedule another preload in a little while if necessary.
1854 if (-1 != preloadPid) {
1855 [self scheduleVimControllerPreloadAfterDelay:2];
1859 if ([cachedVimControllers count] >= [self maxPreloadCacheSize])
1862 preloadPid = [self launchVimProcessWithArguments:
1863 [NSArray arrayWithObject:@"--mmwaitforack"]
1864 workingDirectory:nil];
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 takeVimControllerFromCache];
2024 // Open files in a new window using a cached vim controller. This
2025 // requires virtually no loading time so the new window will pop up
2027 [vc passArguments:arguments];
2028 [[vc backendProxy] acknowledgeConnection];
2030 NSArray *cmdline = nil;
2031 NSString *cwd = [self workingDirectoryForArguments:arguments];
2032 arguments = [self convertVimControllerArguments:arguments
2033 toCommandLine:&cmdline];
2034 int pid = [self launchVimProcessWithArguments:cmdline
2035 workingDirectory:cwd];
2039 // TODO: If the Vim process fails to start, or if it changes PID,
2040 // then the memory allocated for these parameters will leak.
2041 // Ensure that this cannot happen or somehow detect it.
2043 if ([arguments count] > 0)
2044 [pidArguments setObject:arguments
2045 forKey:[NSNumber numberWithInt:pid]];
2051 - (void)activateWhenNextWindowOpens
2053 ASLogDebug(@"Activate MacVim when next window opens");
2054 shouldActivateWhenNextWindowOpens = YES;
2057 - (void)startWatchingVimDir
2059 #if (MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5)
2062 if (NULL == FSEventStreamStart)
2063 return; // FSEvent functions are weakly linked
2065 NSString *path = [@"~/.vim" stringByExpandingTildeInPath];
2066 NSArray *pathsToWatch = [NSArray arrayWithObject:path];
2068 fsEventStream = FSEventStreamCreate(NULL, &fsEventCallback, NULL,
2069 (CFArrayRef)pathsToWatch, kFSEventStreamEventIdSinceNow,
2070 MMEventStreamLatency, kFSEventStreamCreateFlagNone);
2072 FSEventStreamScheduleWithRunLoop(fsEventStream,
2073 [[NSRunLoop currentRunLoop] getCFRunLoop],
2074 kCFRunLoopDefaultMode);
2076 FSEventStreamStart(fsEventStream);
2077 ASLogDebug(@"Started FS event stream");
2081 - (void)stopWatchingVimDir
2083 #if (MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5)
2084 if (NULL == FSEventStreamStop)
2085 return; // FSEvent functions are weakly linked
2087 if (fsEventStream) {
2088 FSEventStreamStop(fsEventStream);
2089 FSEventStreamInvalidate(fsEventStream);
2090 FSEventStreamRelease(fsEventStream);
2091 fsEventStream = NULL;
2092 ASLogDebug(@"Stopped FS event stream");
2098 - (void)handleFSEvent
2100 [self clearPreloadCacheWithCount:-1];
2102 // Several FS events may arrive in quick succession so make sure to cancel
2103 // any previous preload requests before making a new one.
2104 [self cancelVimControllerPreloadRequests];
2105 [self scheduleVimControllerPreloadAfterDelay:0.5];
2108 - (void)loadDefaultFont
2110 // It is possible to set a user default to avoid loading the default font
2111 // (this cuts down on startup time).
2112 if (![[NSUserDefaults standardUserDefaults] boolForKey:MMLoadDefaultFontKey]
2113 || fontContainerRef) {
2114 ASLogInfo(@"Skip loading of the default font...");
2118 ASLogInfo(@"Loading the default font...");
2120 // Load all fonts in the Resouces folder of the app bundle.
2121 NSString *fontsFolder = [[NSBundle mainBundle] resourcePath];
2123 NSURL *fontsURL = [NSURL fileURLWithPath:fontsFolder];
2126 CFURLGetFSRef((CFURLRef)fontsURL, &fsRef);
2128 #if (MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5)
2129 // This is the font activation API for OS X 10.5. Only compile
2130 // this code if we're building on OS X 10.5 or later.
2131 if (NULL != ATSFontActivateFromFileReference) { // Weakly linked
2132 ATSFontActivateFromFileReference(&fsRef, kATSFontContextLocal,
2133 kATSFontFormatUnspecified,
2134 NULL, kATSOptionFlagsDefault,
2138 #if (MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_5)
2139 // The following font activation API was deprecated in OS X 10.5.
2140 // Don't compile this code unless we're targeting OS X 10.4.
2142 if (fontContainerRef == 0 &&
2143 FSGetCatalogInfo(&fsRef, kFSCatInfoNone, NULL, NULL,
2144 &fsSpec, NULL) == noErr) {
2145 ATSFontActivateFromFileSpecification(&fsSpec,
2146 kATSFontContextLocal, kATSFontFormatUnspecified, NULL,
2147 kATSOptionFlagsDefault, &fontContainerRef);
2153 if (!fontContainerRef) {
2154 ASLogNotice(@"Failed to activate the default font (the app bundle "
2155 "may be incomplete)");
2159 - (int)executeInLoginShell:(NSString *)path arguments:(NSArray *)args
2161 // Start a login shell and execute the command 'path' with arguments 'args'
2162 // in the shell. This ensures that user environment variables are set even
2163 // when MacVim was started from the Finder.
2166 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
2168 // Determine which shell to use to execute the command. The user
2169 // may decide which shell to use by setting a user default or the
2170 // $SHELL environment variable.
2171 NSString *shell = [ud stringForKey:MMLoginShellCommandKey];
2172 if (!shell || [shell length] == 0)
2173 shell = [[[NSProcessInfo processInfo] environment]
2174 objectForKey:@"SHELL"];
2176 shell = @"/bin/bash";
2178 // Bash needs the '-l' flag to launch a login shell. The user may add
2179 // flags by setting a user default.
2180 NSString *shellArgument = [ud stringForKey:MMLoginShellArgumentKey];
2181 if (!shellArgument || [shellArgument length] == 0) {
2182 if ([[shell lastPathComponent] isEqual:@"bash"])
2183 shellArgument = @"-l";
2185 shellArgument = nil;
2188 // Build input string to pipe to the login shell.
2189 NSMutableString *input = [NSMutableString stringWithFormat:
2190 @"exec \"%@\"", path];
2192 // Append all arguments, making sure they are properly quoted, even
2193 // when they contain single quotes.
2194 NSEnumerator *e = [args objectEnumerator];
2197 while ((obj = [e nextObject])) {
2198 NSMutableString *arg = [NSMutableString stringWithString:obj];
2199 [arg replaceOccurrencesOfString:@"'" withString:@"'\"'\"'"
2200 options:NSLiteralSearch
2201 range:NSMakeRange(0, [arg length])];
2202 [input appendFormat:@" '%@'", arg];
2206 // Build the argument vector used to start the login shell.
2207 NSString *shellArg0 = [NSString stringWithFormat:@"-%@",
2208 [shell lastPathComponent]];
2209 char *shellArgv[3] = { (char *)[shellArg0 UTF8String], NULL, NULL };
2211 shellArgv[1] = (char *)[shellArgument UTF8String];
2213 // Get the C string representation of the shell path before the fork since
2214 // we must not call Foundation functions after a fork.
2215 const char *shellPath = [shell fileSystemRepresentation];
2217 // Fork and execute the process.
2219 if (pipe(ds)) return -1;
2224 } else if (pid == 0) {
2227 if (close(ds[1]) == -1) exit(255);
2228 if (dup2(ds[0], 0) == -1) exit(255);
2230 // Without the following call warning messages like this appear on the
2232 // com.apple.launchd[69] : Stray process with PGID equal to this
2233 // dead job: PID 1589 PPID 1 Vim
2236 execv(shellPath, shellArgv);
2238 // Never reached unless execv fails
2242 if (close(ds[0]) == -1) return -1;
2244 // Send input to execute to the child process
2245 [input appendString:@"\n"];
2246 int bytes = [input lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
2248 if (write(ds[1], [input UTF8String], bytes) != bytes) return -1;
2249 if (close(ds[1]) == -1) return -1;
2251 ++numChildProcesses;
2252 ASLogDebug(@"new process pid=%d (count=%d)", pid, numChildProcesses);
2258 - (void)reapChildProcesses:(id)sender
2260 // NOTE: numChildProcesses (currently) only counts the number of Vim
2261 // processes that have been started with executeInLoginShell::. If other
2262 // processes are spawned this code may need to be adjusted (or
2263 // numChildProcesses needs to be incremented when such a process is
2265 while (numChildProcesses > 0) {
2267 int pid = waitpid(-1, &status, WNOHANG);
2271 ASLogDebug(@"Wait for pid=%d complete", pid);
2272 --numChildProcesses;
2276 - (void)processInputQueues:(id)sender
2278 // NOTE: Because we use distributed objects it is quite possible for this
2279 // function to be re-entered. This can cause all sorts of unexpected
2280 // problems so we guard against it here so that the rest of the code does
2281 // not need to worry about it.
2283 // The processing flag is > 0 if this function is already on the call
2284 // stack; < 0 if this function was also re-entered.
2285 if (processingFlag != 0) {
2286 ASLogDebug(@"BUSY!");
2287 processingFlag = -1;
2291 // NOTE: Be _very_ careful that no exceptions can be raised between here
2292 // and the point at which 'processingFlag' is reset. Otherwise the above
2293 // test could end up always failing and no input queues would ever be
2297 // NOTE: New input may arrive while we're busy processing; we deal with
2298 // this by putting the current queue aside and creating a new input queue
2299 // for future input.
2300 NSDictionary *queues = inputQueues;
2301 inputQueues = [NSMutableDictionary new];
2303 // Pass each input queue on to the vim controller with matching
2304 // identifier (and note that it could be cached).
2305 NSEnumerator *e = [queues keyEnumerator];
2307 while ((key = [e nextObject])) {
2308 unsigned ukey = [key unsignedIntValue];
2309 int i = 0, count = [vimControllers count];
2310 for (i = 0; i < count; ++i) {
2311 MMVimController *vc = [vimControllers objectAtIndex:i];
2312 if (ukey == [vc vimControllerId]) {
2313 [vc processInputQueue:[queues objectForKey:key]]; // !exceptions
2318 if (i < count) continue;
2320 count = [cachedVimControllers count];
2321 for (i = 0; i < count; ++i) {
2322 MMVimController *vc = [cachedVimControllers objectAtIndex:i];
2323 if (ukey == [vc vimControllerId]) {
2324 [vc processInputQueue:[queues objectForKey:key]]; // !exceptions
2330 ASLogWarn(@"No Vim controller for identifier=%d", ukey);
2336 // If new input arrived while we were processing it would have been
2337 // blocked so we have to schedule it to be processed again.
2338 if (processingFlag < 0)
2339 [self performSelectorOnMainThread:@selector(processInputQueues:)
2342 modes:[NSArray arrayWithObjects:
2343 NSDefaultRunLoopMode,
2344 NSEventTrackingRunLoopMode, nil]];
2349 - (void)addVimController:(MMVimController *)vc
2351 ASLogDebug(@"Add Vim controller pid=%d id=%d",
2352 [vc pid], [vc vimControllerId]);
2355 NSNumber *pidKey = [NSNumber numberWithInt:pid];
2357 if (preloadPid == pid) {
2358 // This controller was preloaded, so add it to the cache and
2359 // schedule another vim process to be preloaded.
2361 [vc setIsPreloading:YES];
2362 [cachedVimControllers addObject:vc];
2363 [self scheduleVimControllerPreloadAfterDelay:1];
2365 [vimControllers addObject:vc];
2367 id args = [pidArguments objectForKey:pidKey];
2368 if (args && [NSNull null] != args)
2369 [vc passArguments:args];
2371 // HACK! MacVim does not get activated if it is launched from the
2372 // terminal, so we forcibly activate here unless it is an untitled
2373 // window opening. Untitled windows are treated differently, else
2374 // MacVim would steal the focus if another app was activated while the
2375 // untitled window was loading.
2376 if (!args || args != [NSNull null])
2377 [self activateWhenNextWindowOpens];
2380 [pidArguments removeObjectForKey:pidKey];
2384 - (NSDictionary *)convertVimControllerArguments:(NSDictionary *)args
2385 toCommandLine:(NSArray **)cmdline
2387 // Take all arguments out of 'args' and put them on an array suitable to
2388 // pass as arguments to launchVimProcessWithArguments:. The untouched
2389 // dictionary items are returned in a new autoreleased dictionary.
2394 NSArray *filenames = [args objectForKey:@"filenames"];
2395 int numFiles = filenames ? [filenames count] : 0;
2396 BOOL openFiles = ![[args objectForKey:@"dontOpen"] boolValue];
2398 if (numFiles <= 0 || !openFiles)
2401 NSMutableArray *a = [NSMutableArray array];
2402 NSMutableDictionary *d = [[args mutableCopy] autorelease];
2404 // Search for text using "+/text".
2405 NSString *searchText = [args objectForKey:@"searchText"];
2407 // TODO: If the search pattern is not found an error is shown when
2408 // starting. Figure out a way to get rid of this message (The help
2409 // says to use ':silent exe "normal /pat\<CR>"' but this does not
2411 [a addObject:[NSString stringWithFormat:@"+/%@", searchText]];
2413 [d removeObjectForKey:@"searchText"];
2416 // Position cursor using "+line" or "-c :cal cursor(line,column)".
2417 NSString *lineString = [args objectForKey:@"cursorLine"];
2418 if (lineString && [lineString intValue] > 0) {
2419 NSString *columnString = [args objectForKey:@"cursorColumn"];
2420 if (columnString && [columnString intValue] > 0) {
2421 [a addObject:@"-c"];
2422 [a addObject:[NSString stringWithFormat:@":cal cursor(%@,%@)",
2423 lineString, columnString]];
2425 [d removeObjectForKey:@"cursorColumn"];
2427 [a addObject:[NSString stringWithFormat:@"+%@", lineString]];
2430 [d removeObjectForKey:@"cursorLine"];
2433 // Set selection using normal mode commands.
2434 NSString *rangeString = [args objectForKey:@"selectionRange"];
2436 NSRange r = NSRangeFromString(rangeString);
2437 [a addObject:@"-c"];
2439 // Select given range.
2440 [a addObject:[NSString stringWithFormat:@"norm %dGV%dGz.0",
2441 NSMaxRange(r), r.location]];
2443 // Position cursor on start of range.
2444 [a addObject:[NSString stringWithFormat:@"norm %dGz.0",
2448 [d removeObjectForKey:@"selectionRange"];
2451 // Choose file layout using "-[o|O|p]".
2452 int layout = [[args objectForKey:@"layout"] intValue];
2454 case MMLayoutHorizontalSplit: [a addObject:@"-o"]; break;
2455 case MMLayoutVerticalSplit: [a addObject:@"-O"]; break;
2456 case MMLayoutTabs: [a addObject:@"-p"]; break;
2458 [d removeObjectForKey:@"layout"];
2461 // Last of all add the names of all files to open (DO NOT add more args
2462 // after this point).
2463 [a addObjectsFromArray:filenames];
2465 if ([args objectForKey:@"remoteID"]) {
2466 // These files should be edited remotely so keep the filenames on the
2467 // argument list -- they will need to be passed back to Vim when it
2468 // checks in. Also set the 'dontOpen' flag or the files will be
2470 [d setObject:[NSNumber numberWithBool:YES] forKey:@"dontOpen"];
2472 [d removeObjectForKey:@"dontOpen"];
2473 [d removeObjectForKey:@"filenames"];
2482 - (NSString *)workingDirectoryForArguments:(NSDictionary *)args
2484 // Find the "filenames" argument and pick the first path that actually
2485 // exists and return it.
2486 // TODO: Return common parent directory in the case of multiple files?
2487 NSFileManager *fm = [NSFileManager defaultManager];
2488 NSArray *filenames = [args objectForKey:@"filenames"];
2489 NSUInteger i, count = [filenames count];
2490 for (i = 0; i < count; ++i) {
2492 NSString *file = [filenames objectAtIndex:i];
2493 if ([fm fileExistsAtPath:file isDirectory:&isdir])
2494 return isdir ? file : [file stringByDeletingLastPathComponent];
2500 - (NSScreen *)screenContainingPoint:(NSPoint)pt
2502 NSArray *screens = [NSScreen screens];
2503 NSUInteger i, count = [screens count];
2504 for (i = 0; i < count; ++i) {
2505 NSScreen *screen = [screens objectAtIndex:i];
2506 NSRect frame = [screen frame];
2507 if (NSPointInRect(pt, frame))
2514 @end // MMAppController (Private)