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 NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:
158 [NSNumber numberWithBool:NO], MMNoWindowKey,
159 [NSNumber numberWithInt:64], MMTabMinWidthKey,
160 [NSNumber numberWithInt:6*64], MMTabMaxWidthKey,
161 [NSNumber numberWithInt:132], MMTabOptimumWidthKey,
162 [NSNumber numberWithInt:2], MMTextInsetLeftKey,
163 [NSNumber numberWithInt:1], MMTextInsetRightKey,
164 [NSNumber numberWithInt:1], MMTextInsetTopKey,
165 [NSNumber numberWithInt:1], MMTextInsetBottomKey,
166 @"MMTypesetter", MMTypesetterKey,
167 [NSNumber numberWithFloat:1], MMCellWidthMultiplierKey,
168 [NSNumber numberWithFloat:-1], MMBaselineOffsetKey,
169 [NSNumber numberWithBool:YES], MMTranslateCtrlClickKey,
170 [NSNumber numberWithInt:0], MMOpenInCurrentWindowKey,
171 [NSNumber numberWithBool:NO], MMNoFontSubstitutionKey,
172 [NSNumber numberWithBool:YES], MMLoginShellKey,
173 [NSNumber numberWithBool:NO], MMAtsuiRendererKey,
174 [NSNumber numberWithInt:MMUntitledWindowAlways],
176 [NSNumber numberWithBool:NO], MMTexturedWindowKey,
177 [NSNumber numberWithBool:NO], MMZoomBothKey,
178 @"", MMLoginShellCommandKey,
179 @"", MMLoginShellArgumentKey,
180 [NSNumber numberWithBool:YES], MMDialogsTrackPwdKey,
181 #ifdef MM_ENABLE_PLUGINS
182 [NSNumber numberWithBool:YES], MMShowLeftPlugInContainerKey,
184 [NSNumber numberWithInt:3], MMOpenLayoutKey,
185 [NSNumber numberWithBool:NO], MMVerticalSplitKey,
186 [NSNumber numberWithInt:0], MMPreloadCacheSizeKey,
187 [NSNumber numberWithInt:0], MMLastWindowClosedBehaviorKey,
188 [NSNumber numberWithInt:MMDisableFakeEsc],
189 MMFakeEscModifierKey,
190 [NSNumber numberWithFloat:0.3], MMFakeEscTimeoutKey,
191 [NSNumber numberWithBool:NO], MMFakeEscOnKeyDownKey,
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: When the user is resizing the window the AppKit puts the run
239 // loop in event tracking mode. Unless the connection listens to
240 // request in this mode, live resizing won't work.
241 [connection addRequestMode:NSEventTrackingRunLoopMode];
243 // NOTE! If the name of the connection changes here it must also be
244 // updated in MMBackend.m.
245 NSString *name = [NSString stringWithFormat:@"%@-connection",
246 [[NSBundle mainBundle] bundlePath]];
247 //NSLog(@"Registering connection with name '%@'", name);
248 if (![connection registerName:name]) {
249 NSLog(@"FATAL ERROR: Failed to register connection with name '%@'",
251 [connection release]; connection = nil;
259 //NSLog(@"MMAppController dealloc");
261 [connection release]; connection = nil;
262 [pidArguments release]; pidArguments = nil;
263 [vimControllers release]; vimControllers = nil;
264 [cachedVimControllers release]; cachedVimControllers = nil;
265 [openSelectionString release]; openSelectionString = nil;
266 [recentFilesMenuItem release]; recentFilesMenuItem = nil;
267 [defaultMainMenu release]; defaultMainMenu = nil;
268 #ifdef MM_ENABLE_PLUGINS
269 [plugInMenuItem release]; plugInMenuItem = nil;
271 [appMenuItemTemplate release]; appMenuItemTemplate = nil;
276 - (void)applicationWillFinishLaunching:(NSNotification *)notification
278 // Remember the default menu so that it can be restored if the user closes
279 // all editor windows.
280 defaultMainMenu = [[NSApp mainMenu] retain];
282 // Store a copy of the default app menu so we can use this as a template
283 // for all other menus. We make a copy here because the "Services" menu
284 // will not yet have been populated at this time. If we don't we get
285 // problems trying to set key equivalents later on because they might clash
286 // with items on the "Services" menu.
287 appMenuItemTemplate = [defaultMainMenu itemAtIndex:0];
288 appMenuItemTemplate = [appMenuItemTemplate copy];
290 // Set up the "Open Recent" menu. See
291 // http://lapcatsoftware.com/blog/2007/07/10/
292 // working-without-a-nib-part-5-open-recent-menu/
294 // http://www.cocoabuilder.com/archive/message/cocoa/2007/8/15/187793
295 // for more information.
297 // The menu itself is created in MainMenu.nib but we still seem to have to
298 // hack around a bit to get it to work. (This has to be done in
299 // applicationWillFinishLaunching at the latest, otherwise it doesn't
301 NSMenu *fileMenu = [defaultMainMenu findFileMenu];
303 int idx = [fileMenu indexOfItemWithAction:@selector(fileOpen:)];
304 if (idx >= 0 && idx+1 < [fileMenu numberOfItems])
306 recentFilesMenuItem = [fileMenu itemWithTitle:@"Open Recent"];
307 [[recentFilesMenuItem submenu] performSelector:@selector(_setMenuName:)
308 withObject:@"NSRecentDocumentsMenu"];
310 // Note: The "Recent Files" menu must be moved around since there is no
311 // -[NSApp setRecentFilesMenu:] method. We keep a reference to it to
312 // facilitate this move (see setMainMenu: below).
313 [recentFilesMenuItem retain];
316 #if MM_HANDLE_XCODE_MOD_EVENT
317 [[NSAppleEventManager sharedAppleEventManager]
319 andSelector:@selector(handleXcodeModEvent:replyEvent:)
324 // Register 'mvim://' URL handler
325 [[NSAppleEventManager sharedAppleEventManager]
327 andSelector:@selector(handleGetURLEvent:replyEvent:)
328 forEventClass:kInternetEventClass
329 andEventID:kAEGetURL];
332 - (void)applicationDidFinishLaunching:(NSNotification *)notification
334 [NSApp setServicesProvider:self];
335 #ifdef MM_ENABLE_PLUGINS
336 [[MMPlugInManager sharedManager] loadAllPlugIns];
339 if ([self maxPreloadCacheSize] > 0) {
340 [self scheduleVimControllerPreloadAfterDelay:2];
341 [self startWatchingVimDir];
345 - (BOOL)applicationShouldOpenUntitledFile:(NSApplication *)sender
347 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
348 NSAppleEventManager *aem = [NSAppleEventManager sharedAppleEventManager];
349 NSAppleEventDescriptor *desc = [aem currentAppleEvent];
351 // The user default MMUntitledWindow can be set to control whether an
352 // untitled window should open on 'Open' and 'Reopen' events.
353 int untitledWindowFlag = [ud integerForKey:MMUntitledWindowKey];
354 if ([desc eventID] == kAEOpenApplication
355 && (untitledWindowFlag & MMUntitledWindowOnOpen) == 0)
357 else if ([desc eventID] == kAEReopenApplication
358 && (untitledWindowFlag & MMUntitledWindowOnReopen) == 0)
361 // When a process is started from the command line, the 'Open' event will
362 // contain a parameter to surpress the opening of an untitled window.
363 desc = [desc paramDescriptorForKeyword:keyAEPropData];
364 desc = [desc paramDescriptorForKeyword:keyMMUntitledWindow];
365 if (desc && ![desc booleanValue])
368 // Never open an untitled window if there is at least one open window or if
369 // there are processes that are currently launching.
370 if ([vimControllers count] > 0 || [pidArguments count] > 0)
373 // NOTE! This way it possible to start the app with the command-line
374 // argument '-nowindow yes' and no window will be opened by default.
375 return ![ud boolForKey:MMNoWindowKey];
378 - (BOOL)applicationOpenUntitledFile:(NSApplication *)sender
380 [self newWindow:self];
384 - (void)application:(NSApplication *)sender openFiles:(NSArray *)filenames
386 // Extract ODB/Xcode/Spotlight parameters from the current Apple event,
387 // sort the filenames, and then let openFiles:withArguments: do the heavy
390 if (!(filenames && [filenames count] > 0))
393 // Sort filenames since the Finder doesn't take care in preserving the
394 // order in which files are selected anyway (and "sorted" is more
395 // predictable than "random").
396 if ([filenames count] > 1)
397 filenames = [filenames sortedArrayUsingSelector:
398 @selector(localizedCompare:)];
400 // Extract ODB/Xcode/Spotlight parameters from the current Apple event
401 NSMutableDictionary *arguments = [self extractArgumentsFromOdocEvent:
402 [[NSAppleEventManager sharedAppleEventManager] currentAppleEvent]];
404 if ([self openFiles:filenames withArguments:arguments]) {
405 [NSApp replyToOpenOrPrint:NSApplicationDelegateReplySuccess];
407 // TODO: Notify user of failure?
408 [NSApp replyToOpenOrPrint:NSApplicationDelegateReplyFailure];
412 - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender
414 return (MMTerminateWhenLastWindowClosed ==
415 [[NSUserDefaults standardUserDefaults]
416 integerForKey:MMLastWindowClosedBehaviorKey]);
419 - (NSApplicationTerminateReply)applicationShouldTerminate:
420 (NSApplication *)sender
422 // TODO: Follow Apple's guidelines for 'Graceful Application Termination'
423 // (in particular, allow user to review changes and save).
424 int reply = NSTerminateNow;
425 BOOL modifiedBuffers = NO;
427 // Go through windows, checking for modified buffers. (Each Vim process
428 // tells MacVim when any buffer has been modified and MacVim sets the
429 // 'documentEdited' flag of the window correspondingly.)
430 NSEnumerator *e = [[NSApp windows] objectEnumerator];
432 while ((window = [e nextObject])) {
433 if ([window isDocumentEdited]) {
434 modifiedBuffers = YES;
439 if (modifiedBuffers) {
440 NSAlert *alert = [[NSAlert alloc] init];
441 [alert setAlertStyle:NSWarningAlertStyle];
442 [alert addButtonWithTitle:NSLocalizedString(@"Quit",
444 [alert addButtonWithTitle:NSLocalizedString(@"Cancel",
446 [alert setMessageText:NSLocalizedString(@"Quit without saving?",
447 @"Quit dialog with changed buffers, title")];
448 [alert setInformativeText:NSLocalizedString(
449 @"There are modified buffers, "
450 "if you quit now all changes will be lost. Quit anyway?",
451 @"Quit dialog with changed buffers, text")];
453 if ([alert runModal] != NSAlertFirstButtonReturn)
454 reply = NSTerminateCancel;
458 // No unmodified buffers, but give a warning if there are multiple
459 // windows and/or tabs open.
460 int numWindows = [vimControllers count];
463 // Count the number of open tabs
464 e = [vimControllers objectEnumerator];
466 while ((vc = [e nextObject])) {
467 NSString *eval = [vc evaluateVimExpression:@"tabpagenr('$')"];
469 int count = [eval intValue];
470 if (count > 0 && count < INT_MAX)
475 if (numWindows > 1 || numTabs > 1) {
476 NSAlert *alert = [[NSAlert alloc] init];
477 [alert setAlertStyle:NSWarningAlertStyle];
478 [alert addButtonWithTitle:NSLocalizedString(@"Quit",
480 [alert addButtonWithTitle:NSLocalizedString(@"Cancel",
482 [alert setMessageText:NSLocalizedString(
483 @"Are you sure you want to quit MacVim?",
484 @"Quit dialog with no changed buffers, title")];
486 NSString *info = nil;
487 if (numWindows > 1) {
488 if (numTabs > numWindows)
489 info = [NSString stringWithFormat:NSLocalizedString(
490 @"There are %d windows open in MacVim, with a "
491 "total of %d tabs. Do you want to quit anyway?",
492 @"Quit dialog with no changed buffers, text"),
493 numWindows, numTabs];
495 info = [NSString stringWithFormat:NSLocalizedString(
496 @"There are %d windows open in MacVim. "
497 "Do you want to quit anyway?",
498 @"Quit dialog with no changed buffers, text"),
502 info = [NSString stringWithFormat:NSLocalizedString(
503 @"There are %d tabs open in MacVim. "
504 "Do you want to quit anyway?",
505 @"Quit dialog with no changed buffers, text"),
509 [alert setInformativeText:info];
511 if ([alert runModal] != NSAlertFirstButtonReturn)
512 reply = NSTerminateCancel;
519 // Tell all Vim processes to terminate now (otherwise they'll leave swap
521 if (NSTerminateNow == reply) {
522 e = [vimControllers objectEnumerator];
524 while ((vc = [e nextObject]))
525 [vc sendMessage:TerminateNowMsgID data:nil];
527 e = [cachedVimControllers objectEnumerator];
528 while ((vc = [e nextObject]))
529 [vc sendMessage:TerminateNowMsgID data:nil];
531 // Give Vim processes a chance to terminate before MacVim. If they
532 // haven't terminated by the time applicationWillTerminate: is sent,
533 // they may be forced to quit (see below).
534 usleep(MMTerminationSleepPeriod);
540 - (void)applicationWillTerminate:(NSNotification *)notification
542 [self stopWatchingVimDir];
544 #ifdef MM_ENABLE_PLUGINS
545 [[MMPlugInManager sharedManager] unloadAllPlugIns];
548 #if MM_HANDLE_XCODE_MOD_EVENT
549 [[NSAppleEventManager sharedAppleEventManager]
550 removeEventHandlerForEventClass:'KAHL'
554 // This will invalidate all connections (since they were spawned from this
556 [connection invalidate];
558 // Send a SIGINT to all running Vim processes, so that they are sure to
559 // receive the connectionDidDie: notification (a process has to be checking
560 // the run-loop for this to happen).
561 unsigned i, count = [vimControllers count];
562 for (i = 0; i < count; ++i) {
563 MMVimController *controller = [vimControllers objectAtIndex:i];
564 int pid = [controller pid];
569 if (fontContainerRef) {
570 ATSFontDeactivate(fontContainerRef, NULL, kATSOptionFlagsDefault);
571 fontContainerRef = 0;
574 [NSApp setDelegate:nil];
577 + (MMAppController *)sharedInstance
579 // Note: The app controller is a singleton which is instantiated in
580 // MainMenu.nib where it is also connected as the delegate of NSApp.
581 id delegate = [NSApp delegate];
582 return [delegate isKindOfClass:self] ? (MMAppController*)delegate : nil;
585 - (NSMenu *)defaultMainMenu
587 return defaultMainMenu;
590 - (NSMenuItem *)appMenuItemTemplate
592 return appMenuItemTemplate;
595 - (void)removeVimController:(id)controller
597 int idx = [vimControllers indexOfObject:controller];
598 if (NSNotFound == idx)
601 [controller cleanup];
603 [vimControllers removeObjectAtIndex:idx];
605 if (![vimControllers count]) {
606 // The last editor window just closed so restore the main menu back to
607 // its default state (which is defined in MainMenu.nib).
608 [self setMainMenu:defaultMainMenu];
610 BOOL hide = (MMHideWhenLastWindowClosed ==
611 [[NSUserDefaults standardUserDefaults]
612 integerForKey:MMLastWindowClosedBehaviorKey]);
618 - (void)windowControllerWillOpen:(MMWindowController *)windowController
620 NSPoint topLeft = NSZeroPoint;
621 NSWindow *topWin = [[[self topmostVimController] windowController] window];
622 NSWindow *win = [windowController window];
626 // If there is a window belonging to a Vim process, cascade from it,
627 // otherwise use the autosaved window position (if any).
629 NSRect frame = [topWin frame];
630 topLeft = NSMakePoint(frame.origin.x, NSMaxY(frame));
632 NSString *topLeftString = [[NSUserDefaults standardUserDefaults]
633 stringForKey:MMTopLeftPointKey];
635 topLeft = NSPointFromString(topLeftString);
638 if (!NSEqualPoints(topLeft, NSZeroPoint)) {
639 NSPoint oldTopLeft = topLeft;
641 topLeft = [win cascadeTopLeftFromPoint:topLeft];
643 [win setFrameTopLeftPoint:topLeft];
645 if ([win frame].origin.y < [[win screen] frame].origin.y) {
646 // Try to avoid shifting the new window downwards if it means that
647 // the bottom of the window will be off the screen. E.g. if the
648 // user has set windows to open maximized in the vertical direction
649 // then the new window will cascade horizontally only.
650 topLeft.y = oldTopLeft.y;
651 [win setFrameTopLeftPoint:topLeft];
655 if (1 == [vimControllers count]) {
656 // The first window autosaves its position. (The autosaving
657 // features of Cocoa are not used because we need more control over
658 // what is autosaved and when it is restored.)
659 [windowController setWindowAutosaveKey:MMTopLeftPointKey];
662 if (openSelectionString) {
663 // TODO: Pass this as a parameter instead! Get rid of
664 // 'openSelectionString' etc.
666 // There is some text to paste into this window as a result of the
667 // services menu "Open selection ..." being used.
668 [[windowController vimController] dropString:openSelectionString];
669 [openSelectionString release];
670 openSelectionString = nil;
673 if (shouldActivateWhenNextWindowOpens) {
674 [NSApp activateIgnoringOtherApps:YES];
675 shouldActivateWhenNextWindowOpens = NO;
679 - (void)setMainMenu:(NSMenu *)mainMenu
681 if ([NSApp mainMenu] == mainMenu) return;
683 // If the new menu has a "Recent Files" dummy item, then swap the real item
684 // for the dummy. We are forced to do this since Cocoa initializes the
685 // "Recent Files" menu and there is no way to simply point Cocoa to a new
686 // item each time the menus are swapped.
687 NSMenu *fileMenu = [mainMenu findFileMenu];
688 if (recentFilesMenuItem && fileMenu) {
690 [fileMenu indexOfItemWithAction:@selector(recentFilesDummy:)];
692 NSMenuItem *dummyItem = [[fileMenu itemAtIndex:dummyIdx] retain];
693 [fileMenu removeItemAtIndex:dummyIdx];
695 NSMenu *recentFilesParentMenu = [recentFilesMenuItem menu];
696 int idx = [recentFilesParentMenu indexOfItem:recentFilesMenuItem];
698 [[recentFilesMenuItem retain] autorelease];
699 [recentFilesParentMenu removeItemAtIndex:idx];
700 [recentFilesParentMenu insertItem:dummyItem atIndex:idx];
703 [fileMenu insertItem:recentFilesMenuItem atIndex:dummyIdx];
708 // Now set the new menu. Notice that we keep one menu for each editor
709 // window since each editor can have its own set of menus. When swapping
710 // menus we have to tell Cocoa where the new "MacVim", "Windows", and
711 // "Services" menu are.
712 [NSApp setMainMenu:mainMenu];
714 // Setting the "MacVim" (or "Application") menu ensures that it is typeset
715 // in boldface. (The setAppleMenu: method used to be public but is now
716 // private so this will have to be considered a bit of a hack!)
717 NSMenu *appMenu = [mainMenu findApplicationMenu];
718 [NSApp performSelector:@selector(setAppleMenu:) withObject:appMenu];
720 NSMenu *servicesMenu = [mainMenu findServicesMenu];
721 [NSApp setServicesMenu:servicesMenu];
723 NSMenu *windowsMenu = [mainMenu findWindowsMenu];
725 // Cocoa isn't clever enough to get rid of items it has added to the
726 // "Windows" menu so we have to do it ourselves otherwise there will be
727 // multiple menu items for each window in the "Windows" menu.
728 // This code assumes that the only items Cocoa add are ones which
729 // send off the action makeKeyAndOrderFront:. (Cocoa will not add
730 // another separator item if the last item on the "Windows" menu
731 // already is a separator, so we needen't worry about separators.)
732 int i, count = [windowsMenu numberOfItems];
733 for (i = count-1; i >= 0; --i) {
734 NSMenuItem *item = [windowsMenu itemAtIndex:i];
735 if ([item action] == @selector(makeKeyAndOrderFront:))
736 [windowsMenu removeItem:item];
739 [NSApp setWindowsMenu:windowsMenu];
741 #ifdef MM_ENABLE_PLUGINS
742 // Move plugin menu from old to new main menu.
743 [self removePlugInMenu];
744 [self addPlugInMenuToMenu:mainMenu];
748 - (NSArray *)filterOpenFiles:(NSArray *)filenames
750 return [self filterOpenFiles:filenames openFilesDict:nil];
753 - (BOOL)openFiles:(NSArray *)filenames withArguments:(NSDictionary *)args
755 // Opening files works like this:
756 // a) filter out any already open files
757 // b) open any remaining files
759 // A file is opened in an untitled window if there is one (it may be
760 // currently launching, or it may already be visible), otherwise a new
763 // Each launching Vim process has a dictionary of arguments that are passed
764 // to the process when in checks in (via connectBackend:pid:). The
765 // arguments for each launching process can be looked up by its PID (in the
766 // pidArguments dictionary).
768 NSMutableDictionary *arguments = (args ? [[args mutableCopy] autorelease]
769 : [NSMutableDictionary dictionary]);
772 // a) Filter out any already open files
774 NSString *firstFile = [filenames objectAtIndex:0];
775 MMVimController *firstController = nil;
776 NSDictionary *openFilesDict = nil;
777 filenames = [self filterOpenFiles:filenames openFilesDict:&openFilesDict];
779 // Pass arguments to vim controllers that had files open.
781 NSEnumerator *e = [openFilesDict keyEnumerator];
783 // (Indicate that we do not wish to open any files at the moment.)
784 [arguments setObject:[NSNumber numberWithBool:YES] forKey:@"dontOpen"];
786 while ((key = [e nextObject])) {
787 NSArray *files = [openFilesDict objectForKey:key];
788 [arguments setObject:files forKey:@"filenames"];
790 MMVimController *vc = [key pointerValue];
791 [vc passArguments:arguments];
793 // If this controller holds the first file, then remember it for later.
794 if ([files containsObject:firstFile])
795 firstController = vc;
798 if ([filenames count] == 0) {
799 // Raise the window containing the first file that was already open,
800 // and make sure that the tab containing that file is selected. Only
801 // do this when there are no more files to open, otherwise sometimes
802 // the window with 'firstFile' will be raised, other times it might be
803 // the window that will open with the files in the 'filenames' array.
804 firstFile = [firstFile stringByEscapingSpecialFilenameCharacters];
805 NSString *input = [NSString stringWithFormat:@"<C-\\><C-N>"
806 ":let oldswb=&swb|let &swb=\"useopen,usetab\"|"
807 "tab sb %@|let &swb=oldswb|unl oldswb|"
808 "cal foreground()|redr|f<CR>", firstFile];
810 [firstController addVimInput:input];
815 // Add filenames to "Recent Files" menu, unless they are being edited
816 // remotely (using ODB).
817 if ([arguments objectForKey:@"remoteID"] == nil) {
818 [[NSDocumentController sharedDocumentController]
819 noteNewRecentFilePaths:filenames];
823 // b) Open any remaining files
826 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
827 BOOL openInCurrentWindow = [ud boolForKey:MMOpenInCurrentWindowKey];
829 // The meaning of "layout" is defined by the WIN_* defines in main.c.
830 int layout = [ud integerForKey:MMOpenLayoutKey];
831 BOOL splitVert = [ud boolForKey:MMVerticalSplitKey];
832 if (splitVert && MMLayoutHorizontalSplit == layout)
833 layout = MMLayoutVerticalSplit;
834 if (layout < 0 || (layout > MMLayoutTabs && openInCurrentWindow))
835 layout = MMLayoutTabs;
837 [arguments setObject:[NSNumber numberWithInt:layout] forKey:@"layout"];
838 [arguments setObject:filenames forKey:@"filenames"];
839 // (Indicate that files should be opened from now on.)
840 [arguments setObject:[NSNumber numberWithBool:NO] forKey:@"dontOpen"];
842 if (openInCurrentWindow && (vc = [self topmostVimController])) {
843 // Open files in an already open window.
844 [[[vc windowController] window] makeKeyAndOrderFront:self];
845 [vc passArguments:arguments];
850 int numFiles = [filenames count];
851 if (MMLayoutWindows == layout && numFiles > 1) {
852 // Open one file at a time in a new window, but don't open too many at
853 // once (at most cap+1 windows will open). If the user has increased
854 // the preload cache size we'll take that as a hint that more windows
855 // should be able to open at once.
856 int cap = [self maxPreloadCacheSize] - 1;
857 if (cap < 4) cap = 4;
858 if (cap > numFiles) cap = numFiles;
861 for (i = 0; i < cap; ++i) {
862 NSArray *a = [NSArray arrayWithObject:[filenames objectAtIndex:i]];
863 [arguments setObject:a forKey:@"filenames"];
865 // NOTE: We have to copy the args since we'll mutate them in the
866 // next loop and the below call may retain the arguments while
867 // waiting for a process to start.
868 NSDictionary *args = [[arguments copy] autorelease];
870 openOk = [self openVimControllerWithArguments:args];
874 // Open remaining files in tabs in a new window.
875 if (openOk && numFiles > cap) {
876 NSRange range = { i, numFiles-cap };
877 NSArray *a = [filenames subarrayWithRange:range];
878 [arguments setObject:a forKey:@"filenames"];
879 [arguments setObject:[NSNumber numberWithInt:MMLayoutTabs]
882 openOk = [self openVimControllerWithArguments:arguments];
885 // Open all files at once.
886 openOk = [self openVimControllerWithArguments:arguments];
892 #ifdef MM_ENABLE_PLUGINS
893 - (void)addItemToPlugInMenu:(NSMenuItem *)item
895 NSMenu *menu = [plugInMenuItem submenu];
897 if ([menu numberOfItems] == 1)
898 [self addPlugInMenuToMenu:[NSApp mainMenu]];
901 - (void)removeItemFromPlugInMenu:(NSMenuItem *)item
903 NSMenu *menu = [plugInMenuItem submenu];
904 [menu removeItem:item];
905 if ([menu numberOfItems] == 0)
906 [self removePlugInMenu];
910 - (IBAction)newWindow:(id)sender
912 // A cached controller requires no loading times and results in the new
913 // window popping up instantaneously. If the cache is empty it may take
914 // 1-2 seconds to start a new Vim process.
915 MMVimController *vc = [self takeVimControllerFromCache];
917 [[vc backendProxy] acknowledgeConnection];
919 [self launchVimProcessWithArguments:nil];
923 - (IBAction)newWindowAndActivate:(id)sender
925 [self activateWhenNextWindowOpens];
926 [self newWindow:sender];
929 - (IBAction)fileOpen:(id)sender
932 BOOL trackPwd = [[NSUserDefaults standardUserDefaults]
933 boolForKey:MMDialogsTrackPwdKey];
935 MMVimController *vc = [self keyVimController];
936 if (vc) dir = [[vc vimState] objectForKey:@"pwd"];
939 NSOpenPanel *panel = [NSOpenPanel openPanel];
940 [panel setAllowsMultipleSelection:YES];
941 [panel setAccessoryView:openPanelAccessoryView()];
943 int result = [panel runModalForDirectory:dir file:nil types:nil];
944 if (NSOKButton == result)
945 [self application:NSApp openFiles:[panel filenames]];
948 - (IBAction)selectNextWindow:(id)sender
950 unsigned i, count = [vimControllers count];
953 NSWindow *keyWindow = [NSApp keyWindow];
954 for (i = 0; i < count; ++i) {
955 MMVimController *vc = [vimControllers objectAtIndex:i];
956 if ([[[vc windowController] window] isEqual:keyWindow])
963 MMVimController *vc = [vimControllers objectAtIndex:i];
964 [[vc windowController] showWindow:self];
968 - (IBAction)selectPreviousWindow:(id)sender
970 unsigned i, count = [vimControllers count];
973 NSWindow *keyWindow = [NSApp keyWindow];
974 for (i = 0; i < count; ++i) {
975 MMVimController *vc = [vimControllers objectAtIndex:i];
976 if ([[[vc windowController] window] isEqual:keyWindow])
986 MMVimController *vc = [vimControllers objectAtIndex:i];
987 [[vc windowController] showWindow:self];
991 - (IBAction)orderFrontPreferencePanel:(id)sender
993 [[MMPreferenceController sharedPrefsWindowController] showWindow:self];
996 - (IBAction)openWebsite:(id)sender
998 [[NSWorkspace sharedWorkspace] openURL:
999 [NSURL URLWithString:MMWebsiteString]];
1002 - (IBAction)showVimHelp:(id)sender
1004 // Open a new window with the help window maximized.
1005 [self launchVimProcessWithArguments:[NSArray arrayWithObjects:
1006 @"-c", @":h gui_mac", @"-c", @":res", nil]];
1009 - (IBAction)zoomAll:(id)sender
1011 [NSApp makeWindowsPerform:@selector(performZoom:) inOrder:YES];
1014 - (IBAction)atsuiButtonClicked:(id)sender
1016 // This action is called when the user clicks the "use ATSUI renderer"
1017 // button in the advanced preferences pane.
1018 [self rebuildPreloadCache];
1021 - (IBAction)loginShellButtonClicked:(id)sender
1023 // This action is called when the user clicks the "use login shell" button
1024 // in the advanced preferences pane.
1025 [self rebuildPreloadCache];
1028 - (IBAction)quickstartButtonClicked:(id)sender
1030 if ([self maxPreloadCacheSize] > 0) {
1031 [self scheduleVimControllerPreloadAfterDelay:1.0];
1032 [self startWatchingVimDir];
1034 [self cancelVimControllerPreloadRequests];
1035 [self clearPreloadCacheWithCount:-1];
1036 [self stopWatchingVimDir];
1040 - (byref id <MMFrontendProtocol>)
1041 connectBackend:(byref in id <MMBackendProtocol>)backend
1044 //NSLog(@"Connect backend (pid=%d)", pid);
1045 NSNumber *pidKey = [NSNumber numberWithInt:pid];
1046 MMVimController *vc = nil;
1049 [(NSDistantObject*)backend
1050 setProtocolForProxy:@protocol(MMBackendProtocol)];
1052 vc = [[[MMVimController alloc] initWithBackend:backend pid:pid]
1055 if (preloadPid == pid) {
1056 // This backend was preloaded, so add it to the cache and schedule
1057 // another vim process to be preloaded.
1059 [vc setIsPreloading:YES];
1060 [cachedVimControllers addObject:vc];
1061 [self scheduleVimControllerPreloadAfterDelay:1];
1066 [vimControllers addObject:vc];
1068 id args = [pidArguments objectForKey:pidKey];
1069 if (args && [NSNull null] != args)
1070 [vc passArguments:args];
1072 // HACK! MacVim does not get activated if it is launched from the
1073 // terminal, so we forcibly activate here unless it is an untitled
1074 // window opening. Untitled windows are treated differently, else
1075 // MacVim would steal the focus if another app was activated while the
1076 // untitled window was loading.
1077 if (!args || args != [NSNull null])
1078 [NSApp activateIgnoringOtherApps:YES];
1081 [pidArguments removeObjectForKey:pidKey];
1086 @catch (NSException *e) {
1087 NSLog(@"Exception caught in %s: \"%@\"", _cmd, e);
1090 [vimControllers removeObject:vc];
1092 [pidArguments removeObjectForKey:pidKey];
1098 - (NSArray *)serverList
1100 NSMutableArray *array = [NSMutableArray array];
1102 unsigned i, count = [vimControllers count];
1103 for (i = 0; i < count; ++i) {
1104 MMVimController *controller = [vimControllers objectAtIndex:i];
1105 if ([controller serverName])
1106 [array addObject:[controller serverName]];
1112 - (MMVimController *)keyVimController
1114 NSWindow *keyWindow = [NSApp keyWindow];
1116 unsigned i, count = [vimControllers count];
1117 for (i = 0; i < count; ++i) {
1118 MMVimController *vc = [vimControllers objectAtIndex:i];
1119 if ([[[vc windowController] window] isEqual:keyWindow])
1127 @end // MMAppController
1132 @implementation MMAppController (MMServices)
1134 - (void)openSelection:(NSPasteboard *)pboard userData:(NSString *)userData
1135 error:(NSString **)error
1137 if (![[pboard types] containsObject:NSStringPboardType]) {
1138 NSLog(@"WARNING: Pasteboard contains no object of type "
1139 "NSStringPboardType");
1143 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
1144 BOOL openInCurrentWindow = [ud boolForKey:MMOpenInCurrentWindowKey];
1145 MMVimController *vc;
1147 if (openInCurrentWindow && (vc = [self topmostVimController])) {
1148 [vc sendMessage:AddNewTabMsgID data:nil];
1149 [vc dropString:[pboard stringForType:NSStringPboardType]];
1151 // Save the text, open a new window, and paste the text when the next
1152 // window opens. (If this is called several times in a row, then all
1153 // but the last call may be ignored.)
1154 if (openSelectionString) [openSelectionString release];
1155 openSelectionString = [[pboard stringForType:NSStringPboardType] copy];
1157 [self newWindow:self];
1161 - (void)openFile:(NSPasteboard *)pboard userData:(NSString *)userData
1162 error:(NSString **)error
1164 if (![[pboard types] containsObject:NSStringPboardType]) {
1165 NSLog(@"WARNING: Pasteboard contains no object of type "
1166 "NSStringPboardType");
1170 // TODO: Parse multiple filenames and create array with names.
1171 NSString *string = [pboard stringForType:NSStringPboardType];
1172 string = [string stringByTrimmingCharactersInSet:
1173 [NSCharacterSet whitespaceAndNewlineCharacterSet]];
1174 string = [string stringByStandardizingPath];
1176 NSArray *filenames = [self filterFilesAndNotify:
1177 [NSArray arrayWithObject:string]];
1178 if ([filenames count] == 0)
1181 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
1182 BOOL openInCurrentWindow = [ud boolForKey:MMOpenInCurrentWindowKey];
1183 MMVimController *vc;
1185 if (openInCurrentWindow && (vc = [self topmostVimController])) {
1186 [vc dropFiles:filenames forceOpen:YES];
1188 [self openFiles:filenames withArguments:nil];
1192 - (void)newFileHere:(NSPasteboard *)pboard userData:(NSString *)userData
1193 error:(NSString **)error
1195 if (![[pboard types] containsObject:NSStringPboardType]) {
1196 NSLog(@"WARNING: Pasteboard contains no object of type "
1197 "NSStringPboardType");
1201 NSString *path = [pboard stringForType:NSStringPboardType];
1204 if (![[NSFileManager defaultManager] fileExistsAtPath:path
1205 isDirectory:&dirIndicator]) {
1206 NSLog(@"Invalid path. Cannot open new document at: %@", path);
1211 path = [path stringByDeletingLastPathComponent];
1213 path = [path stringByReplacingOccurrencesOfString:@" " withString:@"\\ "];
1215 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
1216 BOOL openInCurrentWindow = [ud boolForKey:MMOpenInCurrentWindowKey];
1217 MMVimController *vc;
1219 if (openInCurrentWindow && (vc = [self topmostVimController])) {
1220 NSString *input = [NSString stringWithFormat:@"<C-\\><C-N>"
1221 ":tabe|cd %@<CR>", path];
1222 [vc addVimInput:input];
1224 NSString *input = [NSString stringWithFormat:@":cd %@", path];
1225 [self launchVimProcessWithArguments:[NSArray arrayWithObjects:
1226 @"-c", input, nil]];
1230 @end // MMAppController (MMServices)
1235 @implementation MMAppController (Private)
1237 - (MMVimController *)topmostVimController
1239 // Find the topmost visible window which has an associated vim controller.
1240 NSEnumerator *e = [[NSApp orderedWindows] objectEnumerator];
1242 while ((window = [e nextObject]) && [window isVisible]) {
1243 unsigned i, count = [vimControllers count];
1244 for (i = 0; i < count; ++i) {
1245 MMVimController *vc = [vimControllers objectAtIndex:i];
1246 if ([[[vc windowController] window] isEqual:window])
1254 - (int)launchVimProcessWithArguments:(NSArray *)args
1257 NSString *path = [[NSBundle mainBundle] pathForAuxiliaryExecutable:@"Vim"];
1260 NSLog(@"ERROR: Vim executable could not be found inside app bundle!");
1264 NSArray *taskArgs = [NSArray arrayWithObjects:@"-g", @"-f", nil];
1266 taskArgs = [taskArgs arrayByAddingObjectsFromArray:args];
1268 BOOL useLoginShell = [[NSUserDefaults standardUserDefaults]
1269 boolForKey:MMLoginShellKey];
1270 if (useLoginShell) {
1271 // Run process with a login shell, roughly:
1272 // echo "exec Vim -g -f args" | ARGV0=-`basename $SHELL` $SHELL [-l]
1273 pid = executeInLoginShell(path, taskArgs);
1275 // Run process directly:
1277 NSTask *task = [NSTask launchedTaskWithLaunchPath:path
1278 arguments:taskArgs];
1279 pid = task ? [task processIdentifier] : -1;
1283 // NOTE: If the process has no arguments, then add a null argument to
1284 // the pidArguments dictionary. This is later used to detect that a
1285 // process without arguments is being launched.
1287 [pidArguments setObject:[NSNull null]
1288 forKey:[NSNumber numberWithInt:pid]];
1290 NSLog(@"WARNING: %s%@ failed (useLoginShell=%d)", _cmd, args,
1297 - (NSArray *)filterFilesAndNotify:(NSArray *)filenames
1299 // Go trough 'filenames' array and make sure each file exists. Present
1300 // warning dialog if some file was missing.
1302 NSString *firstMissingFile = nil;
1303 NSMutableArray *files = [NSMutableArray array];
1304 unsigned i, count = [filenames count];
1306 for (i = 0; i < count; ++i) {
1307 NSString *name = [filenames objectAtIndex:i];
1308 if ([[NSFileManager defaultManager] fileExistsAtPath:name]) {
1309 [files addObject:name];
1310 } else if (!firstMissingFile) {
1311 firstMissingFile = name;
1315 if (firstMissingFile) {
1316 NSAlert *alert = [[NSAlert alloc] init];
1317 [alert addButtonWithTitle:NSLocalizedString(@"OK",
1321 if ([files count] >= count-1) {
1322 [alert setMessageText:NSLocalizedString(@"File not found",
1323 @"File not found dialog, title")];
1324 text = [NSString stringWithFormat:NSLocalizedString(
1325 @"Could not open file with name %@.",
1326 @"File not found dialog, text"), firstMissingFile];
1328 [alert setMessageText:NSLocalizedString(@"Multiple files not found",
1329 @"File not found dialog, title")];
1330 text = [NSString stringWithFormat:NSLocalizedString(
1331 @"Could not open file with name %@, and %d other files.",
1332 @"File not found dialog, text"),
1333 firstMissingFile, count-[files count]-1];
1336 [alert setInformativeText:text];
1337 [alert setAlertStyle:NSWarningAlertStyle];
1342 [NSApp replyToOpenOrPrint:NSApplicationDelegateReplyFailure];
1348 - (NSArray *)filterOpenFiles:(NSArray *)filenames
1349 openFilesDict:(NSDictionary **)openFiles
1351 // Filter out any files in the 'filenames' array that are open and return
1352 // all files that are not already open. On return, the 'openFiles'
1353 // parameter (if non-nil) will point to a dictionary of open files, indexed
1354 // by Vim controller.
1356 NSMutableDictionary *dict = [NSMutableDictionary dictionary];
1357 NSMutableArray *files = [filenames mutableCopy];
1359 // TODO: Escape special characters in 'files'?
1360 NSString *expr = [NSString stringWithFormat:
1361 @"map([\"%@\"],\"bufloaded(v:val)\")",
1362 [files componentsJoinedByString:@"\",\""]];
1364 unsigned i, count = [vimControllers count];
1365 for (i = 0; i < count && [files count] > 0; ++i) {
1366 MMVimController *vc = [vimControllers objectAtIndex:i];
1368 // Query Vim for which files in the 'files' array are open.
1369 NSString *eval = [vc evaluateVimExpression:expr];
1370 if (!eval) continue;
1372 NSIndexSet *idxSet = [NSIndexSet indexSetWithVimList:eval];
1373 if ([idxSet count] > 0) {
1374 [dict setObject:[files objectsAtIndexes:idxSet]
1375 forKey:[NSValue valueWithPointer:vc]];
1377 // Remove all the files that were open in this Vim process and
1378 // create a new expression to evaluate.
1379 [files removeObjectsAtIndexes:idxSet];
1380 expr = [NSString stringWithFormat:
1381 @"map([\"%@\"],\"bufloaded(v:val)\")",
1382 [files componentsJoinedByString:@"\",\""]];
1386 if (openFiles != nil)
1392 #if MM_HANDLE_XCODE_MOD_EVENT
1393 - (void)handleXcodeModEvent:(NSAppleEventDescriptor *)event
1394 replyEvent:(NSAppleEventDescriptor *)reply
1397 // Xcode sends this event to query MacVim which open files have been
1399 NSLog(@"reply:%@", reply);
1400 NSLog(@"event:%@", event);
1402 NSEnumerator *e = [vimControllers objectEnumerator];
1404 while ((vc = [e nextObject])) {
1405 DescType type = [reply descriptorType];
1406 unsigned len = [[type data] length];
1407 NSMutableData *data = [NSMutableData data];
1409 [data appendBytes:&type length:sizeof(DescType)];
1410 [data appendBytes:&len length:sizeof(unsigned)];
1411 [data appendBytes:[reply data] length:len];
1413 [vc sendMessage:XcodeModMsgID data:data];
1419 - (void)handleGetURLEvent:(NSAppleEventDescriptor *)event
1420 replyEvent:(NSAppleEventDescriptor *)reply
1422 NSString *urlString = [[event paramDescriptorForKeyword:keyDirectObject]
1424 NSURL *url = [NSURL URLWithString:urlString];
1426 // We try to be compatible with TextMate's URL scheme here, as documented
1427 // at http://blog.macromates.com/2007/the-textmate-url-scheme/ . Currently,
1430 // The format is: mvim://open?<arguments> where arguments can be:
1432 // * url — the actual file to open (i.e. a file://… URL), if you leave
1433 // out this argument, the frontmost document is implied.
1434 // * line — line number to go to (one based).
1435 // * column — column number to go to (one based).
1437 // Example: mvim://open?url=file:///etc/profile&line=20
1439 if ([[url host] isEqualToString:@"open"]) {
1440 NSMutableDictionary *dict = [NSMutableDictionary dictionary];
1442 // Parse query ("url=file://...&line=14") into a dictionary
1443 NSArray *queries = [[url query] componentsSeparatedByString:@"&"];
1444 NSEnumerator *enumerator = [queries objectEnumerator];
1446 while( param = [enumerator nextObject] ) {
1447 NSArray *arr = [param componentsSeparatedByString:@"="];
1448 if ([arr count] == 2) {
1449 [dict setValue:[[arr lastObject]
1450 stringByReplacingPercentEscapesUsingEncoding:
1451 NSUTF8StringEncoding]
1452 forKey:[[arr objectAtIndex:0]
1453 stringByReplacingPercentEscapesUsingEncoding:
1454 NSUTF8StringEncoding]];
1458 // Actually open the file.
1459 NSString *file = [dict objectForKey:@"url"];
1461 NSURL *fileUrl= [NSURL URLWithString:file];
1462 // TextMate only opens files that already exist.
1463 if ([fileUrl isFileURL]
1464 && [[NSFileManager defaultManager] fileExistsAtPath:
1466 // Strip 'file://' path, else application:openFiles: might think
1467 // the file is not yet open.
1468 NSArray *filenames = [NSArray arrayWithObject:[fileUrl path]];
1470 // Look for the line and column options.
1471 NSDictionary *args = nil;
1472 NSString *line = [dict objectForKey:@"line"];
1474 NSString *column = [dict objectForKey:@"column"];
1476 args = [NSDictionary dictionaryWithObjectsAndKeys:
1477 line, @"cursorLine",
1478 column, @"cursorColumn",
1481 args = [NSDictionary dictionaryWithObject:line
1482 forKey:@"cursorLine"];
1485 [self openFiles:filenames withArguments:args];
1489 NSAlert *alert = [[NSAlert alloc] init];
1490 [alert addButtonWithTitle:NSLocalizedString(@"OK",
1493 [alert setMessageText:NSLocalizedString(@"Unknown URL Scheme",
1494 @"Unknown URL Scheme dialog, title")];
1495 [alert setInformativeText:[NSString stringWithFormat:NSLocalizedString(
1496 @"This version of MacVim does not support \"%@\""
1497 @" in its URL scheme.",
1498 @"Unknown URL Scheme dialog, text"),
1501 [alert setAlertStyle:NSWarningAlertStyle];
1508 - (int)findLaunchingProcessWithoutArguments
1510 NSArray *keys = [pidArguments allKeysForObject:[NSNull null]];
1511 if ([keys count] > 0) {
1512 //NSLog(@"found launching process without arguments");
1513 return [[keys objectAtIndex:0] intValue];
1519 - (MMVimController *)findUnusedEditor
1521 NSEnumerator *e = [vimControllers objectEnumerator];
1523 while ((vc = [e nextObject])) {
1524 if ([[[vc vimState] objectForKey:@"unusedEditor"] boolValue])
1531 - (NSMutableDictionary *)extractArgumentsFromOdocEvent:
1532 (NSAppleEventDescriptor *)desc
1534 NSMutableDictionary *dict = [NSMutableDictionary dictionary];
1536 // 1. Extract ODB parameters (if any)
1537 NSAppleEventDescriptor *odbdesc = desc;
1538 if (![odbdesc paramDescriptorForKeyword:keyFileSender]) {
1539 // The ODB paramaters may hide inside the 'keyAEPropData' descriptor.
1540 odbdesc = [odbdesc paramDescriptorForKeyword:keyAEPropData];
1541 if (![odbdesc paramDescriptorForKeyword:keyFileSender])
1546 NSAppleEventDescriptor *p =
1547 [odbdesc paramDescriptorForKeyword:keyFileSender];
1549 [dict setObject:[NSNumber numberWithUnsignedInt:[p typeCodeValue]]
1550 forKey:@"remoteID"];
1552 p = [odbdesc paramDescriptorForKeyword:keyFileCustomPath];
1554 [dict setObject:[p stringValue] forKey:@"remotePath"];
1556 p = [odbdesc paramDescriptorForKeyword:keyFileSenderToken];
1558 [dict setObject:[NSNumber numberWithUnsignedLong:[p descriptorType]]
1559 forKey:@"remoteTokenDescType"];
1560 [dict setObject:[p data] forKey:@"remoteTokenData"];
1564 // 2. Extract Xcode parameters (if any)
1565 NSAppleEventDescriptor *xcodedesc =
1566 [desc paramDescriptorForKeyword:keyAEPosition];
1569 MMSelectionRange *sr = (MMSelectionRange*)[[xcodedesc data] bytes];
1571 if (sr->lineNum < 0) {
1572 // Should select a range of lines.
1573 range.location = sr->startRange + 1;
1574 range.length = sr->endRange - sr->startRange + 1;
1576 // Should only move cursor to a line.
1577 range.location = sr->lineNum + 1;
1581 [dict setObject:NSStringFromRange(range) forKey:@"selectionRange"];
1584 // 3. Extract Spotlight search text (if any)
1585 NSAppleEventDescriptor *spotlightdesc =
1586 [desc paramDescriptorForKeyword:keyAESearchText];
1588 [dict setObject:[spotlightdesc stringValue] forKey:@"searchText"];
1593 #ifdef MM_ENABLE_PLUGINS
1594 - (void)removePlugInMenu
1596 if ([plugInMenuItem menu])
1597 [[plugInMenuItem menu] removeItem:plugInMenuItem];
1600 - (void)addPlugInMenuToMenu:(NSMenu *)mainMenu
1602 NSMenu *windowsMenu = [mainMenu findWindowsMenu];
1604 if ([[plugInMenuItem submenu] numberOfItems] > 0) {
1605 int idx = windowsMenu ? [mainMenu indexOfItemWithSubmenu:windowsMenu]
1608 [mainMenu insertItem:plugInMenuItem atIndex:idx];
1610 [mainMenu addItem:plugInMenuItem];
1616 - (void)scheduleVimControllerPreloadAfterDelay:(NSTimeInterval)delay
1618 [self performSelector:@selector(preloadVimController:)
1623 - (void)cancelVimControllerPreloadRequests
1625 [NSObject cancelPreviousPerformRequestsWithTarget:self
1626 selector:@selector(preloadVimController:)
1630 - (void)preloadVimController:(id)sender
1632 // We only allow preloading of one Vim process at a time (to avoid hogging
1633 // CPU), so schedule another preload in a little while if necessary.
1634 if (-1 != preloadPid) {
1635 [self scheduleVimControllerPreloadAfterDelay:2];
1639 if ([cachedVimControllers count] >= [self maxPreloadCacheSize])
1642 preloadPid = [self launchVimProcessWithArguments:
1643 [NSArray arrayWithObject:@"--mmwaitforack"]];
1646 - (int)maxPreloadCacheSize
1648 // The maximum number of Vim processes to keep in the cache can be
1649 // controlled via the user default "MMPreloadCacheSize".
1650 int maxCacheSize = [[NSUserDefaults standardUserDefaults]
1651 integerForKey:MMPreloadCacheSizeKey];
1652 if (maxCacheSize < 0) maxCacheSize = 0;
1653 else if (maxCacheSize > 10) maxCacheSize = 10;
1655 return maxCacheSize;
1658 - (MMVimController *)takeVimControllerFromCache
1660 // NOTE: After calling this message the backend corresponding to the
1661 // returned vim controller must be sent an acknowledgeConnection message,
1662 // else the vim process will be stuck.
1664 // This method may return nil even though the cache might be non-empty; the
1665 // caller should handle this by starting a new Vim process.
1667 int i, count = [cachedVimControllers count];
1668 if (0 == count) return nil;
1670 // Locate the first Vim controller with up-to-date rc-files sourced.
1671 NSDate *rcDate = [self rcFilesModificationDate];
1672 for (i = 0; i < count; ++i) {
1673 MMVimController *vc = [cachedVimControllers objectAtIndex:i];
1674 NSDate *date = [vc creationDate];
1675 if ([date compare:rcDate] != NSOrderedAscending)
1680 // Clear out cache entries whose vimrc/gvimrc files were sourced before
1681 // the latest modification date for those files. This ensures that the
1682 // latest rc-files are always sourced for new windows.
1683 [self clearPreloadCacheWithCount:i];
1686 if ([cachedVimControllers count] == 0) {
1687 [self scheduleVimControllerPreloadAfterDelay:2.0];
1691 MMVimController *vc = [cachedVimControllers objectAtIndex:0];
1692 [vimControllers addObject:vc];
1693 [cachedVimControllers removeObjectAtIndex:0];
1694 [vc setIsPreloading:NO];
1696 // If the Vim process has finished loading then the window will displayed
1697 // now, otherwise it will be displayed when the OpenWindowMsgID message is
1699 [[vc windowController] showWindow];
1701 // Since we've taken one controller from the cache we take the opportunity
1702 // to preload another.
1703 [self scheduleVimControllerPreloadAfterDelay:1];
1708 - (void)clearPreloadCacheWithCount:(int)count
1710 // Remove the 'count' first entries in the preload cache. It is assumed
1711 // that objects are added/removed from the cache in a FIFO manner so that
1712 // this effectively clears the 'count' oldest entries.
1713 // If 'count' is negative, then the entire cache is cleared.
1715 if ([cachedVimControllers count] == 0 || count == 0)
1719 count = [cachedVimControllers count];
1721 // Make sure the preloaded Vim processes get killed or they'll just hang
1722 // around being useless until MacVim is terminated.
1723 NSEnumerator *e = [cachedVimControllers objectEnumerator];
1724 MMVimController *vc;
1726 while ((vc = [e nextObject]) && n-- > 0) {
1727 [[NSNotificationCenter defaultCenter] removeObserver:vc];
1728 [vc sendMessage:TerminateNowMsgID data:nil];
1730 // Since the preloaded processes were killed "prematurely" we have to
1731 // manually tell them to cleanup (it is not enough to simply release
1732 // them since deallocation and cleanup are separated).
1737 while (n-- > 0 && [cachedVimControllers count] > 0)
1738 [cachedVimControllers removeObjectAtIndex:0];
1741 - (void)rebuildPreloadCache
1743 if ([self maxPreloadCacheSize] > 0) {
1744 [self clearPreloadCacheWithCount:-1];
1745 [self cancelVimControllerPreloadRequests];
1746 [self scheduleVimControllerPreloadAfterDelay:1.0];
1750 - (NSDate *)rcFilesModificationDate
1752 // Check modification dates for ~/.vimrc and ~/.gvimrc and return the
1753 // latest modification date. If ~/.vimrc does not exist, check ~/_vimrc
1754 // and similarly for gvimrc.
1755 // Returns distantPath if no rc files were found.
1757 NSDate *date = [NSDate distantPast];
1758 NSFileManager *fm = [NSFileManager defaultManager];
1760 NSString *path = [@"~/.vimrc" stringByExpandingTildeInPath];
1761 NSDictionary *attr = [fm fileAttributesAtPath:path traverseLink:YES];
1763 path = [@"~/_vimrc" stringByExpandingTildeInPath];
1764 attr = [fm fileAttributesAtPath:path traverseLink:YES];
1766 NSDate *modDate = [attr objectForKey:NSFileModificationDate];
1770 path = [@"~/.gvimrc" stringByExpandingTildeInPath];
1771 attr = [fm fileAttributesAtPath:path traverseLink:YES];
1773 path = [@"~/_gvimrc" stringByExpandingTildeInPath];
1774 attr = [fm fileAttributesAtPath:path traverseLink:YES];
1776 modDate = [attr objectForKey:NSFileModificationDate];
1778 date = [date laterDate:modDate];
1783 - (BOOL)openVimControllerWithArguments:(NSDictionary *)arguments
1785 MMVimController *vc = [self findUnusedEditor];
1787 // Open files in an already open window.
1788 [[[vc windowController] window] makeKeyAndOrderFront:self];
1789 [vc passArguments:arguments];
1790 } else if ((vc = [self takeVimControllerFromCache])) {
1791 // Open files in a new window using a cached vim controller. This
1792 // requires virtually no loading time so the new window will pop up
1794 [vc passArguments:arguments];
1795 [[vc backendProxy] acknowledgeConnection];
1797 // Open files in a launching Vim process or start a new process. This
1798 // may take 1-2 seconds so there will be a visible delay before the
1799 // window appears on screen.
1800 int pid = [self findLaunchingProcessWithoutArguments];
1802 pid = [self launchVimProcessWithArguments:nil];
1807 // TODO: If the Vim process fails to start, or if it changes PID,
1808 // then the memory allocated for these parameters will leak.
1809 // Ensure that this cannot happen or somehow detect it.
1811 if ([arguments count] > 0)
1812 [pidArguments setObject:arguments
1813 forKey:[NSNumber numberWithInt:pid]];
1819 - (void)activateWhenNextWindowOpens
1821 shouldActivateWhenNextWindowOpens = YES;
1824 - (void)startWatchingVimDir
1826 //NSLog(@"%s", _cmd);
1827 #if (MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_4)
1830 if (NULL == FSEventStreamStart)
1831 return; // FSEvent functions are weakly linked
1833 NSString *path = [@"~/.vim" stringByExpandingTildeInPath];
1834 NSArray *pathsToWatch = [NSArray arrayWithObject:path];
1836 fsEventStream = FSEventStreamCreate(NULL, &fsEventCallback, NULL,
1837 (CFArrayRef)pathsToWatch, kFSEventStreamEventIdSinceNow,
1838 MMEventStreamLatency, kFSEventStreamCreateFlagNone);
1840 FSEventStreamScheduleWithRunLoop(fsEventStream,
1841 [[NSRunLoop currentRunLoop] getCFRunLoop],
1842 kCFRunLoopDefaultMode);
1844 FSEventStreamStart(fsEventStream);
1845 //NSLog(@"Started FS event stream");
1849 - (void)stopWatchingVimDir
1851 //NSLog(@"%s", _cmd);
1852 #if (MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_4)
1853 if (NULL == FSEventStreamStop)
1854 return; // FSEvent functions are weakly linked
1856 if (fsEventStream) {
1857 FSEventStreamStop(fsEventStream);
1858 FSEventStreamInvalidate(fsEventStream);
1859 FSEventStreamRelease(fsEventStream);
1860 fsEventStream = NULL;
1861 //NSLog(@"Stopped FS event stream");
1867 - (void)handleFSEvent
1869 //NSLog(@"%s", _cmd);
1870 [self clearPreloadCacheWithCount:-1];
1872 // Several FS events may arrive in quick succession so make sure to cancel
1873 // any previous preload requests before making a new one.
1874 [self cancelVimControllerPreloadRequests];
1875 [self scheduleVimControllerPreloadAfterDelay:0.5];
1878 @end // MMAppController (Private)
1884 executeInLoginShell(NSString *path, NSArray *args)
1886 // Start a login shell and execute the command 'path' with arguments 'args'
1887 // in the shell. This ensures that user environment variables are set even
1888 // when MacVim was started from the Finder.
1891 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
1893 // Determine which shell to use to execute the command. The user
1894 // may decide which shell to use by setting a user default or the
1895 // $SHELL environment variable.
1896 NSString *shell = [ud stringForKey:MMLoginShellCommandKey];
1897 if (!shell || [shell length] == 0)
1898 shell = [[[NSProcessInfo processInfo] environment]
1899 objectForKey:@"SHELL"];
1901 shell = @"/bin/bash";
1903 //NSLog(@"shell = %@", shell);
1905 // Bash needs the '-l' flag to launch a login shell. The user may add
1906 // flags by setting a user default.
1907 NSString *shellArgument = [ud stringForKey:MMLoginShellArgumentKey];
1908 if (!shellArgument || [shellArgument length] == 0) {
1909 if ([[shell lastPathComponent] isEqual:@"bash"])
1910 shellArgument = @"-l";
1912 shellArgument = nil;
1915 //NSLog(@"shellArgument = %@", shellArgument);
1917 // Build input string to pipe to the login shell.
1918 NSMutableString *input = [NSMutableString stringWithFormat:
1919 @"exec \"%@\"", path];
1921 // Append all arguments, making sure they are properly quoted, even
1922 // when they contain single quotes.
1923 NSEnumerator *e = [args objectEnumerator];
1926 while ((obj = [e nextObject])) {
1927 NSMutableString *arg = [NSMutableString stringWithString:obj];
1928 [arg replaceOccurrencesOfString:@"'" withString:@"'\"'\"'"
1929 options:NSLiteralSearch
1930 range:NSMakeRange(0, [arg length])];
1931 [input appendFormat:@" '%@'", arg];
1935 // Build the argument vector used to start the login shell.
1936 NSString *shellArg0 = [NSString stringWithFormat:@"-%@",
1937 [shell lastPathComponent]];
1938 char *shellArgv[3] = { (char *)[shellArg0 UTF8String], NULL, NULL };
1940 shellArgv[1] = (char *)[shellArgument UTF8String];
1942 // Get the C string representation of the shell path before the fork since
1943 // we must not call Foundation functions after a fork.
1944 const char *shellPath = [shell fileSystemRepresentation];
1946 // Fork and execute the process.
1948 if (pipe(ds)) return -1;
1953 } else if (pid == 0) {
1955 if (close(ds[1]) == -1) exit(255);
1956 if (dup2(ds[0], 0) == -1) exit(255);
1958 execv(shellPath, shellArgv);
1960 // Never reached unless execv fails
1964 if (close(ds[0]) == -1) return -1;
1966 // Send input to execute to the child process
1967 [input appendString:@"\n"];
1968 int bytes = [input lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
1970 if (write(ds[1], [input UTF8String], bytes) != bytes) return -1;
1971 if (close(ds[1]) == -1) return -1;