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 *topWin = [[[self topmostVimController] windowController] window];
747 NSWindow *win = [windowController window];
751 // If there is a window belonging to a Vim process, cascade from it,
752 // otherwise use the autosaved window position (if any).
754 NSRect frame = [topWin frame];
755 topLeft = NSMakePoint(frame.origin.x, NSMaxY(frame));
757 NSString *topLeftString = [[NSUserDefaults standardUserDefaults]
758 stringForKey:MMTopLeftPointKey];
760 topLeft = NSPointFromString(topLeftString);
763 if (!NSEqualPoints(topLeft, NSZeroPoint)) {
764 // Try to tile from the correct screen in case the user has multiple
765 // monitors ([win screen] always seems to return the "main" screen).
766 NSScreen *screen = [self screenContainingPoint:topLeft];
768 screen = [win screen];
771 // Do manual cascading instead of using
772 // -[MMWindow cascadeTopLeftFromPoint:] since it is rather
774 topLeft.x += MMCascadeHorizontalOffset;
775 topLeft.y -= MMCascadeVerticalOffset;
779 // Constrain the window so that it is entirely visible on the
780 // screen. If it sticks out on the right, move it all the way
781 // left. If it sticks out on the bottom, move it all the way up.
782 // (Assumption: the cascading offsets are positive.)
783 NSRect screenFrame = [screen frame];
784 NSSize winSize = [win frame].size;
786 { { topLeft.x, topLeft.y - winSize.height }, winSize };
788 if (NSMaxX(winFrame) > NSMaxX(screenFrame))
789 topLeft.x = NSMinX(screenFrame);
790 if (NSMinY(winFrame) < NSMinY(screenFrame))
791 topLeft.y = NSMaxY(screenFrame);
793 ASLogNotice(@"Window not on screen, don't constrain position");
796 [win setFrameTopLeftPoint:topLeft];
799 if (1 == [vimControllers count]) {
800 // The first window autosaves its position. (The autosaving
801 // features of Cocoa are not used because we need more control over
802 // what is autosaved and when it is restored.)
803 [windowController setWindowAutosaveKey:MMTopLeftPointKey];
806 if (openSelectionString) {
807 // TODO: Pass this as a parameter instead! Get rid of
808 // 'openSelectionString' etc.
810 // There is some text to paste into this window as a result of the
811 // services menu "Open selection ..." being used.
812 [[windowController vimController] dropString:openSelectionString];
813 [openSelectionString release];
814 openSelectionString = nil;
817 if (shouldActivateWhenNextWindowOpens) {
818 [NSApp activateIgnoringOtherApps:YES];
819 shouldActivateWhenNextWindowOpens = NO;
823 - (void)setMainMenu:(NSMenu *)mainMenu
825 if ([NSApp mainMenu] == mainMenu) return;
827 // If the new menu has a "Recent Files" dummy item, then swap the real item
828 // for the dummy. We are forced to do this since Cocoa initializes the
829 // "Recent Files" menu and there is no way to simply point Cocoa to a new
830 // item each time the menus are swapped.
831 NSMenu *fileMenu = [mainMenu findFileMenu];
832 if (recentFilesMenuItem && fileMenu) {
834 [fileMenu indexOfItemWithAction:@selector(recentFilesDummy:)];
836 NSMenuItem *dummyItem = [[fileMenu itemAtIndex:dummyIdx] retain];
837 [fileMenu removeItemAtIndex:dummyIdx];
839 NSMenu *recentFilesParentMenu = [recentFilesMenuItem menu];
840 int idx = [recentFilesParentMenu indexOfItem:recentFilesMenuItem];
842 [[recentFilesMenuItem retain] autorelease];
843 [recentFilesParentMenu removeItemAtIndex:idx];
844 [recentFilesParentMenu insertItem:dummyItem atIndex:idx];
847 [fileMenu insertItem:recentFilesMenuItem atIndex:dummyIdx];
852 // Now set the new menu. Notice that we keep one menu for each editor
853 // window since each editor can have its own set of menus. When swapping
854 // menus we have to tell Cocoa where the new "MacVim", "Windows", and
855 // "Services" menu are.
856 [NSApp setMainMenu:mainMenu];
858 // Setting the "MacVim" (or "Application") menu ensures that it is typeset
859 // in boldface. (The setAppleMenu: method used to be public but is now
860 // private so this will have to be considered a bit of a hack!)
861 NSMenu *appMenu = [mainMenu findApplicationMenu];
862 [NSApp performSelector:@selector(setAppleMenu:) withObject:appMenu];
864 NSMenu *servicesMenu = [mainMenu findServicesMenu];
865 [NSApp setServicesMenu:servicesMenu];
867 NSMenu *windowsMenu = [mainMenu findWindowsMenu];
869 // Cocoa isn't clever enough to get rid of items it has added to the
870 // "Windows" menu so we have to do it ourselves otherwise there will be
871 // multiple menu items for each window in the "Windows" menu.
872 // This code assumes that the only items Cocoa add are ones which
873 // send off the action makeKeyAndOrderFront:. (Cocoa will not add
874 // another separator item if the last item on the "Windows" menu
875 // already is a separator, so we needen't worry about separators.)
876 int i, count = [windowsMenu numberOfItems];
877 for (i = count-1; i >= 0; --i) {
878 NSMenuItem *item = [windowsMenu itemAtIndex:i];
879 if ([item action] == @selector(makeKeyAndOrderFront:))
880 [windowsMenu removeItem:item];
883 [NSApp setWindowsMenu:windowsMenu];
885 #ifdef MM_ENABLE_PLUGINS
886 // Move plugin menu from old to new main menu.
887 [self removePlugInMenu];
888 [self addPlugInMenuToMenu:mainMenu];
892 - (NSArray *)filterOpenFiles:(NSArray *)filenames
894 return [self filterOpenFiles:filenames openFilesDict:nil];
897 - (BOOL)openFiles:(NSArray *)filenames withArguments:(NSDictionary *)args
899 // Opening files works like this:
900 // a) filter out any already open files
901 // b) open any remaining files
903 // A file is opened in an untitled window if there is one (it may be
904 // currently launching, or it may already be visible), otherwise a new
907 // Each launching Vim process has a dictionary of arguments that are passed
908 // to the process when in checks in (via connectBackend:pid:). The
909 // arguments for each launching process can be looked up by its PID (in the
910 // pidArguments dictionary).
912 NSMutableDictionary *arguments = (args ? [[args mutableCopy] autorelease]
913 : [NSMutableDictionary dictionary]);
915 filenames = normalizeFilenames(filenames);
918 // a) Filter out any already open files
920 NSString *firstFile = [filenames objectAtIndex:0];
921 MMVimController *firstController = nil;
922 NSDictionary *openFilesDict = nil;
923 filenames = [self filterOpenFiles:filenames openFilesDict:&openFilesDict];
925 // Pass arguments to vim controllers that had files open.
927 NSEnumerator *e = [openFilesDict keyEnumerator];
929 // (Indicate that we do not wish to open any files at the moment.)
930 [arguments setObject:[NSNumber numberWithBool:YES] forKey:@"dontOpen"];
932 while ((key = [e nextObject])) {
933 NSArray *files = [openFilesDict objectForKey:key];
934 [arguments setObject:files forKey:@"filenames"];
936 MMVimController *vc = [key pointerValue];
937 [vc passArguments:arguments];
939 // If this controller holds the first file, then remember it for later.
940 if ([files containsObject:firstFile])
941 firstController = vc;
944 // The meaning of "layout" is defined by the WIN_* defines in main.c.
945 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
946 int layout = [ud integerForKey:MMOpenLayoutKey];
947 BOOL splitVert = [ud boolForKey:MMVerticalSplitKey];
948 BOOL openInCurrentWindow = [ud boolForKey:MMOpenInCurrentWindowKey];
950 if (splitVert && MMLayoutHorizontalSplit == layout)
951 layout = MMLayoutVerticalSplit;
952 if (layout < 0 || (layout > MMLayoutTabs && openInCurrentWindow))
953 layout = MMLayoutTabs;
955 if ([filenames count] == 0) {
956 // Raise the window containing the first file that was already open,
957 // and make sure that the tab containing that file is selected. Only
958 // do this when there are no more files to open, otherwise sometimes
959 // the window with 'firstFile' will be raised, other times it might be
960 // the window that will open with the files in the 'filenames' array.
961 firstFile = [firstFile stringByEscapingSpecialFilenameCharacters];
963 NSString *bufCmd = @"tab sb";
965 case MMLayoutHorizontalSplit: bufCmd = @"sb"; break;
966 case MMLayoutVerticalSplit: bufCmd = @"vert sb"; break;
967 case MMLayoutArglist: bufCmd = @"b"; break;
970 NSString *input = [NSString stringWithFormat:@"<C-\\><C-N>"
971 ":let oldswb=&swb|let &swb=\"useopen,usetab\"|"
972 "%@ %@|let &swb=oldswb|unl oldswb|"
973 "cal foreground()<CR>", bufCmd, firstFile];
975 [firstController addVimInput:input];
980 // Add filenames to "Recent Files" menu, unless they are being edited
981 // remotely (using ODB).
982 if ([arguments objectForKey:@"remoteID"] == nil) {
983 [[NSDocumentController sharedDocumentController]
984 noteNewRecentFilePaths:filenames];
988 // b) Open any remaining files
991 [arguments setObject:[NSNumber numberWithInt:layout] forKey:@"layout"];
992 [arguments setObject:filenames forKey:@"filenames"];
993 // (Indicate that files should be opened from now on.)
994 [arguments setObject:[NSNumber numberWithBool:NO] forKey:@"dontOpen"];
997 if (openInCurrentWindow && (vc = [self topmostVimController])) {
998 // Open files in an already open window.
999 [[[vc windowController] window] makeKeyAndOrderFront:self];
1000 [vc passArguments:arguments];
1005 int numFiles = [filenames count];
1006 if (MMLayoutWindows == layout && numFiles > 1) {
1007 // Open one file at a time in a new window, but don't open too many at
1008 // once (at most cap+1 windows will open). If the user has increased
1009 // the preload cache size we'll take that as a hint that more windows
1010 // should be able to open at once.
1011 int cap = [self maxPreloadCacheSize] - 1;
1012 if (cap < 4) cap = 4;
1013 if (cap > numFiles) cap = numFiles;
1016 for (i = 0; i < cap; ++i) {
1017 NSArray *a = [NSArray arrayWithObject:[filenames objectAtIndex:i]];
1018 [arguments setObject:a forKey:@"filenames"];
1020 // NOTE: We have to copy the args since we'll mutate them in the
1021 // next loop and the below call may retain the arguments while
1022 // waiting for a process to start.
1023 NSDictionary *args = [[arguments copy] autorelease];
1025 openOk = [self openVimControllerWithArguments:args];
1029 // Open remaining files in tabs in a new window.
1030 if (openOk && numFiles > cap) {
1031 NSRange range = { i, numFiles-cap };
1032 NSArray *a = [filenames subarrayWithRange:range];
1033 [arguments setObject:a forKey:@"filenames"];
1034 [arguments setObject:[NSNumber numberWithInt:MMLayoutTabs]
1037 openOk = [self openVimControllerWithArguments:arguments];
1040 // Open all files at once.
1041 openOk = [self openVimControllerWithArguments:arguments];
1047 #ifdef MM_ENABLE_PLUGINS
1048 - (void)addItemToPlugInMenu:(NSMenuItem *)item
1050 NSMenu *menu = [plugInMenuItem submenu];
1051 [menu addItem:item];
1052 if ([menu numberOfItems] == 1)
1053 [self addPlugInMenuToMenu:[NSApp mainMenu]];
1056 - (void)removeItemFromPlugInMenu:(NSMenuItem *)item
1058 NSMenu *menu = [plugInMenuItem submenu];
1059 [menu removeItem:item];
1060 if ([menu numberOfItems] == 0)
1061 [self removePlugInMenu];
1065 - (IBAction)newWindow:(id)sender
1067 ASLogDebug(@"Open new window");
1069 // A cached controller requires no loading times and results in the new
1070 // window popping up instantaneously. If the cache is empty it may take
1071 // 1-2 seconds to start a new Vim process.
1072 MMVimController *vc = [self takeVimControllerFromCache];
1074 [[vc backendProxy] acknowledgeConnection];
1076 [self launchVimProcessWithArguments:nil workingDirectory:nil];
1080 - (IBAction)newWindowAndActivate:(id)sender
1082 [self activateWhenNextWindowOpens];
1083 [self newWindow:sender];
1086 - (IBAction)fileOpen:(id)sender
1088 ASLogDebug(@"Show file open panel");
1090 NSString *dir = nil;
1091 BOOL trackPwd = [[NSUserDefaults standardUserDefaults]
1092 boolForKey:MMDialogsTrackPwdKey];
1094 MMVimController *vc = [self keyVimController];
1095 if (vc) dir = [vc objectForVimStateKey:@"pwd"];
1098 NSOpenPanel *panel = [NSOpenPanel openPanel];
1099 [panel setAllowsMultipleSelection:YES];
1100 [panel setAccessoryView:showHiddenFilesView()];
1102 int result = [panel runModalForDirectory:dir file:nil types:nil];
1103 if (NSOKButton == result)
1104 [self application:NSApp openFiles:[panel filenames]];
1107 - (IBAction)selectNextWindow:(id)sender
1109 ASLogDebug(@"Select next window");
1111 unsigned i, count = [vimControllers count];
1114 NSWindow *keyWindow = [NSApp keyWindow];
1115 for (i = 0; i < count; ++i) {
1116 MMVimController *vc = [vimControllers objectAtIndex:i];
1117 if ([[[vc windowController] window] isEqual:keyWindow])
1124 MMVimController *vc = [vimControllers objectAtIndex:i];
1125 [[vc windowController] showWindow:self];
1129 - (IBAction)selectPreviousWindow:(id)sender
1131 ASLogDebug(@"Select previous window");
1133 unsigned i, count = [vimControllers count];
1136 NSWindow *keyWindow = [NSApp keyWindow];
1137 for (i = 0; i < count; ++i) {
1138 MMVimController *vc = [vimControllers objectAtIndex:i];
1139 if ([[[vc windowController] window] isEqual:keyWindow])
1149 MMVimController *vc = [vimControllers objectAtIndex:i];
1150 [[vc windowController] showWindow:self];
1154 - (IBAction)orderFrontPreferencePanel:(id)sender
1156 ASLogDebug(@"Show preferences panel");
1157 [[MMPreferenceController sharedPrefsWindowController] showWindow:self];
1160 - (IBAction)openWebsite:(id)sender
1162 ASLogDebug(@"Open MacVim website");
1163 [[NSWorkspace sharedWorkspace] openURL:
1164 [NSURL URLWithString:MMWebsiteString]];
1167 - (IBAction)showVimHelp:(id)sender
1169 ASLogDebug(@"Open window with Vim help");
1170 // Open a new window with the help window maximized.
1171 [self launchVimProcessWithArguments:[NSArray arrayWithObjects:
1172 @"-c", @":h gui_mac", @"-c", @":res", nil]
1173 workingDirectory:nil];
1176 - (IBAction)zoomAll:(id)sender
1178 ASLogDebug(@"Zoom all windows");
1179 [NSApp makeWindowsPerform:@selector(performZoom:) inOrder:YES];
1182 - (IBAction)atsuiButtonClicked:(id)sender
1184 ASLogDebug(@"Toggle ATSUI renderer");
1185 NSInteger renderer = MMRendererDefault;
1186 BOOL enable = ([sender state] == NSOnState);
1190 renderer = MMRendererATSUI;
1192 renderer = MMRendererCoreText;
1196 // Update the user default MMRenderer and synchronize the change so that
1197 // any new Vim process will pick up on the changed setting.
1198 CFPreferencesSetAppValue(
1199 (CFStringRef)MMRendererKey,
1200 (CFPropertyListRef)[NSNumber numberWithInt:renderer],
1201 kCFPreferencesCurrentApplication);
1202 CFPreferencesAppSynchronize(kCFPreferencesCurrentApplication);
1204 ASLogInfo(@"Use renderer=%d", renderer);
1206 // This action is called when the user clicks the "use ATSUI renderer"
1207 // button in the advanced preferences pane.
1208 [self rebuildPreloadCache];
1211 - (IBAction)loginShellButtonClicked:(id)sender
1213 ASLogDebug(@"Toggle login shell option");
1214 // This action is called when the user clicks the "use login shell" button
1215 // in the advanced preferences pane.
1216 [self rebuildPreloadCache];
1219 - (IBAction)quickstartButtonClicked:(id)sender
1221 ASLogDebug(@"Toggle Quickstart option");
1222 if ([self maxPreloadCacheSize] > 0) {
1223 [self scheduleVimControllerPreloadAfterDelay:1.0];
1224 [self startWatchingVimDir];
1226 [self cancelVimControllerPreloadRequests];
1227 [self clearPreloadCacheWithCount:-1];
1228 [self stopWatchingVimDir];
1232 - (MMVimController *)keyVimController
1234 NSWindow *keyWindow = [NSApp keyWindow];
1236 unsigned i, count = [vimControllers count];
1237 for (i = 0; i < count; ++i) {
1238 MMVimController *vc = [vimControllers objectAtIndex:i];
1239 if ([[[vc windowController] window] isEqual:keyWindow])
1247 - (unsigned)connectBackend:(byref in id <MMBackendProtocol>)proxy pid:(int)pid
1249 ASLogDebug(@"pid=%d", pid);
1251 [(NSDistantObject*)proxy setProtocolForProxy:@protocol(MMBackendProtocol)];
1253 // NOTE: Allocate the vim controller now but don't add it to the list of
1254 // controllers since this is a distributed object call and as such can
1255 // arrive at unpredictable times (e.g. while iterating the list of vim
1257 // (What if input arrives before the vim controller is added to the list of
1258 // controllers? This should not be a problem since the input isn't
1259 // processed immediately (see processInput:forIdentifier:).)
1260 // Also, since the app may be multithreaded (e.g. as a result of showing
1261 // the open panel) we have to ensure this call happens on the main thread,
1262 // else there is a race condition that may lead to a crash.
1263 MMVimController *vc = [[MMVimController alloc] initWithBackend:proxy
1265 [self performSelectorOnMainThread:@selector(addVimController:)
1268 modes:[NSArray arrayWithObject:
1269 NSDefaultRunLoopMode]];
1273 return [vc vimControllerId];
1276 - (oneway void)processInput:(in bycopy NSArray *)queue
1277 forIdentifier:(unsigned)identifier
1279 // NOTE: Input is not handled immediately since this is a distributed
1280 // object call and as such can arrive at unpredictable times. Instead,
1281 // queue the input and process it when the run loop is updated.
1283 if (!(queue && identifier)) {
1284 ASLogWarn(@"Bad input for identifier=%d", identifier);
1288 ASLogDebug(@"QUEUE for identifier=%d: <<< %@>>>", identifier,
1289 debugStringForMessageQueue(queue));
1291 NSNumber *key = [NSNumber numberWithUnsignedInt:identifier];
1292 NSArray *q = [inputQueues objectForKey:key];
1294 q = [q arrayByAddingObjectsFromArray:queue];
1295 [inputQueues setObject:q forKey:key];
1297 [inputQueues setObject:queue forKey:key];
1300 // NOTE: We must use "event tracking mode" as well as "default mode",
1301 // otherwise the input queue will not be processed e.g. during live
1303 // Also, since the app may be multithreaded (e.g. as a result of showing
1304 // the open panel) we have to ensure this call happens on the main thread,
1305 // else there is a race condition that may lead to a crash.
1306 [self performSelectorOnMainThread:@selector(processInputQueues:)
1309 modes:[NSArray arrayWithObjects:
1310 NSDefaultRunLoopMode,
1311 NSEventTrackingRunLoopMode, nil]];
1314 - (NSArray *)serverList
1316 NSMutableArray *array = [NSMutableArray array];
1318 unsigned i, count = [vimControllers count];
1319 for (i = 0; i < count; ++i) {
1320 MMVimController *controller = [vimControllers objectAtIndex:i];
1321 if ([controller serverName])
1322 [array addObject:[controller serverName]];
1328 @end // MMAppController
1333 @implementation MMAppController (MMServices)
1335 - (void)openSelection:(NSPasteboard *)pboard userData:(NSString *)userData
1336 error:(NSString **)error
1338 if (![[pboard types] containsObject:NSStringPboardType]) {
1339 ASLogNotice(@"Pasteboard contains no NSStringPboardType");
1343 ASLogInfo(@"Open new window containing current selection");
1345 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
1346 BOOL openInCurrentWindow = [ud boolForKey:MMOpenInCurrentWindowKey];
1347 MMVimController *vc;
1349 if (openInCurrentWindow && (vc = [self topmostVimController])) {
1350 [vc sendMessage:AddNewTabMsgID data:nil];
1351 [vc dropString:[pboard stringForType:NSStringPboardType]];
1353 // Save the text, open a new window, and paste the text when the next
1354 // window opens. (If this is called several times in a row, then all
1355 // but the last call may be ignored.)
1356 if (openSelectionString) [openSelectionString release];
1357 openSelectionString = [[pboard stringForType:NSStringPboardType] copy];
1359 [self newWindow:self];
1363 - (void)openFile:(NSPasteboard *)pboard userData:(NSString *)userData
1364 error:(NSString **)error
1366 if (![[pboard types] containsObject:NSStringPboardType]) {
1367 ASLogNotice(@"Pasteboard contains no NSStringPboardType");
1371 // TODO: Parse multiple filenames and create array with names.
1372 NSString *string = [pboard stringForType:NSStringPboardType];
1373 string = [string stringByTrimmingCharactersInSet:
1374 [NSCharacterSet whitespaceAndNewlineCharacterSet]];
1375 string = [string stringByStandardizingPath];
1377 ASLogInfo(@"Open new window with selected file: %@", string);
1379 NSArray *filenames = [self filterFilesAndNotify:
1380 [NSArray arrayWithObject:string]];
1381 if ([filenames count] == 0)
1384 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
1385 BOOL openInCurrentWindow = [ud boolForKey:MMOpenInCurrentWindowKey];
1386 MMVimController *vc;
1388 if (openInCurrentWindow && (vc = [self topmostVimController])) {
1389 [vc dropFiles:filenames forceOpen:YES];
1391 [self openFiles:filenames withArguments:nil];
1395 - (void)newFileHere:(NSPasteboard *)pboard userData:(NSString *)userData
1396 error:(NSString **)error
1398 if (![[pboard types] containsObject:NSFilenamesPboardType]) {
1399 ASLogNotice(@"Pasteboard contains no NSFilenamesPboardType");
1403 NSArray *filenames = [pboard propertyListForType:NSFilenamesPboardType];
1404 NSString *path = [filenames lastObject];
1407 if (![[NSFileManager defaultManager] fileExistsAtPath:path
1408 isDirectory:&dirIndicator]) {
1409 ASLogNotice(@"Invalid path. Cannot open new document at: %@", path);
1413 ASLogInfo(@"Open new file at path=%@", path);
1416 path = [path stringByDeletingLastPathComponent];
1418 path = [path stringByEscapingSpecialFilenameCharacters];
1420 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
1421 BOOL openInCurrentWindow = [ud boolForKey:MMOpenInCurrentWindowKey];
1422 MMVimController *vc;
1424 if (openInCurrentWindow && (vc = [self topmostVimController])) {
1425 NSString *input = [NSString stringWithFormat:@"<C-\\><C-N>"
1426 ":tabe|cd %@<CR>", path];
1427 [vc addVimInput:input];
1429 [self launchVimProcessWithArguments:nil workingDirectory:path];
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
1458 workingDirectory:(NSString *)cwd
1461 NSString *path = [[NSBundle mainBundle] pathForAuxiliaryExecutable:@"Vim"];
1464 ASLogCrit(@"Vim executable could not be found inside app bundle!");
1468 // Change current working directory so that the child process picks it up.
1469 NSFileManager *fm = [NSFileManager defaultManager];
1470 NSString *restoreCwd = nil;
1472 restoreCwd = [fm currentDirectoryPath];
1473 [fm changeCurrentDirectoryPath:cwd];
1476 NSArray *taskArgs = [NSArray arrayWithObjects:@"-g", @"-f", nil];
1478 taskArgs = [taskArgs arrayByAddingObjectsFromArray:args];
1480 BOOL useLoginShell = [[NSUserDefaults standardUserDefaults]
1481 boolForKey:MMLoginShellKey];
1482 if (useLoginShell) {
1483 // Run process with a login shell, roughly:
1484 // echo "exec Vim -g -f args" | ARGV0=-`basename $SHELL` $SHELL [-l]
1485 pid = [self executeInLoginShell:path arguments:taskArgs];
1487 // Run process directly:
1489 NSTask *task = [NSTask launchedTaskWithLaunchPath:path
1490 arguments:taskArgs];
1491 pid = task ? [task processIdentifier] : -1;
1495 // The 'pidArguments' dictionary keeps arguments to be passed to the
1496 // process when it connects (this is in contrast to arguments which are
1497 // passed on the command line, like '-f' and '-g').
1498 // If this method is called with nil arguments we take this as a hint
1499 // that this is an "untitled window" being launched and add a null
1500 // object to the 'pidArguments' dictionary. This way we can detect if
1501 // an untitled window is being launched by looking for null objects in
1503 // If this method is called with non-nil arguments then it is assumed
1504 // that the caller takes care of adding items to 'pidArguments' as
1505 // necessary (only some arguments are passed on connect, e.g. files to
1508 [pidArguments setObject:[NSNull null]
1509 forKey:[NSNumber numberWithInt:pid]];
1511 ASLogWarn(@"Failed to launch Vim process: args=%@, useLoginShell=%d",
1512 args, useLoginShell);
1515 // Now that child has launched, restore the current working directory.
1517 [fm changeCurrentDirectoryPath:restoreCwd];
1522 - (NSArray *)filterFilesAndNotify:(NSArray *)filenames
1524 // Go trough 'filenames' array and make sure each file exists. Present
1525 // warning dialog if some file was missing.
1527 NSString *firstMissingFile = nil;
1528 NSMutableArray *files = [NSMutableArray array];
1529 unsigned i, count = [filenames count];
1531 for (i = 0; i < count; ++i) {
1532 NSString *name = [filenames objectAtIndex:i];
1533 if ([[NSFileManager defaultManager] fileExistsAtPath:name]) {
1534 [files addObject:name];
1535 } else if (!firstMissingFile) {
1536 firstMissingFile = name;
1540 if (firstMissingFile) {
1541 NSAlert *alert = [[NSAlert alloc] init];
1542 [alert addButtonWithTitle:NSLocalizedString(@"OK",
1546 if ([files count] >= count-1) {
1547 [alert setMessageText:NSLocalizedString(@"File not found",
1548 @"File not found dialog, title")];
1549 text = [NSString stringWithFormat:NSLocalizedString(
1550 @"Could not open file with name %@.",
1551 @"File not found dialog, text"), firstMissingFile];
1553 [alert setMessageText:NSLocalizedString(@"Multiple files not found",
1554 @"File not found dialog, title")];
1555 text = [NSString stringWithFormat:NSLocalizedString(
1556 @"Could not open file with name %@, and %d other files.",
1557 @"File not found dialog, text"),
1558 firstMissingFile, count-[files count]-1];
1561 [alert setInformativeText:text];
1562 [alert setAlertStyle:NSWarningAlertStyle];
1567 [NSApp replyToOpenOrPrint:NSApplicationDelegateReplyFailure];
1573 - (NSArray *)filterOpenFiles:(NSArray *)filenames
1574 openFilesDict:(NSDictionary **)openFiles
1576 // Filter out any files in the 'filenames' array that are open and return
1577 // all files that are not already open. On return, the 'openFiles'
1578 // parameter (if non-nil) will point to a dictionary of open files, indexed
1579 // by Vim controller.
1581 NSMutableDictionary *dict = [NSMutableDictionary dictionary];
1582 NSMutableArray *files = [filenames mutableCopy];
1584 // TODO: Escape special characters in 'files'?
1585 NSString *expr = [NSString stringWithFormat:
1586 @"map([\"%@\"],\"bufloaded(v:val)\")",
1587 [files componentsJoinedByString:@"\",\""]];
1589 unsigned i, count = [vimControllers count];
1590 for (i = 0; i < count && [files count] > 0; ++i) {
1591 MMVimController *vc = [vimControllers objectAtIndex:i];
1593 // Query Vim for which files in the 'files' array are open.
1594 NSString *eval = [vc evaluateVimExpression:expr];
1595 if (!eval) continue;
1597 NSIndexSet *idxSet = [NSIndexSet indexSetWithVimList:eval];
1598 if ([idxSet count] > 0) {
1599 [dict setObject:[files objectsAtIndexes:idxSet]
1600 forKey:[NSValue valueWithPointer:vc]];
1602 // Remove all the files that were open in this Vim process and
1603 // create a new expression to evaluate.
1604 [files removeObjectsAtIndexes:idxSet];
1605 expr = [NSString stringWithFormat:
1606 @"map([\"%@\"],\"bufloaded(v:val)\")",
1607 [files componentsJoinedByString:@"\",\""]];
1611 if (openFiles != nil)
1614 return [files autorelease];
1617 #if MM_HANDLE_XCODE_MOD_EVENT
1618 - (void)handleXcodeModEvent:(NSAppleEventDescriptor *)event
1619 replyEvent:(NSAppleEventDescriptor *)reply
1622 // Xcode sends this event to query MacVim which open files have been
1624 ASLogDebug(@"reply:%@", reply);
1625 ASLogDebug(@"event:%@", event);
1627 NSEnumerator *e = [vimControllers objectEnumerator];
1629 while ((vc = [e nextObject])) {
1630 DescType type = [reply descriptorType];
1631 unsigned len = [[type data] length];
1632 NSMutableData *data = [NSMutableData data];
1634 [data appendBytes:&type length:sizeof(DescType)];
1635 [data appendBytes:&len length:sizeof(unsigned)];
1636 [data appendBytes:[reply data] length:len];
1638 [vc sendMessage:XcodeModMsgID data:data];
1644 - (void)handleGetURLEvent:(NSAppleEventDescriptor *)event
1645 replyEvent:(NSAppleEventDescriptor *)reply
1647 NSString *urlString = [[event paramDescriptorForKeyword:keyDirectObject]
1649 NSURL *url = [NSURL URLWithString:urlString];
1651 // We try to be compatible with TextMate's URL scheme here, as documented
1652 // at http://blog.macromates.com/2007/the-textmate-url-scheme/ . Currently,
1655 // The format is: mvim://open?<arguments> where arguments can be:
1657 // * url — the actual file to open (i.e. a file://… URL), if you leave
1658 // out this argument, the frontmost document is implied.
1659 // * line — line number to go to (one based).
1660 // * column — column number to go to (one based).
1662 // Example: mvim://open?url=file:///etc/profile&line=20
1664 if ([[url host] isEqualToString:@"open"]) {
1665 NSMutableDictionary *dict = [NSMutableDictionary dictionary];
1667 // Parse query ("url=file://...&line=14") into a dictionary
1668 NSArray *queries = [[url query] componentsSeparatedByString:@"&"];
1669 NSEnumerator *enumerator = [queries objectEnumerator];
1671 while ((param = [enumerator nextObject])) {
1672 NSArray *arr = [param componentsSeparatedByString:@"="];
1673 if ([arr count] == 2) {
1674 [dict setValue:[[arr lastObject]
1675 stringByReplacingPercentEscapesUsingEncoding:
1676 NSUTF8StringEncoding]
1677 forKey:[[arr objectAtIndex:0]
1678 stringByReplacingPercentEscapesUsingEncoding:
1679 NSUTF8StringEncoding]];
1683 // Actually open the file.
1684 NSString *file = [dict objectForKey:@"url"];
1686 NSURL *fileUrl= [NSURL URLWithString:file];
1687 // TextMate only opens files that already exist.
1688 if ([fileUrl isFileURL]
1689 && [[NSFileManager defaultManager] fileExistsAtPath:
1691 // Strip 'file://' path, else application:openFiles: might think
1692 // the file is not yet open.
1693 NSArray *filenames = [NSArray arrayWithObject:[fileUrl path]];
1695 // Look for the line and column options.
1696 NSDictionary *args = nil;
1697 NSString *line = [dict objectForKey:@"line"];
1699 NSString *column = [dict objectForKey:@"column"];
1701 args = [NSDictionary dictionaryWithObjectsAndKeys:
1702 line, @"cursorLine",
1703 column, @"cursorColumn",
1706 args = [NSDictionary dictionaryWithObject:line
1707 forKey:@"cursorLine"];
1710 [self openFiles:filenames withArguments:args];
1714 NSAlert *alert = [[NSAlert alloc] init];
1715 [alert addButtonWithTitle:NSLocalizedString(@"OK",
1718 [alert setMessageText:NSLocalizedString(@"Unknown URL Scheme",
1719 @"Unknown URL Scheme dialog, title")];
1720 [alert setInformativeText:[NSString stringWithFormat:NSLocalizedString(
1721 @"This version of MacVim does not support \"%@\""
1722 @" in its URL scheme.",
1723 @"Unknown URL Scheme dialog, text"),
1726 [alert setAlertStyle:NSWarningAlertStyle];
1732 - (NSMutableDictionary *)extractArgumentsFromOdocEvent:
1733 (NSAppleEventDescriptor *)desc
1735 NSMutableDictionary *dict = [NSMutableDictionary dictionary];
1737 // 1. Extract ODB parameters (if any)
1738 NSAppleEventDescriptor *odbdesc = desc;
1739 if (![odbdesc paramDescriptorForKeyword:keyFileSender]) {
1740 // The ODB paramaters may hide inside the 'keyAEPropData' descriptor.
1741 odbdesc = [odbdesc paramDescriptorForKeyword:keyAEPropData];
1742 if (![odbdesc paramDescriptorForKeyword:keyFileSender])
1747 NSAppleEventDescriptor *p =
1748 [odbdesc paramDescriptorForKeyword:keyFileSender];
1750 [dict setObject:[NSNumber numberWithUnsignedInt:[p typeCodeValue]]
1751 forKey:@"remoteID"];
1753 p = [odbdesc paramDescriptorForKeyword:keyFileCustomPath];
1755 [dict setObject:[p stringValue] forKey:@"remotePath"];
1757 p = [odbdesc paramDescriptorForKeyword:keyFileSenderToken];
1759 [dict setObject:[NSNumber numberWithUnsignedLong:[p descriptorType]]
1760 forKey:@"remoteTokenDescType"];
1761 [dict setObject:[p data] forKey:@"remoteTokenData"];
1765 // 2. Extract Xcode parameters (if any)
1766 NSAppleEventDescriptor *xcodedesc =
1767 [desc paramDescriptorForKeyword:keyAEPosition];
1770 NSData *data = [xcodedesc data];
1771 NSUInteger length = [data length];
1773 if (length == sizeof(MMXcodeSelectionRange)) {
1774 MMXcodeSelectionRange *sr = (MMXcodeSelectionRange*)[data bytes];
1775 ASLogDebug(@"Xcode selection range (%d,%d,%d,%d,%d,%d)",
1776 sr->unused1, sr->lineNum, sr->startRange, sr->endRange,
1777 sr->unused2, sr->theDate);
1779 if (sr->lineNum < 0) {
1780 // Should select a range of lines.
1781 range.location = sr->startRange + 1;
1782 range.length = sr->endRange - sr->startRange + 1;
1784 // Should only move cursor to a line.
1785 range.location = sr->lineNum + 1;
1789 [dict setObject:NSStringFromRange(range) forKey:@"selectionRange"];
1791 ASLogErr(@"Xcode selection range size mismatch! got=%d expected=%d",
1792 length, sizeof(MMXcodeSelectionRange));
1796 // 3. Extract Spotlight search text (if any)
1797 NSAppleEventDescriptor *spotlightdesc =
1798 [desc paramDescriptorForKeyword:keyAESearchText];
1800 [dict setObject:[spotlightdesc stringValue] forKey:@"searchText"];
1805 #ifdef MM_ENABLE_PLUGINS
1806 - (void)removePlugInMenu
1808 if ([plugInMenuItem menu])
1809 [[plugInMenuItem menu] removeItem:plugInMenuItem];
1812 - (void)addPlugInMenuToMenu:(NSMenu *)mainMenu
1814 NSMenu *windowsMenu = [mainMenu findWindowsMenu];
1816 if ([[plugInMenuItem submenu] numberOfItems] > 0) {
1817 int idx = windowsMenu ? [mainMenu indexOfItemWithSubmenu:windowsMenu]
1820 [mainMenu insertItem:plugInMenuItem atIndex:idx];
1822 [mainMenu addItem:plugInMenuItem];
1828 - (void)scheduleVimControllerPreloadAfterDelay:(NSTimeInterval)delay
1830 [self performSelector:@selector(preloadVimController:)
1835 - (void)cancelVimControllerPreloadRequests
1837 [NSObject cancelPreviousPerformRequestsWithTarget:self
1838 selector:@selector(preloadVimController:)
1842 - (void)preloadVimController:(id)sender
1844 // We only allow preloading of one Vim process at a time (to avoid hogging
1845 // CPU), so schedule another preload in a little while if necessary.
1846 if (-1 != preloadPid) {
1847 [self scheduleVimControllerPreloadAfterDelay:2];
1851 if ([cachedVimControllers count] >= [self maxPreloadCacheSize])
1854 preloadPid = [self launchVimProcessWithArguments:
1855 [NSArray arrayWithObject:@"--mmwaitforack"]
1856 workingDirectory:nil];
1859 - (int)maxPreloadCacheSize
1861 // The maximum number of Vim processes to keep in the cache can be
1862 // controlled via the user default "MMPreloadCacheSize".
1863 int maxCacheSize = [[NSUserDefaults standardUserDefaults]
1864 integerForKey:MMPreloadCacheSizeKey];
1865 if (maxCacheSize < 0) maxCacheSize = 0;
1866 else if (maxCacheSize > 10) maxCacheSize = 10;
1868 return maxCacheSize;
1871 - (MMVimController *)takeVimControllerFromCache
1873 // NOTE: After calling this message the backend corresponding to the
1874 // returned vim controller must be sent an acknowledgeConnection message,
1875 // else the vim process will be stuck.
1877 // This method may return nil even though the cache might be non-empty; the
1878 // caller should handle this by starting a new Vim process.
1880 int i, count = [cachedVimControllers count];
1881 if (0 == count) return nil;
1883 // Locate the first Vim controller with up-to-date rc-files sourced.
1884 NSDate *rcDate = [self rcFilesModificationDate];
1885 for (i = 0; i < count; ++i) {
1886 MMVimController *vc = [cachedVimControllers objectAtIndex:i];
1887 NSDate *date = [vc creationDate];
1888 if ([date compare:rcDate] != NSOrderedAscending)
1893 // Clear out cache entries whose vimrc/gvimrc files were sourced before
1894 // the latest modification date for those files. This ensures that the
1895 // latest rc-files are always sourced for new windows.
1896 [self clearPreloadCacheWithCount:i];
1899 if ([cachedVimControllers count] == 0) {
1900 [self scheduleVimControllerPreloadAfterDelay:2.0];
1904 MMVimController *vc = [cachedVimControllers objectAtIndex:0];
1905 [vimControllers addObject:vc];
1906 [cachedVimControllers removeObjectAtIndex:0];
1907 [vc setIsPreloading:NO];
1909 // If the Vim process has finished loading then the window will displayed
1910 // now, otherwise it will be displayed when the OpenWindowMsgID message is
1912 [[vc windowController] showWindow];
1914 // Since we've taken one controller from the cache we take the opportunity
1915 // to preload another.
1916 [self scheduleVimControllerPreloadAfterDelay:1];
1921 - (void)clearPreloadCacheWithCount:(int)count
1923 // Remove the 'count' first entries in the preload cache. It is assumed
1924 // that objects are added/removed from the cache in a FIFO manner so that
1925 // this effectively clears the 'count' oldest entries.
1926 // If 'count' is negative, then the entire cache is cleared.
1928 if ([cachedVimControllers count] == 0 || count == 0)
1932 count = [cachedVimControllers count];
1934 // Make sure the preloaded Vim processes get killed or they'll just hang
1935 // around being useless until MacVim is terminated.
1936 NSEnumerator *e = [cachedVimControllers objectEnumerator];
1937 MMVimController *vc;
1939 while ((vc = [e nextObject]) && n-- > 0) {
1940 [[NSNotificationCenter defaultCenter] removeObserver:vc];
1941 [vc sendMessage:TerminateNowMsgID data:nil];
1943 // Since the preloaded processes were killed "prematurely" we have to
1944 // manually tell them to cleanup (it is not enough to simply release
1945 // them since deallocation and cleanup are separated).
1950 while (n-- > 0 && [cachedVimControllers count] > 0)
1951 [cachedVimControllers removeObjectAtIndex:0];
1953 // There is a small delay before the Vim process actually exits so wait a
1954 // little before trying to reap the child process. If the process still
1955 // hasn't exited after this wait it won't be reaped until the next time
1956 // reapChildProcesses: is called (but this should be harmless).
1957 [self performSelector:@selector(reapChildProcesses:)
1962 - (void)rebuildPreloadCache
1964 if ([self maxPreloadCacheSize] > 0) {
1965 [self clearPreloadCacheWithCount:-1];
1966 [self cancelVimControllerPreloadRequests];
1967 [self scheduleVimControllerPreloadAfterDelay:1.0];
1972 // HACK: fileAttributesAtPath was deprecated in 10.5
1973 #if (MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5)
1974 #define MM_fileAttributes(fm,p) [fm attributesOfItemAtPath:p error:NULL]
1976 #define MM_fileAttributes(fm,p) [fm fileAttributesAtPath:p traverseLink:YES]
1978 - (NSDate *)rcFilesModificationDate
1980 // Check modification dates for ~/.vimrc and ~/.gvimrc and return the
1981 // latest modification date. If ~/.vimrc does not exist, check ~/_vimrc
1982 // and similarly for gvimrc.
1983 // Returns distantPath if no rc files were found.
1985 NSDate *date = [NSDate distantPast];
1986 NSFileManager *fm = [NSFileManager defaultManager];
1988 NSString *path = [@"~/.vimrc" stringByExpandingTildeInPath];
1989 NSDictionary *attr = MM_fileAttributes(fm, path);
1991 path = [@"~/_vimrc" stringByExpandingTildeInPath];
1992 attr = MM_fileAttributes(fm, path);
1994 NSDate *modDate = [attr objectForKey:NSFileModificationDate];
1998 path = [@"~/.gvimrc" stringByExpandingTildeInPath];
1999 attr = MM_fileAttributes(fm, path);
2001 path = [@"~/_gvimrc" stringByExpandingTildeInPath];
2002 attr = MM_fileAttributes(fm, path);
2004 modDate = [attr objectForKey:NSFileModificationDate];
2006 date = [date laterDate:modDate];
2010 #undef MM_fileAttributes
2012 - (BOOL)openVimControllerWithArguments:(NSDictionary *)arguments
2014 MMVimController *vc = [self takeVimControllerFromCache];
2016 // Open files in a new window using a cached vim controller. This
2017 // requires virtually no loading time so the new window will pop up
2019 [vc passArguments:arguments];
2020 [[vc backendProxy] acknowledgeConnection];
2022 NSArray *cmdline = nil;
2023 NSString *cwd = [self workingDirectoryForArguments:arguments];
2024 arguments = [self convertVimControllerArguments:arguments
2025 toCommandLine:&cmdline];
2026 int pid = [self launchVimProcessWithArguments:cmdline
2027 workingDirectory:cwd];
2031 // TODO: If the Vim process fails to start, or if it changes PID,
2032 // then the memory allocated for these parameters will leak.
2033 // Ensure that this cannot happen or somehow detect it.
2035 if ([arguments count] > 0)
2036 [pidArguments setObject:arguments
2037 forKey:[NSNumber numberWithInt:pid]];
2043 - (void)activateWhenNextWindowOpens
2045 ASLogDebug(@"Activate MacVim when next window opens");
2046 shouldActivateWhenNextWindowOpens = YES;
2049 - (void)startWatchingVimDir
2051 #if (MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5)
2054 if (NULL == FSEventStreamStart)
2055 return; // FSEvent functions are weakly linked
2057 NSString *path = [@"~/.vim" stringByExpandingTildeInPath];
2058 NSArray *pathsToWatch = [NSArray arrayWithObject:path];
2060 fsEventStream = FSEventStreamCreate(NULL, &fsEventCallback, NULL,
2061 (CFArrayRef)pathsToWatch, kFSEventStreamEventIdSinceNow,
2062 MMEventStreamLatency, kFSEventStreamCreateFlagNone);
2064 FSEventStreamScheduleWithRunLoop(fsEventStream,
2065 [[NSRunLoop currentRunLoop] getCFRunLoop],
2066 kCFRunLoopDefaultMode);
2068 FSEventStreamStart(fsEventStream);
2069 ASLogDebug(@"Started FS event stream");
2073 - (void)stopWatchingVimDir
2075 #if (MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5)
2076 if (NULL == FSEventStreamStop)
2077 return; // FSEvent functions are weakly linked
2079 if (fsEventStream) {
2080 FSEventStreamStop(fsEventStream);
2081 FSEventStreamInvalidate(fsEventStream);
2082 FSEventStreamRelease(fsEventStream);
2083 fsEventStream = NULL;
2084 ASLogDebug(@"Stopped FS event stream");
2090 - (void)handleFSEvent
2092 [self clearPreloadCacheWithCount:-1];
2094 // Several FS events may arrive in quick succession so make sure to cancel
2095 // any previous preload requests before making a new one.
2096 [self cancelVimControllerPreloadRequests];
2097 [self scheduleVimControllerPreloadAfterDelay:0.5];
2100 - (void)loadDefaultFont
2102 // It is possible to set a user default to avoid loading the default font
2103 // (this cuts down on startup time).
2104 if (![[NSUserDefaults standardUserDefaults] boolForKey:MMLoadDefaultFontKey]
2105 || fontContainerRef) {
2106 ASLogInfo(@"Skip loading of the default font...");
2110 ASLogInfo(@"Loading the default font...");
2112 // Load all fonts in the Resouces folder of the app bundle.
2113 NSString *fontsFolder = [[NSBundle mainBundle] resourcePath];
2115 NSURL *fontsURL = [NSURL fileURLWithPath:fontsFolder];
2118 CFURLGetFSRef((CFURLRef)fontsURL, &fsRef);
2120 #if (MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5)
2121 // This is the font activation API for OS X 10.5. Only compile
2122 // this code if we're building on OS X 10.5 or later.
2123 if (NULL != ATSFontActivateFromFileReference) { // Weakly linked
2124 ATSFontActivateFromFileReference(&fsRef, kATSFontContextLocal,
2125 kATSFontFormatUnspecified,
2126 NULL, kATSOptionFlagsDefault,
2130 #if (MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_5)
2131 // The following font activation API was deprecated in OS X 10.5.
2132 // Don't compile this code unless we're targeting OS X 10.4.
2134 if (fontContainerRef == 0 &&
2135 FSGetCatalogInfo(&fsRef, kFSCatInfoNone, NULL, NULL,
2136 &fsSpec, NULL) == noErr) {
2137 ATSFontActivateFromFileSpecification(&fsSpec,
2138 kATSFontContextLocal, kATSFontFormatUnspecified, NULL,
2139 kATSOptionFlagsDefault, &fontContainerRef);
2145 if (!fontContainerRef) {
2146 ASLogNotice(@"Failed to activate the default font (the app bundle "
2147 "may be incomplete)");
2151 - (int)executeInLoginShell:(NSString *)path arguments:(NSArray *)args
2153 // Start a login shell and execute the command 'path' with arguments 'args'
2154 // in the shell. This ensures that user environment variables are set even
2155 // when MacVim was started from the Finder.
2158 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
2160 // Determine which shell to use to execute the command. The user
2161 // may decide which shell to use by setting a user default or the
2162 // $SHELL environment variable.
2163 NSString *shell = [ud stringForKey:MMLoginShellCommandKey];
2164 if (!shell || [shell length] == 0)
2165 shell = [[[NSProcessInfo processInfo] environment]
2166 objectForKey:@"SHELL"];
2168 shell = @"/bin/bash";
2170 // Bash needs the '-l' flag to launch a login shell. The user may add
2171 // flags by setting a user default.
2172 NSString *shellArgument = [ud stringForKey:MMLoginShellArgumentKey];
2173 if (!shellArgument || [shellArgument length] == 0) {
2174 if ([[shell lastPathComponent] isEqual:@"bash"])
2175 shellArgument = @"-l";
2177 shellArgument = nil;
2180 // Build input string to pipe to the login shell.
2181 NSMutableString *input = [NSMutableString stringWithFormat:
2182 @"exec \"%@\"", path];
2184 // Append all arguments, making sure they are properly quoted, even
2185 // when they contain single quotes.
2186 NSEnumerator *e = [args objectEnumerator];
2189 while ((obj = [e nextObject])) {
2190 NSMutableString *arg = [NSMutableString stringWithString:obj];
2191 [arg replaceOccurrencesOfString:@"'" withString:@"'\"'\"'"
2192 options:NSLiteralSearch
2193 range:NSMakeRange(0, [arg length])];
2194 [input appendFormat:@" '%@'", arg];
2198 // Build the argument vector used to start the login shell.
2199 NSString *shellArg0 = [NSString stringWithFormat:@"-%@",
2200 [shell lastPathComponent]];
2201 char *shellArgv[3] = { (char *)[shellArg0 UTF8String], NULL, NULL };
2203 shellArgv[1] = (char *)[shellArgument UTF8String];
2205 // Get the C string representation of the shell path before the fork since
2206 // we must not call Foundation functions after a fork.
2207 const char *shellPath = [shell fileSystemRepresentation];
2209 // Fork and execute the process.
2211 if (pipe(ds)) return -1;
2216 } else if (pid == 0) {
2219 if (close(ds[1]) == -1) exit(255);
2220 if (dup2(ds[0], 0) == -1) exit(255);
2222 // Without the following call warning messages like this appear on the
2224 // com.apple.launchd[69] : Stray process with PGID equal to this
2225 // dead job: PID 1589 PPID 1 Vim
2228 execv(shellPath, shellArgv);
2230 // Never reached unless execv fails
2234 if (close(ds[0]) == -1) return -1;
2236 // Send input to execute to the child process
2237 [input appendString:@"\n"];
2238 int bytes = [input lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
2240 if (write(ds[1], [input UTF8String], bytes) != bytes) return -1;
2241 if (close(ds[1]) == -1) return -1;
2243 ++numChildProcesses;
2244 ASLogDebug(@"new process pid=%d (count=%d)", pid, numChildProcesses);
2250 - (void)reapChildProcesses:(id)sender
2252 // NOTE: numChildProcesses (currently) only counts the number of Vim
2253 // processes that have been started with executeInLoginShell::. If other
2254 // processes are spawned this code may need to be adjusted (or
2255 // numChildProcesses needs to be incremented when such a process is
2257 while (numChildProcesses > 0) {
2259 int pid = waitpid(-1, &status, WNOHANG);
2263 ASLogDebug(@"Wait for pid=%d complete", pid);
2264 --numChildProcesses;
2268 - (void)processInputQueues:(id)sender
2270 // NOTE: Because we use distributed objects it is quite possible for this
2271 // function to be re-entered. This can cause all sorts of unexpected
2272 // problems so we guard against it here so that the rest of the code does
2273 // not need to worry about it.
2275 // The processing flag is > 0 if this function is already on the call
2276 // stack; < 0 if this function was also re-entered.
2277 if (processingFlag != 0) {
2278 ASLogDebug(@"BUSY!");
2279 processingFlag = -1;
2283 // NOTE: Be _very_ careful that no exceptions can be raised between here
2284 // and the point at which 'processingFlag' is reset. Otherwise the above
2285 // test could end up always failing and no input queues would ever be
2289 // NOTE: New input may arrive while we're busy processing; we deal with
2290 // this by putting the current queue aside and creating a new input queue
2291 // for future input.
2292 NSDictionary *queues = inputQueues;
2293 inputQueues = [NSMutableDictionary new];
2295 // Pass each input queue on to the vim controller with matching
2296 // identifier (and note that it could be cached).
2297 NSEnumerator *e = [queues keyEnumerator];
2299 while ((key = [e nextObject])) {
2300 unsigned ukey = [key unsignedIntValue];
2301 int i = 0, count = [vimControllers count];
2302 for (i = 0; i < count; ++i) {
2303 MMVimController *vc = [vimControllers objectAtIndex:i];
2304 if (ukey == [vc vimControllerId]) {
2305 [vc processInputQueue:[queues objectForKey:key]]; // !exceptions
2310 if (i < count) continue;
2312 count = [cachedVimControllers count];
2313 for (i = 0; i < count; ++i) {
2314 MMVimController *vc = [cachedVimControllers objectAtIndex:i];
2315 if (ukey == [vc vimControllerId]) {
2316 [vc processInputQueue:[queues objectForKey:key]]; // !exceptions
2322 ASLogWarn(@"No Vim controller for identifier=%d", ukey);
2328 // If new input arrived while we were processing it would have been
2329 // blocked so we have to schedule it to be processed again.
2330 if (processingFlag < 0)
2331 [self performSelectorOnMainThread:@selector(processInputQueues:)
2334 modes:[NSArray arrayWithObjects:
2335 NSDefaultRunLoopMode,
2336 NSEventTrackingRunLoopMode, nil]];
2341 - (void)addVimController:(MMVimController *)vc
2343 ASLogDebug(@"Add Vim controller pid=%d id=%d",
2344 [vc pid], [vc vimControllerId]);
2347 NSNumber *pidKey = [NSNumber numberWithInt:pid];
2349 if (preloadPid == pid) {
2350 // This controller was preloaded, so add it to the cache and
2351 // schedule another vim process to be preloaded.
2353 [vc setIsPreloading:YES];
2354 [cachedVimControllers addObject:vc];
2355 [self scheduleVimControllerPreloadAfterDelay:1];
2357 [vimControllers addObject:vc];
2359 id args = [pidArguments objectForKey:pidKey];
2360 if (args && [NSNull null] != args)
2361 [vc passArguments:args];
2363 // HACK! MacVim does not get activated if it is launched from the
2364 // terminal, so we forcibly activate here unless it is an untitled
2365 // window opening. Untitled windows are treated differently, else
2366 // MacVim would steal the focus if another app was activated while the
2367 // untitled window was loading.
2368 if (!args || args != [NSNull null])
2369 [self activateWhenNextWindowOpens];
2372 [pidArguments removeObjectForKey:pidKey];
2376 - (NSDictionary *)convertVimControllerArguments:(NSDictionary *)args
2377 toCommandLine:(NSArray **)cmdline
2379 // Take all arguments out of 'args' and put them on an array suitable to
2380 // pass as arguments to launchVimProcessWithArguments:. The untouched
2381 // dictionary items are returned in a new autoreleased dictionary.
2386 NSArray *filenames = [args objectForKey:@"filenames"];
2387 int numFiles = filenames ? [filenames count] : 0;
2388 BOOL openFiles = ![[args objectForKey:@"dontOpen"] boolValue];
2390 if (numFiles <= 0 || !openFiles)
2393 NSMutableArray *a = [NSMutableArray array];
2394 NSMutableDictionary *d = [[args mutableCopy] autorelease];
2396 // Search for text using "+/text".
2397 NSString *searchText = [args objectForKey:@"searchText"];
2399 // TODO: If the search pattern is not found an error is shown when
2400 // starting. Figure out a way to get rid of this message (The help
2401 // says to use ':silent exe "normal /pat\<CR>"' but this does not
2403 [a addObject:[NSString stringWithFormat:@"+/%@", searchText]];
2405 [d removeObjectForKey:@"searchText"];
2408 // Position cursor using "+line" or "-c :cal cursor(line,column)".
2409 NSString *lineString = [args objectForKey:@"cursorLine"];
2410 if (lineString && [lineString intValue] > 0) {
2411 NSString *columnString = [args objectForKey:@"cursorColumn"];
2412 if (columnString && [columnString intValue] > 0) {
2413 [a addObject:@"-c"];
2414 [a addObject:[NSString stringWithFormat:@":cal cursor(%@,%@)",
2415 lineString, columnString]];
2417 [d removeObjectForKey:@"cursorColumn"];
2419 [a addObject:[NSString stringWithFormat:@"+%@", lineString]];
2422 [d removeObjectForKey:@"cursorLine"];
2425 // Set selection using normal mode commands.
2426 NSString *rangeString = [args objectForKey:@"selectionRange"];
2428 NSRange r = NSRangeFromString(rangeString);
2429 [a addObject:@"-c"];
2431 // Select given range.
2432 [a addObject:[NSString stringWithFormat:@"norm %dGV%dGz.0",
2433 NSMaxRange(r), r.location]];
2435 // Position cursor on start of range.
2436 [a addObject:[NSString stringWithFormat:@"norm %dGz.0",
2440 [d removeObjectForKey:@"selectionRange"];
2443 // Choose file layout using "-[o|O|p]".
2444 int layout = [[args objectForKey:@"layout"] intValue];
2446 case MMLayoutHorizontalSplit: [a addObject:@"-o"]; break;
2447 case MMLayoutVerticalSplit: [a addObject:@"-O"]; break;
2448 case MMLayoutTabs: [a addObject:@"-p"]; break;
2450 [d removeObjectForKey:@"layout"];
2453 // Last of all add the names of all files to open (DO NOT add more args
2454 // after this point).
2455 [a addObjectsFromArray:filenames];
2457 if ([args objectForKey:@"remoteID"]) {
2458 // These files should be edited remotely so keep the filenames on the
2459 // argument list -- they will need to be passed back to Vim when it
2460 // checks in. Also set the 'dontOpen' flag or the files will be
2462 [d setObject:[NSNumber numberWithBool:YES] forKey:@"dontOpen"];
2464 [d removeObjectForKey:@"dontOpen"];
2465 [d removeObjectForKey:@"filenames"];
2474 - (NSString *)workingDirectoryForArguments:(NSDictionary *)args
2476 // Find the "filenames" argument and pick the first path that actually
2477 // exists and return it.
2478 // TODO: Return common parent directory in the case of multiple files?
2479 NSFileManager *fm = [NSFileManager defaultManager];
2480 NSArray *filenames = [args objectForKey:@"filenames"];
2481 NSUInteger i, count = [filenames count];
2482 for (i = 0; i < count; ++i) {
2484 NSString *file = [filenames objectAtIndex:i];
2485 if ([fm fileExistsAtPath:file isDirectory:&isdir])
2486 return isdir ? file : [file stringByDeletingLastPathComponent];
2492 - (NSScreen *)screenContainingPoint:(NSPoint)pt
2494 NSArray *screens = [NSScreen screens];
2495 NSUInteger i, count = [screens count];
2496 for (i = 0; i < count; ++i) {
2497 NSScreen *screen = [screens objectAtIndex:i];
2498 NSRect frame = [screen frame];
2499 if (NSPointInRect(pt, frame))
2506 @end // MMAppController (Private)