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 // When terminating, notify Vim processes then sleep for these many
66 static useconds_t MMTerminationSleepPeriod = 10000;
68 #if (MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_4)
69 // Latency (in s) between FS event occuring and being reported to MacVim.
70 // Should be small so that MacVim is notified of changes to the ~/.vim
71 // directory more or less immediately.
72 static CFTimeInterval MMEventStreamLatency = 0.1;
76 #pragma options align=mac68k
79 short unused1; // 0 (not used)
80 short lineNum; // line to select (< 0 to specify range)
81 long startRange; // start of selection range (if line < 0)
82 long endRange; // end of selection range (if line < 0)
83 long unused2; // 0 (not used)
84 long theDate; // modification date/time
86 #pragma options align=reset
89 static int executeInLoginShell(NSString *path, NSArray *args);
92 @interface MMAppController (MMServices)
93 - (void)openSelection:(NSPasteboard *)pboard userData:(NSString *)userData
94 error:(NSString **)error;
95 - (void)openFile:(NSPasteboard *)pboard userData:(NSString *)userData
96 error:(NSString **)error;
97 - (void)newFileHere:(NSPasteboard *)pboard userData:(NSString *)userData
98 error:(NSString **)error;
102 @interface MMAppController (Private)
103 - (MMVimController *)topmostVimController;
104 - (int)launchVimProcessWithArguments:(NSArray *)args;
105 - (NSArray *)filterFilesAndNotify:(NSArray *)files;
106 - (NSArray *)filterOpenFiles:(NSArray *)filenames
107 openFilesDict:(NSDictionary **)openFiles;
108 #if MM_HANDLE_XCODE_MOD_EVENT
109 - (void)handleXcodeModEvent:(NSAppleEventDescriptor *)event
110 replyEvent:(NSAppleEventDescriptor *)reply;
112 - (void)handleGetURLEvent:(NSAppleEventDescriptor *)event
113 replyEvent:(NSAppleEventDescriptor *)reply;
114 - (int)findLaunchingProcessWithoutArguments;
115 - (MMVimController *)findUnusedEditor;
116 - (NSMutableDictionary *)extractArgumentsFromOdocEvent:
117 (NSAppleEventDescriptor *)desc;
118 - (void)scheduleVimControllerPreloadAfterDelay:(NSTimeInterval)delay;
119 - (void)cancelVimControllerPreloadRequests;
120 - (void)preloadVimController:(id)sender;
121 - (int)maxPreloadCacheSize;
122 - (MMVimController *)takeVimControllerFromCache;
123 - (void)clearPreloadCacheWithCount:(int)count;
124 - (void)rebuildPreloadCache;
125 - (NSDate *)rcFilesModificationDate;
126 - (BOOL)openVimControllerWithArguments:(NSDictionary *)arguments;
127 - (void)activateWhenNextWindowOpens;
128 - (void)startWatchingVimDir;
129 - (void)stopWatchingVimDir;
130 - (void)handleFSEvent;
132 #ifdef MM_ENABLE_PLUGINS
133 - (void)removePlugInMenu;
134 - (void)addPlugInMenuToMenu:(NSMenu *)mainMenu;
140 #if (MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_4)
142 fsEventCallback(ConstFSEventStreamRef streamRef,
143 void *clientCallBackInfo,
146 const FSEventStreamEventFlags eventFlags[],
147 const FSEventStreamEventId eventIds[])
149 [[MMAppController sharedInstance] handleFSEvent];
153 @implementation MMAppController
157 // Avoid zombies (we fork Vim processes which we don't want to wait for).
158 signal(SIGCHLD, SIG_IGN);
160 NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:
161 [NSNumber numberWithBool:NO], MMNoWindowKey,
162 [NSNumber numberWithInt:64], MMTabMinWidthKey,
163 [NSNumber numberWithInt:6*64], MMTabMaxWidthKey,
164 [NSNumber numberWithInt:132], MMTabOptimumWidthKey,
165 [NSNumber numberWithBool:YES], MMShowAddTabButtonKey,
166 [NSNumber numberWithInt:2], MMTextInsetLeftKey,
167 [NSNumber numberWithInt:1], MMTextInsetRightKey,
168 [NSNumber numberWithInt:1], MMTextInsetTopKey,
169 [NSNumber numberWithInt:1], MMTextInsetBottomKey,
170 @"MMTypesetter", MMTypesetterKey,
171 [NSNumber numberWithFloat:1], MMCellWidthMultiplierKey,
172 [NSNumber numberWithFloat:-1], MMBaselineOffsetKey,
173 [NSNumber numberWithBool:YES], MMTranslateCtrlClickKey,
174 [NSNumber numberWithInt:0], MMOpenInCurrentWindowKey,
175 [NSNumber numberWithBool:NO], MMNoFontSubstitutionKey,
176 [NSNumber numberWithBool:YES], MMLoginShellKey,
177 [NSNumber numberWithBool:NO], MMAtsuiRendererKey,
178 [NSNumber numberWithInt:MMUntitledWindowAlways],
180 [NSNumber numberWithBool:NO], MMTexturedWindowKey,
181 [NSNumber numberWithBool:NO], MMZoomBothKey,
182 @"", MMLoginShellCommandKey,
183 @"", MMLoginShellArgumentKey,
184 [NSNumber numberWithBool:YES], MMDialogsTrackPwdKey,
185 #ifdef MM_ENABLE_PLUGINS
186 [NSNumber numberWithBool:YES], MMShowLeftPlugInContainerKey,
188 [NSNumber numberWithInt:3], MMOpenLayoutKey,
189 [NSNumber numberWithBool:NO], MMVerticalSplitKey,
190 [NSNumber numberWithInt:0], MMPreloadCacheSizeKey,
191 [NSNumber numberWithInt:0], MMLastWindowClosedBehaviorKey,
194 [[NSUserDefaults standardUserDefaults] registerDefaults:dict];
196 NSArray *types = [NSArray arrayWithObject:NSStringPboardType];
197 [NSApp registerServicesMenuSendTypes:types returnTypes:types];
199 // NOTE: Set the current directory to user's home directory, otherwise it
200 // will default to the root directory. (This matters since new Vim
201 // processes inherit MacVim's environment variables.)
202 [[NSFileManager defaultManager] changeCurrentDirectoryPath:
208 if (!(self = [super init])) return nil;
210 fontContainerRef = loadFonts();
212 vimControllers = [NSMutableArray new];
213 cachedVimControllers = [NSMutableArray new];
215 pidArguments = [NSMutableDictionary new];
217 #ifdef MM_ENABLE_PLUGINS
218 NSString *plugInTitle = NSLocalizedString(@"Plug-In",
219 @"Plug-In menu title");
220 plugInMenuItem = [[NSMenuItem alloc] initWithTitle:plugInTitle
223 NSMenu *submenu = [[NSMenu alloc] initWithTitle:plugInTitle];
224 [plugInMenuItem setSubmenu:submenu];
228 // NOTE: Do not use the default connection since the Logitech Control
229 // Center (LCC) input manager steals and this would cause MacVim to
230 // never open any windows. (This is a bug in LCC but since they are
231 // unlikely to fix it, we graciously give them the default connection.)
232 connection = [[NSConnection alloc] initWithReceivePort:[NSPort port]
234 [connection setRootObject:self];
235 [connection setRequestTimeout:MMRequestTimeout];
236 [connection setReplyTimeout:MMReplyTimeout];
238 // NOTE! If the name of the connection changes here it must also be
239 // updated in MMBackend.m.
240 NSString *name = [NSString stringWithFormat:@"%@-connection",
241 [[NSBundle mainBundle] bundlePath]];
242 //NSLog(@"Registering connection with name '%@'", name);
243 if (![connection registerName:name]) {
244 NSLog(@"FATAL ERROR: Failed to register connection with name '%@'",
246 [connection release]; connection = nil;
254 //NSLog(@"MMAppController dealloc");
256 [connection release]; connection = nil;
257 [pidArguments release]; pidArguments = nil;
258 [vimControllers release]; vimControllers = nil;
259 [cachedVimControllers release]; cachedVimControllers = nil;
260 [openSelectionString release]; openSelectionString = nil;
261 [recentFilesMenuItem release]; recentFilesMenuItem = nil;
262 [defaultMainMenu release]; defaultMainMenu = nil;
263 #ifdef MM_ENABLE_PLUGINS
264 [plugInMenuItem release]; plugInMenuItem = nil;
266 [appMenuItemTemplate release]; appMenuItemTemplate = nil;
271 - (void)applicationWillFinishLaunching:(NSNotification *)notification
273 // Remember the default menu so that it can be restored if the user closes
274 // all editor windows.
275 defaultMainMenu = [[NSApp mainMenu] retain];
277 // Store a copy of the default app menu so we can use this as a template
278 // for all other menus. We make a copy here because the "Services" menu
279 // will not yet have been populated at this time. If we don't we get
280 // problems trying to set key equivalents later on because they might clash
281 // with items on the "Services" menu.
282 appMenuItemTemplate = [defaultMainMenu itemAtIndex:0];
283 appMenuItemTemplate = [appMenuItemTemplate copy];
285 // Set up the "Open Recent" menu. See
286 // http://lapcatsoftware.com/blog/2007/07/10/
287 // working-without-a-nib-part-5-open-recent-menu/
289 // http://www.cocoabuilder.com/archive/message/cocoa/2007/8/15/187793
290 // for more information.
292 // The menu itself is created in MainMenu.nib but we still seem to have to
293 // hack around a bit to get it to work. (This has to be done in
294 // applicationWillFinishLaunching at the latest, otherwise it doesn't
296 NSMenu *fileMenu = [defaultMainMenu findFileMenu];
298 int idx = [fileMenu indexOfItemWithAction:@selector(fileOpen:)];
299 if (idx >= 0 && idx+1 < [fileMenu numberOfItems])
301 recentFilesMenuItem = [fileMenu itemWithTitle:@"Open Recent"];
302 [[recentFilesMenuItem submenu] performSelector:@selector(_setMenuName:)
303 withObject:@"NSRecentDocumentsMenu"];
305 // Note: The "Recent Files" menu must be moved around since there is no
306 // -[NSApp setRecentFilesMenu:] method. We keep a reference to it to
307 // facilitate this move (see setMainMenu: below).
308 [recentFilesMenuItem retain];
311 #if MM_HANDLE_XCODE_MOD_EVENT
312 [[NSAppleEventManager sharedAppleEventManager]
314 andSelector:@selector(handleXcodeModEvent:replyEvent:)
319 // Register 'mvim://' URL handler
320 [[NSAppleEventManager sharedAppleEventManager]
322 andSelector:@selector(handleGetURLEvent:replyEvent:)
323 forEventClass:kInternetEventClass
324 andEventID:kAEGetURL];
327 - (void)applicationDidFinishLaunching:(NSNotification *)notification
329 [NSApp setServicesProvider:self];
330 #ifdef MM_ENABLE_PLUGINS
331 [[MMPlugInManager sharedManager] loadAllPlugIns];
334 if ([self maxPreloadCacheSize] > 0) {
335 [self scheduleVimControllerPreloadAfterDelay:2];
336 [self startWatchingVimDir];
340 - (BOOL)applicationShouldOpenUntitledFile:(NSApplication *)sender
342 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
343 NSAppleEventManager *aem = [NSAppleEventManager sharedAppleEventManager];
344 NSAppleEventDescriptor *desc = [aem currentAppleEvent];
346 // The user default MMUntitledWindow can be set to control whether an
347 // untitled window should open on 'Open' and 'Reopen' events.
348 int untitledWindowFlag = [ud integerForKey:MMUntitledWindowKey];
349 if ([desc eventID] == kAEOpenApplication
350 && (untitledWindowFlag & MMUntitledWindowOnOpen) == 0)
352 else if ([desc eventID] == kAEReopenApplication
353 && (untitledWindowFlag & MMUntitledWindowOnReopen) == 0)
356 // When a process is started from the command line, the 'Open' event will
357 // contain a parameter to surpress the opening of an untitled window.
358 desc = [desc paramDescriptorForKeyword:keyAEPropData];
359 desc = [desc paramDescriptorForKeyword:keyMMUntitledWindow];
360 if (desc && ![desc booleanValue])
363 // Never open an untitled window if there is at least one open window or if
364 // there are processes that are currently launching.
365 if ([vimControllers count] > 0 || [pidArguments count] > 0)
368 // NOTE! This way it possible to start the app with the command-line
369 // argument '-nowindow yes' and no window will be opened by default.
370 return ![ud boolForKey:MMNoWindowKey];
373 - (BOOL)applicationOpenUntitledFile:(NSApplication *)sender
375 [self newWindow:self];
379 - (void)application:(NSApplication *)sender openFiles:(NSArray *)filenames
381 // Extract ODB/Xcode/Spotlight parameters from the current Apple event,
382 // sort the filenames, and then let openFiles:withArguments: do the heavy
385 if (!(filenames && [filenames count] > 0))
388 // Sort filenames since the Finder doesn't take care in preserving the
389 // order in which files are selected anyway (and "sorted" is more
390 // predictable than "random").
391 if ([filenames count] > 1)
392 filenames = [filenames sortedArrayUsingSelector:
393 @selector(localizedCompare:)];
395 // Extract ODB/Xcode/Spotlight parameters from the current Apple event
396 NSMutableDictionary *arguments = [self extractArgumentsFromOdocEvent:
397 [[NSAppleEventManager sharedAppleEventManager] currentAppleEvent]];
399 if ([self openFiles:filenames withArguments:arguments]) {
400 [NSApp replyToOpenOrPrint:NSApplicationDelegateReplySuccess];
402 // TODO: Notify user of failure?
403 [NSApp replyToOpenOrPrint:NSApplicationDelegateReplyFailure];
407 - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender
409 return (MMTerminateWhenLastWindowClosed ==
410 [[NSUserDefaults standardUserDefaults]
411 integerForKey:MMLastWindowClosedBehaviorKey]);
414 - (NSApplicationTerminateReply)applicationShouldTerminate:
415 (NSApplication *)sender
417 // TODO: Follow Apple's guidelines for 'Graceful Application Termination'
418 // (in particular, allow user to review changes and save).
419 int reply = NSTerminateNow;
420 BOOL modifiedBuffers = NO;
422 // Go through windows, checking for modified buffers. (Each Vim process
423 // tells MacVim when any buffer has been modified and MacVim sets the
424 // 'documentEdited' flag of the window correspondingly.)
425 NSEnumerator *e = [[NSApp windows] objectEnumerator];
427 while ((window = [e nextObject])) {
428 if ([window isDocumentEdited]) {
429 modifiedBuffers = YES;
434 if (modifiedBuffers) {
435 NSAlert *alert = [[NSAlert alloc] init];
436 [alert setAlertStyle:NSWarningAlertStyle];
437 [alert addButtonWithTitle:NSLocalizedString(@"Quit",
439 [alert addButtonWithTitle:NSLocalizedString(@"Cancel",
441 [alert setMessageText:NSLocalizedString(@"Quit without saving?",
442 @"Quit dialog with changed buffers, title")];
443 [alert setInformativeText:NSLocalizedString(
444 @"There are modified buffers, "
445 "if you quit now all changes will be lost. Quit anyway?",
446 @"Quit dialog with changed buffers, text")];
448 if ([alert runModal] != NSAlertFirstButtonReturn)
449 reply = NSTerminateCancel;
453 // No unmodified buffers, but give a warning if there are multiple
454 // windows and/or tabs open.
455 int numWindows = [vimControllers count];
458 // Count the number of open tabs
459 e = [vimControllers objectEnumerator];
461 while ((vc = [e nextObject])) {
462 NSString *eval = [vc evaluateVimExpression:@"tabpagenr('$')"];
464 int count = [eval intValue];
465 if (count > 0 && count < INT_MAX)
470 if (numWindows > 1 || numTabs > 1) {
471 NSAlert *alert = [[NSAlert alloc] init];
472 [alert setAlertStyle:NSWarningAlertStyle];
473 [alert addButtonWithTitle:NSLocalizedString(@"Quit",
475 [alert addButtonWithTitle:NSLocalizedString(@"Cancel",
477 [alert setMessageText:NSLocalizedString(
478 @"Are you sure you want to quit MacVim?",
479 @"Quit dialog with no changed buffers, title")];
481 NSString *info = nil;
482 if (numWindows > 1) {
483 if (numTabs > numWindows)
484 info = [NSString stringWithFormat:NSLocalizedString(
485 @"There are %d windows open in MacVim, with a "
486 "total of %d tabs. Do you want to quit anyway?",
487 @"Quit dialog with no changed buffers, text"),
488 numWindows, numTabs];
490 info = [NSString stringWithFormat:NSLocalizedString(
491 @"There are %d windows open in MacVim. "
492 "Do you want to quit anyway?",
493 @"Quit dialog with no changed buffers, text"),
497 info = [NSString stringWithFormat:NSLocalizedString(
498 @"There are %d tabs open in MacVim. "
499 "Do you want to quit anyway?",
500 @"Quit dialog with no changed buffers, text"),
504 [alert setInformativeText:info];
506 if ([alert runModal] != NSAlertFirstButtonReturn)
507 reply = NSTerminateCancel;
514 // Tell all Vim processes to terminate now (otherwise they'll leave swap
516 if (NSTerminateNow == reply) {
517 e = [vimControllers objectEnumerator];
519 while ((vc = [e nextObject]))
520 [vc sendMessage:TerminateNowMsgID data:nil];
522 e = [cachedVimControllers objectEnumerator];
523 while ((vc = [e nextObject]))
524 [vc sendMessage:TerminateNowMsgID data:nil];
526 // Give Vim processes a chance to terminate before MacVim. If they
527 // haven't terminated by the time applicationWillTerminate: is sent,
528 // they may be forced to quit (see below).
529 usleep(MMTerminationSleepPeriod);
535 - (void)applicationWillTerminate:(NSNotification *)notification
537 [self stopWatchingVimDir];
539 #ifdef MM_ENABLE_PLUGINS
540 [[MMPlugInManager sharedManager] unloadAllPlugIns];
543 #if MM_HANDLE_XCODE_MOD_EVENT
544 [[NSAppleEventManager sharedAppleEventManager]
545 removeEventHandlerForEventClass:'KAHL'
549 // This will invalidate all connections (since they were spawned from this
551 [connection invalidate];
553 // Send a SIGINT to all running Vim processes, so that they are sure to
554 // receive the connectionDidDie: notification (a process has to be checking
555 // the run-loop for this to happen).
556 unsigned i, count = [vimControllers count];
557 for (i = 0; i < count; ++i) {
558 MMVimController *controller = [vimControllers objectAtIndex:i];
559 int pid = [controller pid];
564 if (fontContainerRef) {
565 ATSFontDeactivate(fontContainerRef, NULL, kATSOptionFlagsDefault);
566 fontContainerRef = 0;
569 [NSApp setDelegate:nil];
572 + (MMAppController *)sharedInstance
574 // Note: The app controller is a singleton which is instantiated in
575 // MainMenu.nib where it is also connected as the delegate of NSApp.
576 id delegate = [NSApp delegate];
577 return [delegate isKindOfClass:self] ? (MMAppController*)delegate : nil;
580 - (NSMenu *)defaultMainMenu
582 return defaultMainMenu;
585 - (NSMenuItem *)appMenuItemTemplate
587 return appMenuItemTemplate;
590 - (void)removeVimController:(id)controller
592 int idx = [vimControllers indexOfObject:controller];
593 if (NSNotFound == idx)
596 [controller cleanup];
598 [vimControllers removeObjectAtIndex:idx];
600 if (![vimControllers count]) {
601 // The last editor window just closed so restore the main menu back to
602 // its default state (which is defined in MainMenu.nib).
603 [self setMainMenu:defaultMainMenu];
605 BOOL hide = (MMHideWhenLastWindowClosed ==
606 [[NSUserDefaults standardUserDefaults]
607 integerForKey:MMLastWindowClosedBehaviorKey]);
613 - (void)windowControllerWillOpen:(MMWindowController *)windowController
615 NSPoint topLeft = NSZeroPoint;
616 NSWindow *topWin = [[[self topmostVimController] windowController] window];
617 NSWindow *win = [windowController window];
621 // If there is a window belonging to a Vim process, cascade from it,
622 // otherwise use the autosaved window position (if any).
624 NSRect frame = [topWin frame];
625 topLeft = NSMakePoint(frame.origin.x, NSMaxY(frame));
627 NSString *topLeftString = [[NSUserDefaults standardUserDefaults]
628 stringForKey:MMTopLeftPointKey];
630 topLeft = NSPointFromString(topLeftString);
633 if (!NSEqualPoints(topLeft, NSZeroPoint)) {
634 NSPoint oldTopLeft = topLeft;
636 topLeft = [win cascadeTopLeftFromPoint:topLeft];
638 [win setFrameTopLeftPoint:topLeft];
640 NSPoint screenOrigin = [[win screen] frame].origin;
641 if ([win frame].origin.y < screenOrigin.y) {
642 // Try to avoid shifting the new window downwards if it means that
643 // the bottom of the window will be off the screen. E.g. if the
644 // user has set windows to open maximized in the vertical direction
645 // then the new window will cascade horizontally only.
646 topLeft.y = oldTopLeft.y;
647 [win setFrameTopLeftPoint:topLeft];
650 if ([win frame].origin.y < screenOrigin.y) {
651 // Move the window to the top of the screen if the bottom of the
652 // window is still obscured.
653 topLeft.y = NSMaxY([[win screen] frame]);
654 [win setFrameTopLeftPoint:topLeft];
658 if (1 == [vimControllers count]) {
659 // The first window autosaves its position. (The autosaving
660 // features of Cocoa are not used because we need more control over
661 // what is autosaved and when it is restored.)
662 [windowController setWindowAutosaveKey:MMTopLeftPointKey];
665 if (openSelectionString) {
666 // TODO: Pass this as a parameter instead! Get rid of
667 // 'openSelectionString' etc.
669 // There is some text to paste into this window as a result of the
670 // services menu "Open selection ..." being used.
671 [[windowController vimController] dropString:openSelectionString];
672 [openSelectionString release];
673 openSelectionString = nil;
676 if (shouldActivateWhenNextWindowOpens) {
677 [NSApp activateIgnoringOtherApps:YES];
678 shouldActivateWhenNextWindowOpens = NO;
682 - (void)setMainMenu:(NSMenu *)mainMenu
684 if ([NSApp mainMenu] == mainMenu) return;
686 // If the new menu has a "Recent Files" dummy item, then swap the real item
687 // for the dummy. We are forced to do this since Cocoa initializes the
688 // "Recent Files" menu and there is no way to simply point Cocoa to a new
689 // item each time the menus are swapped.
690 NSMenu *fileMenu = [mainMenu findFileMenu];
691 if (recentFilesMenuItem && fileMenu) {
693 [fileMenu indexOfItemWithAction:@selector(recentFilesDummy:)];
695 NSMenuItem *dummyItem = [[fileMenu itemAtIndex:dummyIdx] retain];
696 [fileMenu removeItemAtIndex:dummyIdx];
698 NSMenu *recentFilesParentMenu = [recentFilesMenuItem menu];
699 int idx = [recentFilesParentMenu indexOfItem:recentFilesMenuItem];
701 [[recentFilesMenuItem retain] autorelease];
702 [recentFilesParentMenu removeItemAtIndex:idx];
703 [recentFilesParentMenu insertItem:dummyItem atIndex:idx];
706 [fileMenu insertItem:recentFilesMenuItem atIndex:dummyIdx];
711 // Now set the new menu. Notice that we keep one menu for each editor
712 // window since each editor can have its own set of menus. When swapping
713 // menus we have to tell Cocoa where the new "MacVim", "Windows", and
714 // "Services" menu are.
715 [NSApp setMainMenu:mainMenu];
717 // Setting the "MacVim" (or "Application") menu ensures that it is typeset
718 // in boldface. (The setAppleMenu: method used to be public but is now
719 // private so this will have to be considered a bit of a hack!)
720 NSMenu *appMenu = [mainMenu findApplicationMenu];
721 [NSApp performSelector:@selector(setAppleMenu:) withObject:appMenu];
723 NSMenu *servicesMenu = [mainMenu findServicesMenu];
724 [NSApp setServicesMenu:servicesMenu];
726 NSMenu *windowsMenu = [mainMenu findWindowsMenu];
728 // Cocoa isn't clever enough to get rid of items it has added to the
729 // "Windows" menu so we have to do it ourselves otherwise there will be
730 // multiple menu items for each window in the "Windows" menu.
731 // This code assumes that the only items Cocoa add are ones which
732 // send off the action makeKeyAndOrderFront:. (Cocoa will not add
733 // another separator item if the last item on the "Windows" menu
734 // already is a separator, so we needen't worry about separators.)
735 int i, count = [windowsMenu numberOfItems];
736 for (i = count-1; i >= 0; --i) {
737 NSMenuItem *item = [windowsMenu itemAtIndex:i];
738 if ([item action] == @selector(makeKeyAndOrderFront:))
739 [windowsMenu removeItem:item];
742 [NSApp setWindowsMenu:windowsMenu];
744 #ifdef MM_ENABLE_PLUGINS
745 // Move plugin menu from old to new main menu.
746 [self removePlugInMenu];
747 [self addPlugInMenuToMenu:mainMenu];
751 - (NSArray *)filterOpenFiles:(NSArray *)filenames
753 return [self filterOpenFiles:filenames openFilesDict:nil];
756 - (BOOL)openFiles:(NSArray *)filenames withArguments:(NSDictionary *)args
758 // Opening files works like this:
759 // a) filter out any already open files
760 // b) open any remaining files
762 // A file is opened in an untitled window if there is one (it may be
763 // currently launching, or it may already be visible), otherwise a new
766 // Each launching Vim process has a dictionary of arguments that are passed
767 // to the process when in checks in (via connectBackend:pid:). The
768 // arguments for each launching process can be looked up by its PID (in the
769 // pidArguments dictionary).
771 NSMutableDictionary *arguments = (args ? [[args mutableCopy] autorelease]
772 : [NSMutableDictionary dictionary]);
775 // a) Filter out any already open files
777 NSString *firstFile = [filenames objectAtIndex:0];
778 MMVimController *firstController = nil;
779 NSDictionary *openFilesDict = nil;
780 filenames = [self filterOpenFiles:filenames openFilesDict:&openFilesDict];
782 // Pass arguments to vim controllers that had files open.
784 NSEnumerator *e = [openFilesDict keyEnumerator];
786 // (Indicate that we do not wish to open any files at the moment.)
787 [arguments setObject:[NSNumber numberWithBool:YES] forKey:@"dontOpen"];
789 while ((key = [e nextObject])) {
790 NSArray *files = [openFilesDict objectForKey:key];
791 [arguments setObject:files forKey:@"filenames"];
793 MMVimController *vc = [key pointerValue];
794 [vc passArguments:arguments];
796 // If this controller holds the first file, then remember it for later.
797 if ([files containsObject:firstFile])
798 firstController = vc;
801 if ([filenames count] == 0) {
802 // Raise the window containing the first file that was already open,
803 // and make sure that the tab containing that file is selected. Only
804 // do this when there are no more files to open, otherwise sometimes
805 // the window with 'firstFile' will be raised, other times it might be
806 // the window that will open with the files in the 'filenames' array.
807 firstFile = [firstFile stringByEscapingSpecialFilenameCharacters];
808 NSString *input = [NSString stringWithFormat:@"<C-\\><C-N>"
809 ":let oldswb=&swb|let &swb=\"useopen,usetab\"|"
810 "tab sb %@|let &swb=oldswb|unl oldswb|"
811 "cal foreground()|redr|f<CR>", firstFile];
813 [firstController addVimInput:input];
818 // Add filenames to "Recent Files" menu, unless they are being edited
819 // remotely (using ODB).
820 if ([arguments objectForKey:@"remoteID"] == nil) {
821 [[NSDocumentController sharedDocumentController]
822 noteNewRecentFilePaths:filenames];
826 // b) Open any remaining files
829 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
830 BOOL openInCurrentWindow = [ud boolForKey:MMOpenInCurrentWindowKey];
832 // The meaning of "layout" is defined by the WIN_* defines in main.c.
833 int layout = [ud integerForKey:MMOpenLayoutKey];
834 BOOL splitVert = [ud boolForKey:MMVerticalSplitKey];
835 if (splitVert && MMLayoutHorizontalSplit == layout)
836 layout = MMLayoutVerticalSplit;
837 if (layout < 0 || (layout > MMLayoutTabs && openInCurrentWindow))
838 layout = MMLayoutTabs;
840 [arguments setObject:[NSNumber numberWithInt:layout] forKey:@"layout"];
841 [arguments setObject:filenames forKey:@"filenames"];
842 // (Indicate that files should be opened from now on.)
843 [arguments setObject:[NSNumber numberWithBool:NO] forKey:@"dontOpen"];
845 if (openInCurrentWindow && (vc = [self topmostVimController])) {
846 // Open files in an already open window.
847 [[[vc windowController] window] makeKeyAndOrderFront:self];
848 [vc passArguments:arguments];
853 int numFiles = [filenames count];
854 if (MMLayoutWindows == layout && numFiles > 1) {
855 // Open one file at a time in a new window, but don't open too many at
856 // once (at most cap+1 windows will open). If the user has increased
857 // the preload cache size we'll take that as a hint that more windows
858 // should be able to open at once.
859 int cap = [self maxPreloadCacheSize] - 1;
860 if (cap < 4) cap = 4;
861 if (cap > numFiles) cap = numFiles;
864 for (i = 0; i < cap; ++i) {
865 NSArray *a = [NSArray arrayWithObject:[filenames objectAtIndex:i]];
866 [arguments setObject:a forKey:@"filenames"];
868 // NOTE: We have to copy the args since we'll mutate them in the
869 // next loop and the below call may retain the arguments while
870 // waiting for a process to start.
871 NSDictionary *args = [[arguments copy] autorelease];
873 openOk = [self openVimControllerWithArguments:args];
877 // Open remaining files in tabs in a new window.
878 if (openOk && numFiles > cap) {
879 NSRange range = { i, numFiles-cap };
880 NSArray *a = [filenames subarrayWithRange:range];
881 [arguments setObject:a forKey:@"filenames"];
882 [arguments setObject:[NSNumber numberWithInt:MMLayoutTabs]
885 openOk = [self openVimControllerWithArguments:arguments];
888 // Open all files at once.
889 openOk = [self openVimControllerWithArguments:arguments];
895 #ifdef MM_ENABLE_PLUGINS
896 - (void)addItemToPlugInMenu:(NSMenuItem *)item
898 NSMenu *menu = [plugInMenuItem submenu];
900 if ([menu numberOfItems] == 1)
901 [self addPlugInMenuToMenu:[NSApp mainMenu]];
904 - (void)removeItemFromPlugInMenu:(NSMenuItem *)item
906 NSMenu *menu = [plugInMenuItem submenu];
907 [menu removeItem:item];
908 if ([menu numberOfItems] == 0)
909 [self removePlugInMenu];
913 - (IBAction)newWindow:(id)sender
915 // A cached controller requires no loading times and results in the new
916 // window popping up instantaneously. If the cache is empty it may take
917 // 1-2 seconds to start a new Vim process.
918 MMVimController *vc = [self takeVimControllerFromCache];
920 [[vc backendProxy] acknowledgeConnection];
922 [self launchVimProcessWithArguments:nil];
926 - (IBAction)newWindowAndActivate:(id)sender
928 [self activateWhenNextWindowOpens];
929 [self newWindow:sender];
932 - (IBAction)fileOpen:(id)sender
935 BOOL trackPwd = [[NSUserDefaults standardUserDefaults]
936 boolForKey:MMDialogsTrackPwdKey];
938 MMVimController *vc = [self keyVimController];
939 if (vc) dir = [[vc vimState] objectForKey:@"pwd"];
942 NSOpenPanel *panel = [NSOpenPanel openPanel];
943 [panel setAllowsMultipleSelection:YES];
944 [panel setAccessoryView:openPanelAccessoryView()];
946 int result = [panel runModalForDirectory:dir file:nil types:nil];
947 if (NSOKButton == result)
948 [self application:NSApp openFiles:[panel filenames]];
951 - (IBAction)selectNextWindow:(id)sender
953 unsigned i, count = [vimControllers count];
956 NSWindow *keyWindow = [NSApp keyWindow];
957 for (i = 0; i < count; ++i) {
958 MMVimController *vc = [vimControllers objectAtIndex:i];
959 if ([[[vc windowController] window] isEqual:keyWindow])
966 MMVimController *vc = [vimControllers objectAtIndex:i];
967 [[vc windowController] showWindow:self];
971 - (IBAction)selectPreviousWindow:(id)sender
973 unsigned i, count = [vimControllers count];
976 NSWindow *keyWindow = [NSApp keyWindow];
977 for (i = 0; i < count; ++i) {
978 MMVimController *vc = [vimControllers objectAtIndex:i];
979 if ([[[vc windowController] window] isEqual:keyWindow])
989 MMVimController *vc = [vimControllers objectAtIndex:i];
990 [[vc windowController] showWindow:self];
994 - (IBAction)orderFrontPreferencePanel:(id)sender
996 [[MMPreferenceController sharedPrefsWindowController] showWindow:self];
999 - (IBAction)openWebsite:(id)sender
1001 [[NSWorkspace sharedWorkspace] openURL:
1002 [NSURL URLWithString:MMWebsiteString]];
1005 - (IBAction)showVimHelp:(id)sender
1007 // Open a new window with the help window maximized.
1008 [self launchVimProcessWithArguments:[NSArray arrayWithObjects:
1009 @"-c", @":h gui_mac", @"-c", @":res", nil]];
1012 - (IBAction)zoomAll:(id)sender
1014 [NSApp makeWindowsPerform:@selector(performZoom:) inOrder:YES];
1017 - (IBAction)atsuiButtonClicked:(id)sender
1019 // This action is called when the user clicks the "use ATSUI renderer"
1020 // button in the advanced preferences pane.
1021 [self rebuildPreloadCache];
1024 - (IBAction)loginShellButtonClicked:(id)sender
1026 // This action is called when the user clicks the "use login shell" button
1027 // in the advanced preferences pane.
1028 [self rebuildPreloadCache];
1031 - (IBAction)quickstartButtonClicked:(id)sender
1033 if ([self maxPreloadCacheSize] > 0) {
1034 [self scheduleVimControllerPreloadAfterDelay:1.0];
1035 [self startWatchingVimDir];
1037 [self cancelVimControllerPreloadRequests];
1038 [self clearPreloadCacheWithCount:-1];
1039 [self stopWatchingVimDir];
1043 - (byref id <MMFrontendProtocol>)
1044 connectBackend:(byref in id <MMBackendProtocol>)backend
1047 //NSLog(@"Connect backend (pid=%d)", pid);
1048 NSNumber *pidKey = [NSNumber numberWithInt:pid];
1049 MMVimController *vc = nil;
1052 [(NSDistantObject*)backend
1053 setProtocolForProxy:@protocol(MMBackendProtocol)];
1055 vc = [[[MMVimController alloc] initWithBackend:backend pid:pid]
1058 if (preloadPid == pid) {
1059 // This backend was preloaded, so add it to the cache and schedule
1060 // another vim process to be preloaded.
1062 [vc setIsPreloading:YES];
1063 [cachedVimControllers addObject:vc];
1064 [self scheduleVimControllerPreloadAfterDelay:1];
1069 [vimControllers addObject:vc];
1071 id args = [pidArguments objectForKey:pidKey];
1072 if (args && [NSNull null] != args)
1073 [vc passArguments:args];
1075 // HACK! MacVim does not get activated if it is launched from the
1076 // terminal, so we forcibly activate here unless it is an untitled
1077 // window opening. Untitled windows are treated differently, else
1078 // MacVim would steal the focus if another app was activated while the
1079 // untitled window was loading.
1080 if (!args || args != [NSNull null])
1081 [self activateWhenNextWindowOpens];
1084 [pidArguments removeObjectForKey:pidKey];
1089 @catch (NSException *e) {
1090 NSLog(@"Exception caught in %s: \"%@\"", _cmd, e);
1093 [vimControllers removeObject:vc];
1095 [pidArguments removeObjectForKey:pidKey];
1101 - (NSArray *)serverList
1103 NSMutableArray *array = [NSMutableArray array];
1105 unsigned i, count = [vimControllers count];
1106 for (i = 0; i < count; ++i) {
1107 MMVimController *controller = [vimControllers objectAtIndex:i];
1108 if ([controller serverName])
1109 [array addObject:[controller serverName]];
1115 - (MMVimController *)keyVimController
1117 NSWindow *keyWindow = [NSApp keyWindow];
1119 unsigned i, count = [vimControllers count];
1120 for (i = 0; i < count; ++i) {
1121 MMVimController *vc = [vimControllers objectAtIndex:i];
1122 if ([[[vc windowController] window] isEqual:keyWindow])
1130 @end // MMAppController
1135 @implementation MMAppController (MMServices)
1137 - (void)openSelection:(NSPasteboard *)pboard userData:(NSString *)userData
1138 error:(NSString **)error
1140 if (![[pboard types] containsObject:NSStringPboardType]) {
1141 NSLog(@"WARNING: Pasteboard contains no object of type "
1142 "NSStringPboardType");
1146 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
1147 BOOL openInCurrentWindow = [ud boolForKey:MMOpenInCurrentWindowKey];
1148 MMVimController *vc;
1150 if (openInCurrentWindow && (vc = [self topmostVimController])) {
1151 [vc sendMessage:AddNewTabMsgID data:nil];
1152 [vc dropString:[pboard stringForType:NSStringPboardType]];
1154 // Save the text, open a new window, and paste the text when the next
1155 // window opens. (If this is called several times in a row, then all
1156 // but the last call may be ignored.)
1157 if (openSelectionString) [openSelectionString release];
1158 openSelectionString = [[pboard stringForType:NSStringPboardType] copy];
1160 [self newWindow:self];
1164 - (void)openFile:(NSPasteboard *)pboard userData:(NSString *)userData
1165 error:(NSString **)error
1167 if (![[pboard types] containsObject:NSStringPboardType]) {
1168 NSLog(@"WARNING: Pasteboard contains no object of type "
1169 "NSStringPboardType");
1173 // TODO: Parse multiple filenames and create array with names.
1174 NSString *string = [pboard stringForType:NSStringPboardType];
1175 string = [string stringByTrimmingCharactersInSet:
1176 [NSCharacterSet whitespaceAndNewlineCharacterSet]];
1177 string = [string stringByStandardizingPath];
1179 NSArray *filenames = [self filterFilesAndNotify:
1180 [NSArray arrayWithObject:string]];
1181 if ([filenames count] == 0)
1184 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
1185 BOOL openInCurrentWindow = [ud boolForKey:MMOpenInCurrentWindowKey];
1186 MMVimController *vc;
1188 if (openInCurrentWindow && (vc = [self topmostVimController])) {
1189 [vc dropFiles:filenames forceOpen:YES];
1191 [self openFiles:filenames withArguments:nil];
1195 - (void)newFileHere:(NSPasteboard *)pboard userData:(NSString *)userData
1196 error:(NSString **)error
1198 if (![[pboard types] containsObject:NSStringPboardType]) {
1199 NSLog(@"WARNING: Pasteboard contains no object of type "
1200 "NSStringPboardType");
1204 NSString *path = [pboard stringForType:NSStringPboardType];
1207 if (![[NSFileManager defaultManager] fileExistsAtPath:path
1208 isDirectory:&dirIndicator]) {
1209 NSLog(@"Invalid path. Cannot open new document at: %@", path);
1214 path = [path stringByDeletingLastPathComponent];
1216 path = [path stringByEscapingSpecialFilenameCharacters];
1218 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
1219 BOOL openInCurrentWindow = [ud boolForKey:MMOpenInCurrentWindowKey];
1220 MMVimController *vc;
1222 if (openInCurrentWindow && (vc = [self topmostVimController])) {
1223 NSString *input = [NSString stringWithFormat:@"<C-\\><C-N>"
1224 ":tabe|cd %@<CR>", path];
1225 [vc addVimInput:input];
1227 NSString *input = [NSString stringWithFormat:@":cd %@", path];
1228 [self launchVimProcessWithArguments:[NSArray arrayWithObjects:
1229 @"-c", input, nil]];
1233 @end // MMAppController (MMServices)
1238 @implementation MMAppController (Private)
1240 - (MMVimController *)topmostVimController
1242 // Find the topmost visible window which has an associated vim controller.
1243 NSEnumerator *e = [[NSApp orderedWindows] objectEnumerator];
1245 while ((window = [e nextObject]) && [window isVisible]) {
1246 unsigned i, count = [vimControllers count];
1247 for (i = 0; i < count; ++i) {
1248 MMVimController *vc = [vimControllers objectAtIndex:i];
1249 if ([[[vc windowController] window] isEqual:window])
1257 - (int)launchVimProcessWithArguments:(NSArray *)args
1260 NSString *path = [[NSBundle mainBundle] pathForAuxiliaryExecutable:@"Vim"];
1263 NSLog(@"ERROR: Vim executable could not be found inside app bundle!");
1267 NSArray *taskArgs = [NSArray arrayWithObjects:@"-g", @"-f", nil];
1269 taskArgs = [taskArgs arrayByAddingObjectsFromArray:args];
1271 BOOL useLoginShell = [[NSUserDefaults standardUserDefaults]
1272 boolForKey:MMLoginShellKey];
1273 if (useLoginShell) {
1274 // Run process with a login shell, roughly:
1275 // echo "exec Vim -g -f args" | ARGV0=-`basename $SHELL` $SHELL [-l]
1276 pid = executeInLoginShell(path, taskArgs);
1278 // Run process directly:
1280 NSTask *task = [NSTask launchedTaskWithLaunchPath:path
1281 arguments:taskArgs];
1282 pid = task ? [task processIdentifier] : -1;
1286 // NOTE: If the process has no arguments, then add a null argument to
1287 // the pidArguments dictionary. This is later used to detect that a
1288 // process without arguments is being launched.
1290 [pidArguments setObject:[NSNull null]
1291 forKey:[NSNumber numberWithInt:pid]];
1293 NSLog(@"WARNING: %s%@ failed (useLoginShell=%d)", _cmd, args,
1300 - (NSArray *)filterFilesAndNotify:(NSArray *)filenames
1302 // Go trough 'filenames' array and make sure each file exists. Present
1303 // warning dialog if some file was missing.
1305 NSString *firstMissingFile = nil;
1306 NSMutableArray *files = [NSMutableArray array];
1307 unsigned i, count = [filenames count];
1309 for (i = 0; i < count; ++i) {
1310 NSString *name = [filenames objectAtIndex:i];
1311 if ([[NSFileManager defaultManager] fileExistsAtPath:name]) {
1312 [files addObject:name];
1313 } else if (!firstMissingFile) {
1314 firstMissingFile = name;
1318 if (firstMissingFile) {
1319 NSAlert *alert = [[NSAlert alloc] init];
1320 [alert addButtonWithTitle:NSLocalizedString(@"OK",
1324 if ([files count] >= count-1) {
1325 [alert setMessageText:NSLocalizedString(@"File not found",
1326 @"File not found dialog, title")];
1327 text = [NSString stringWithFormat:NSLocalizedString(
1328 @"Could not open file with name %@.",
1329 @"File not found dialog, text"), firstMissingFile];
1331 [alert setMessageText:NSLocalizedString(@"Multiple files not found",
1332 @"File not found dialog, title")];
1333 text = [NSString stringWithFormat:NSLocalizedString(
1334 @"Could not open file with name %@, and %d other files.",
1335 @"File not found dialog, text"),
1336 firstMissingFile, count-[files count]-1];
1339 [alert setInformativeText:text];
1340 [alert setAlertStyle:NSWarningAlertStyle];
1345 [NSApp replyToOpenOrPrint:NSApplicationDelegateReplyFailure];
1351 - (NSArray *)filterOpenFiles:(NSArray *)filenames
1352 openFilesDict:(NSDictionary **)openFiles
1354 // Filter out any files in the 'filenames' array that are open and return
1355 // all files that are not already open. On return, the 'openFiles'
1356 // parameter (if non-nil) will point to a dictionary of open files, indexed
1357 // by Vim controller.
1359 NSMutableDictionary *dict = [NSMutableDictionary dictionary];
1360 NSMutableArray *files = [filenames mutableCopy];
1362 // TODO: Escape special characters in 'files'?
1363 NSString *expr = [NSString stringWithFormat:
1364 @"map([\"%@\"],\"bufloaded(v:val)\")",
1365 [files componentsJoinedByString:@"\",\""]];
1367 unsigned i, count = [vimControllers count];
1368 for (i = 0; i < count && [files count] > 0; ++i) {
1369 MMVimController *vc = [vimControllers objectAtIndex:i];
1371 // Query Vim for which files in the 'files' array are open.
1372 NSString *eval = [vc evaluateVimExpression:expr];
1373 if (!eval) continue;
1375 NSIndexSet *idxSet = [NSIndexSet indexSetWithVimList:eval];
1376 if ([idxSet count] > 0) {
1377 [dict setObject:[files objectsAtIndexes:idxSet]
1378 forKey:[NSValue valueWithPointer:vc]];
1380 // Remove all the files that were open in this Vim process and
1381 // create a new expression to evaluate.
1382 [files removeObjectsAtIndexes:idxSet];
1383 expr = [NSString stringWithFormat:
1384 @"map([\"%@\"],\"bufloaded(v:val)\")",
1385 [files componentsJoinedByString:@"\",\""]];
1389 if (openFiles != nil)
1395 #if MM_HANDLE_XCODE_MOD_EVENT
1396 - (void)handleXcodeModEvent:(NSAppleEventDescriptor *)event
1397 replyEvent:(NSAppleEventDescriptor *)reply
1400 // Xcode sends this event to query MacVim which open files have been
1402 NSLog(@"reply:%@", reply);
1403 NSLog(@"event:%@", event);
1405 NSEnumerator *e = [vimControllers objectEnumerator];
1407 while ((vc = [e nextObject])) {
1408 DescType type = [reply descriptorType];
1409 unsigned len = [[type data] length];
1410 NSMutableData *data = [NSMutableData data];
1412 [data appendBytes:&type length:sizeof(DescType)];
1413 [data appendBytes:&len length:sizeof(unsigned)];
1414 [data appendBytes:[reply data] length:len];
1416 [vc sendMessage:XcodeModMsgID data:data];
1422 - (void)handleGetURLEvent:(NSAppleEventDescriptor *)event
1423 replyEvent:(NSAppleEventDescriptor *)reply
1425 NSString *urlString = [[event paramDescriptorForKeyword:keyDirectObject]
1427 NSURL *url = [NSURL URLWithString:urlString];
1429 // We try to be compatible with TextMate's URL scheme here, as documented
1430 // at http://blog.macromates.com/2007/the-textmate-url-scheme/ . Currently,
1433 // The format is: mvim://open?<arguments> where arguments can be:
1435 // * url — the actual file to open (i.e. a file://… URL), if you leave
1436 // out this argument, the frontmost document is implied.
1437 // * line — line number to go to (one based).
1438 // * column — column number to go to (one based).
1440 // Example: mvim://open?url=file:///etc/profile&line=20
1442 if ([[url host] isEqualToString:@"open"]) {
1443 NSMutableDictionary *dict = [NSMutableDictionary dictionary];
1445 // Parse query ("url=file://...&line=14") into a dictionary
1446 NSArray *queries = [[url query] componentsSeparatedByString:@"&"];
1447 NSEnumerator *enumerator = [queries objectEnumerator];
1449 while( param = [enumerator nextObject] ) {
1450 NSArray *arr = [param componentsSeparatedByString:@"="];
1451 if ([arr count] == 2) {
1452 [dict setValue:[[arr lastObject]
1453 stringByReplacingPercentEscapesUsingEncoding:
1454 NSUTF8StringEncoding]
1455 forKey:[[arr objectAtIndex:0]
1456 stringByReplacingPercentEscapesUsingEncoding:
1457 NSUTF8StringEncoding]];
1461 // Actually open the file.
1462 NSString *file = [dict objectForKey:@"url"];
1464 NSURL *fileUrl= [NSURL URLWithString:file];
1465 // TextMate only opens files that already exist.
1466 if ([fileUrl isFileURL]
1467 && [[NSFileManager defaultManager] fileExistsAtPath:
1469 // Strip 'file://' path, else application:openFiles: might think
1470 // the file is not yet open.
1471 NSArray *filenames = [NSArray arrayWithObject:[fileUrl path]];
1473 // Look for the line and column options.
1474 NSDictionary *args = nil;
1475 NSString *line = [dict objectForKey:@"line"];
1477 NSString *column = [dict objectForKey:@"column"];
1479 args = [NSDictionary dictionaryWithObjectsAndKeys:
1480 line, @"cursorLine",
1481 column, @"cursorColumn",
1484 args = [NSDictionary dictionaryWithObject:line
1485 forKey:@"cursorLine"];
1488 [self openFiles:filenames withArguments:args];
1492 NSAlert *alert = [[NSAlert alloc] init];
1493 [alert addButtonWithTitle:NSLocalizedString(@"OK",
1496 [alert setMessageText:NSLocalizedString(@"Unknown URL Scheme",
1497 @"Unknown URL Scheme dialog, title")];
1498 [alert setInformativeText:[NSString stringWithFormat:NSLocalizedString(
1499 @"This version of MacVim does not support \"%@\""
1500 @" in its URL scheme.",
1501 @"Unknown URL Scheme dialog, text"),
1504 [alert setAlertStyle:NSWarningAlertStyle];
1511 - (int)findLaunchingProcessWithoutArguments
1513 NSArray *keys = [pidArguments allKeysForObject:[NSNull null]];
1514 if ([keys count] > 0) {
1515 //NSLog(@"found launching process without arguments");
1516 return [[keys objectAtIndex:0] intValue];
1522 - (MMVimController *)findUnusedEditor
1524 NSEnumerator *e = [vimControllers objectEnumerator];
1526 while ((vc = [e nextObject])) {
1527 if ([[[vc vimState] objectForKey:@"unusedEditor"] boolValue])
1534 - (NSMutableDictionary *)extractArgumentsFromOdocEvent:
1535 (NSAppleEventDescriptor *)desc
1537 NSMutableDictionary *dict = [NSMutableDictionary dictionary];
1539 // 1. Extract ODB parameters (if any)
1540 NSAppleEventDescriptor *odbdesc = desc;
1541 if (![odbdesc paramDescriptorForKeyword:keyFileSender]) {
1542 // The ODB paramaters may hide inside the 'keyAEPropData' descriptor.
1543 odbdesc = [odbdesc paramDescriptorForKeyword:keyAEPropData];
1544 if (![odbdesc paramDescriptorForKeyword:keyFileSender])
1549 NSAppleEventDescriptor *p =
1550 [odbdesc paramDescriptorForKeyword:keyFileSender];
1552 [dict setObject:[NSNumber numberWithUnsignedInt:[p typeCodeValue]]
1553 forKey:@"remoteID"];
1555 p = [odbdesc paramDescriptorForKeyword:keyFileCustomPath];
1557 [dict setObject:[p stringValue] forKey:@"remotePath"];
1559 p = [odbdesc paramDescriptorForKeyword:keyFileSenderToken];
1561 [dict setObject:[NSNumber numberWithUnsignedLong:[p descriptorType]]
1562 forKey:@"remoteTokenDescType"];
1563 [dict setObject:[p data] forKey:@"remoteTokenData"];
1567 // 2. Extract Xcode parameters (if any)
1568 NSAppleEventDescriptor *xcodedesc =
1569 [desc paramDescriptorForKeyword:keyAEPosition];
1572 MMSelectionRange *sr = (MMSelectionRange*)[[xcodedesc data] bytes];
1574 if (sr->lineNum < 0) {
1575 // Should select a range of lines.
1576 range.location = sr->startRange + 1;
1577 range.length = sr->endRange - sr->startRange + 1;
1579 // Should only move cursor to a line.
1580 range.location = sr->lineNum + 1;
1584 [dict setObject:NSStringFromRange(range) forKey:@"selectionRange"];
1587 // 3. Extract Spotlight search text (if any)
1588 NSAppleEventDescriptor *spotlightdesc =
1589 [desc paramDescriptorForKeyword:keyAESearchText];
1591 [dict setObject:[spotlightdesc stringValue] forKey:@"searchText"];
1596 #ifdef MM_ENABLE_PLUGINS
1597 - (void)removePlugInMenu
1599 if ([plugInMenuItem menu])
1600 [[plugInMenuItem menu] removeItem:plugInMenuItem];
1603 - (void)addPlugInMenuToMenu:(NSMenu *)mainMenu
1605 NSMenu *windowsMenu = [mainMenu findWindowsMenu];
1607 if ([[plugInMenuItem submenu] numberOfItems] > 0) {
1608 int idx = windowsMenu ? [mainMenu indexOfItemWithSubmenu:windowsMenu]
1611 [mainMenu insertItem:plugInMenuItem atIndex:idx];
1613 [mainMenu addItem:plugInMenuItem];
1619 - (void)scheduleVimControllerPreloadAfterDelay:(NSTimeInterval)delay
1621 [self performSelector:@selector(preloadVimController:)
1626 - (void)cancelVimControllerPreloadRequests
1628 [NSObject cancelPreviousPerformRequestsWithTarget:self
1629 selector:@selector(preloadVimController:)
1633 - (void)preloadVimController:(id)sender
1635 // We only allow preloading of one Vim process at a time (to avoid hogging
1636 // CPU), so schedule another preload in a little while if necessary.
1637 if (-1 != preloadPid) {
1638 [self scheduleVimControllerPreloadAfterDelay:2];
1642 if ([cachedVimControllers count] >= [self maxPreloadCacheSize])
1645 preloadPid = [self launchVimProcessWithArguments:
1646 [NSArray arrayWithObject:@"--mmwaitforack"]];
1649 - (int)maxPreloadCacheSize
1651 // The maximum number of Vim processes to keep in the cache can be
1652 // controlled via the user default "MMPreloadCacheSize".
1653 int maxCacheSize = [[NSUserDefaults standardUserDefaults]
1654 integerForKey:MMPreloadCacheSizeKey];
1655 if (maxCacheSize < 0) maxCacheSize = 0;
1656 else if (maxCacheSize > 10) maxCacheSize = 10;
1658 return maxCacheSize;
1661 - (MMVimController *)takeVimControllerFromCache
1663 // NOTE: After calling this message the backend corresponding to the
1664 // returned vim controller must be sent an acknowledgeConnection message,
1665 // else the vim process will be stuck.
1667 // This method may return nil even though the cache might be non-empty; the
1668 // caller should handle this by starting a new Vim process.
1670 int i, count = [cachedVimControllers count];
1671 if (0 == count) return nil;
1673 // Locate the first Vim controller with up-to-date rc-files sourced.
1674 NSDate *rcDate = [self rcFilesModificationDate];
1675 for (i = 0; i < count; ++i) {
1676 MMVimController *vc = [cachedVimControllers objectAtIndex:i];
1677 NSDate *date = [vc creationDate];
1678 if ([date compare:rcDate] != NSOrderedAscending)
1683 // Clear out cache entries whose vimrc/gvimrc files were sourced before
1684 // the latest modification date for those files. This ensures that the
1685 // latest rc-files are always sourced for new windows.
1686 [self clearPreloadCacheWithCount:i];
1689 if ([cachedVimControllers count] == 0) {
1690 [self scheduleVimControllerPreloadAfterDelay:2.0];
1694 MMVimController *vc = [cachedVimControllers objectAtIndex:0];
1695 [vimControllers addObject:vc];
1696 [cachedVimControllers removeObjectAtIndex:0];
1697 [vc setIsPreloading:NO];
1699 // If the Vim process has finished loading then the window will displayed
1700 // now, otherwise it will be displayed when the OpenWindowMsgID message is
1702 [[vc windowController] showWindow];
1704 // Since we've taken one controller from the cache we take the opportunity
1705 // to preload another.
1706 [self scheduleVimControllerPreloadAfterDelay:1];
1711 - (void)clearPreloadCacheWithCount:(int)count
1713 // Remove the 'count' first entries in the preload cache. It is assumed
1714 // that objects are added/removed from the cache in a FIFO manner so that
1715 // this effectively clears the 'count' oldest entries.
1716 // If 'count' is negative, then the entire cache is cleared.
1718 if ([cachedVimControllers count] == 0 || count == 0)
1722 count = [cachedVimControllers count];
1724 // Make sure the preloaded Vim processes get killed or they'll just hang
1725 // around being useless until MacVim is terminated.
1726 NSEnumerator *e = [cachedVimControllers objectEnumerator];
1727 MMVimController *vc;
1729 while ((vc = [e nextObject]) && n-- > 0) {
1730 [[NSNotificationCenter defaultCenter] removeObserver:vc];
1731 [vc sendMessage:TerminateNowMsgID data:nil];
1733 // Since the preloaded processes were killed "prematurely" we have to
1734 // manually tell them to cleanup (it is not enough to simply release
1735 // them since deallocation and cleanup are separated).
1740 while (n-- > 0 && [cachedVimControllers count] > 0)
1741 [cachedVimControllers removeObjectAtIndex:0];
1744 - (void)rebuildPreloadCache
1746 if ([self maxPreloadCacheSize] > 0) {
1747 [self clearPreloadCacheWithCount:-1];
1748 [self cancelVimControllerPreloadRequests];
1749 [self scheduleVimControllerPreloadAfterDelay:1.0];
1753 - (NSDate *)rcFilesModificationDate
1755 // Check modification dates for ~/.vimrc and ~/.gvimrc and return the
1756 // latest modification date. If ~/.vimrc does not exist, check ~/_vimrc
1757 // and similarly for gvimrc.
1758 // Returns distantPath if no rc files were found.
1760 NSDate *date = [NSDate distantPast];
1761 NSFileManager *fm = [NSFileManager defaultManager];
1763 NSString *path = [@"~/.vimrc" stringByExpandingTildeInPath];
1764 NSDictionary *attr = [fm fileAttributesAtPath:path traverseLink:YES];
1766 path = [@"~/_vimrc" stringByExpandingTildeInPath];
1767 attr = [fm fileAttributesAtPath:path traverseLink:YES];
1769 NSDate *modDate = [attr objectForKey:NSFileModificationDate];
1773 path = [@"~/.gvimrc" stringByExpandingTildeInPath];
1774 attr = [fm fileAttributesAtPath:path traverseLink:YES];
1776 path = [@"~/_gvimrc" stringByExpandingTildeInPath];
1777 attr = [fm fileAttributesAtPath:path traverseLink:YES];
1779 modDate = [attr objectForKey:NSFileModificationDate];
1781 date = [date laterDate:modDate];
1786 - (BOOL)openVimControllerWithArguments:(NSDictionary *)arguments
1788 MMVimController *vc = [self findUnusedEditor];
1790 // Open files in an already open window.
1791 [[[vc windowController] window] makeKeyAndOrderFront:self];
1792 [vc passArguments:arguments];
1793 } else if ((vc = [self takeVimControllerFromCache])) {
1794 // Open files in a new window using a cached vim controller. This
1795 // requires virtually no loading time so the new window will pop up
1797 [vc passArguments:arguments];
1798 [[vc backendProxy] acknowledgeConnection];
1800 // Open files in a launching Vim process or start a new process. This
1801 // may take 1-2 seconds so there will be a visible delay before the
1802 // window appears on screen.
1803 int pid = [self findLaunchingProcessWithoutArguments];
1805 pid = [self launchVimProcessWithArguments:nil];
1810 // TODO: If the Vim process fails to start, or if it changes PID,
1811 // then the memory allocated for these parameters will leak.
1812 // Ensure that this cannot happen or somehow detect it.
1814 if ([arguments count] > 0)
1815 [pidArguments setObject:arguments
1816 forKey:[NSNumber numberWithInt:pid]];
1822 - (void)activateWhenNextWindowOpens
1824 shouldActivateWhenNextWindowOpens = YES;
1827 - (void)startWatchingVimDir
1829 //NSLog(@"%s", _cmd);
1830 #if (MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_4)
1833 if (NULL == FSEventStreamStart)
1834 return; // FSEvent functions are weakly linked
1836 NSString *path = [@"~/.vim" stringByExpandingTildeInPath];
1837 NSArray *pathsToWatch = [NSArray arrayWithObject:path];
1839 fsEventStream = FSEventStreamCreate(NULL, &fsEventCallback, NULL,
1840 (CFArrayRef)pathsToWatch, kFSEventStreamEventIdSinceNow,
1841 MMEventStreamLatency, kFSEventStreamCreateFlagNone);
1843 FSEventStreamScheduleWithRunLoop(fsEventStream,
1844 [[NSRunLoop currentRunLoop] getCFRunLoop],
1845 kCFRunLoopDefaultMode);
1847 FSEventStreamStart(fsEventStream);
1848 //NSLog(@"Started FS event stream");
1852 - (void)stopWatchingVimDir
1854 //NSLog(@"%s", _cmd);
1855 #if (MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_4)
1856 if (NULL == FSEventStreamStop)
1857 return; // FSEvent functions are weakly linked
1859 if (fsEventStream) {
1860 FSEventStreamStop(fsEventStream);
1861 FSEventStreamInvalidate(fsEventStream);
1862 FSEventStreamRelease(fsEventStream);
1863 fsEventStream = NULL;
1864 //NSLog(@"Stopped FS event stream");
1870 - (void)handleFSEvent
1872 //NSLog(@"%s", _cmd);
1873 [self clearPreloadCacheWithCount:-1];
1875 // Several FS events may arrive in quick succession so make sure to cancel
1876 // any previous preload requests before making a new one.
1877 [self cancelVimControllerPreloadRequests];
1878 [self scheduleVimControllerPreloadAfterDelay:0.5];
1881 @end // MMAppController (Private)
1887 executeInLoginShell(NSString *path, NSArray *args)
1889 // Start a login shell and execute the command 'path' with arguments 'args'
1890 // in the shell. This ensures that user environment variables are set even
1891 // when MacVim was started from the Finder.
1894 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
1896 // Determine which shell to use to execute the command. The user
1897 // may decide which shell to use by setting a user default or the
1898 // $SHELL environment variable.
1899 NSString *shell = [ud stringForKey:MMLoginShellCommandKey];
1900 if (!shell || [shell length] == 0)
1901 shell = [[[NSProcessInfo processInfo] environment]
1902 objectForKey:@"SHELL"];
1904 shell = @"/bin/bash";
1906 //NSLog(@"shell = %@", shell);
1908 // Bash needs the '-l' flag to launch a login shell. The user may add
1909 // flags by setting a user default.
1910 NSString *shellArgument = [ud stringForKey:MMLoginShellArgumentKey];
1911 if (!shellArgument || [shellArgument length] == 0) {
1912 if ([[shell lastPathComponent] isEqual:@"bash"])
1913 shellArgument = @"-l";
1915 shellArgument = nil;
1918 //NSLog(@"shellArgument = %@", shellArgument);
1920 // Build input string to pipe to the login shell.
1921 NSMutableString *input = [NSMutableString stringWithFormat:
1922 @"exec \"%@\"", path];
1924 // Append all arguments, making sure they are properly quoted, even
1925 // when they contain single quotes.
1926 NSEnumerator *e = [args objectEnumerator];
1929 while ((obj = [e nextObject])) {
1930 NSMutableString *arg = [NSMutableString stringWithString:obj];
1931 [arg replaceOccurrencesOfString:@"'" withString:@"'\"'\"'"
1932 options:NSLiteralSearch
1933 range:NSMakeRange(0, [arg length])];
1934 [input appendFormat:@" '%@'", arg];
1938 // Build the argument vector used to start the login shell.
1939 NSString *shellArg0 = [NSString stringWithFormat:@"-%@",
1940 [shell lastPathComponent]];
1941 char *shellArgv[3] = { (char *)[shellArg0 UTF8String], NULL, NULL };
1943 shellArgv[1] = (char *)[shellArgument UTF8String];
1945 // Get the C string representation of the shell path before the fork since
1946 // we must not call Foundation functions after a fork.
1947 const char *shellPath = [shell fileSystemRepresentation];
1949 // Fork and execute the process.
1951 if (pipe(ds)) return -1;
1956 } else if (pid == 0) {
1959 // We need to undo our zombie avoidance as Vim waits for and needs
1960 // the exit statuses of processes it spawns.
1961 signal(SIGCHLD, SIG_DFL);
1963 if (close(ds[1]) == -1) exit(255);
1964 if (dup2(ds[0], 0) == -1) exit(255);
1966 // Without the following call warning messages like this appear on the
1968 // com.apple.launchd[69] : Stray process with PGID equal to this
1969 // dead job: PID 1589 PPID 1 Vim
1972 execv(shellPath, shellArgv);
1974 // Never reached unless execv fails
1978 if (close(ds[0]) == -1) return -1;
1980 // Send input to execute to the child process
1981 [input appendString:@"\n"];
1982 int bytes = [input lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
1984 if (write(ds[1], [input UTF8String], bytes) != bytes) return -1;
1985 if (close(ds[1]) == -1) return -1;