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;
100 @interface MMAppController (Private)
101 - (MMVimController *)topmostVimController;
102 - (int)launchVimProcessWithArguments:(NSArray *)args;
103 - (NSArray *)filterFilesAndNotify:(NSArray *)files;
104 - (NSArray *)filterOpenFiles:(NSArray *)filenames
105 openFilesDict:(NSDictionary **)openFiles;
106 #if MM_HANDLE_XCODE_MOD_EVENT
107 - (void)handleXcodeModEvent:(NSAppleEventDescriptor *)event
108 replyEvent:(NSAppleEventDescriptor *)reply;
110 - (void)handleGetURLEvent:(NSAppleEventDescriptor *)event
111 replyEvent:(NSAppleEventDescriptor *)reply;
112 - (int)findLaunchingProcessWithoutArguments;
113 - (MMVimController *)findUnusedEditor;
114 - (NSMutableDictionary *)extractArgumentsFromOdocEvent:
115 (NSAppleEventDescriptor *)desc;
116 - (void)scheduleVimControllerPreloadAfterDelay:(NSTimeInterval)delay;
117 - (void)cancelVimControllerPreloadRequests;
118 - (void)preloadVimController:(id)sender;
119 - (int)maxPreloadCacheSize;
120 - (MMVimController *)takeVimControllerFromCache;
121 - (void)clearPreloadCacheWithCount:(int)count;
122 - (void)rebuildPreloadCache;
123 - (NSDate *)rcFilesModificationDate;
124 - (BOOL)openVimControllerWithArguments:(NSDictionary *)arguments;
125 - (void)activateWhenNextWindowOpens;
126 - (void)startWatchingVimDir;
127 - (void)stopWatchingVimDir;
128 - (void)handleFSEvent;
130 #ifdef MM_ENABLE_PLUGINS
131 - (void)removePlugInMenu;
132 - (void)addPlugInMenuToMenu:(NSMenu *)mainMenu;
138 #if (MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_4)
140 fsEventCallback(ConstFSEventStreamRef streamRef,
141 void *clientCallBackInfo,
144 const FSEventStreamEventFlags eventFlags[],
145 const FSEventStreamEventId eventIds[])
147 [[MMAppController sharedInstance] handleFSEvent];
151 @implementation MMAppController
155 NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:
156 [NSNumber numberWithBool:NO], MMNoWindowKey,
157 [NSNumber numberWithInt:64], MMTabMinWidthKey,
158 [NSNumber numberWithInt:6*64], MMTabMaxWidthKey,
159 [NSNumber numberWithInt:132], MMTabOptimumWidthKey,
160 [NSNumber numberWithInt:2], MMTextInsetLeftKey,
161 [NSNumber numberWithInt:1], MMTextInsetRightKey,
162 [NSNumber numberWithInt:1], MMTextInsetTopKey,
163 [NSNumber numberWithInt:1], MMTextInsetBottomKey,
164 @"MMTypesetter", MMTypesetterKey,
165 [NSNumber numberWithFloat:1], MMCellWidthMultiplierKey,
166 [NSNumber numberWithFloat:-1], MMBaselineOffsetKey,
167 [NSNumber numberWithBool:YES], MMTranslateCtrlClickKey,
168 [NSNumber numberWithInt:0], MMOpenInCurrentWindowKey,
169 [NSNumber numberWithBool:NO], MMNoFontSubstitutionKey,
170 [NSNumber numberWithBool:YES], MMLoginShellKey,
171 [NSNumber numberWithBool:NO], MMAtsuiRendererKey,
172 [NSNumber numberWithInt:MMUntitledWindowAlways],
174 [NSNumber numberWithBool:NO], MMTexturedWindowKey,
175 [NSNumber numberWithBool:NO], MMZoomBothKey,
176 @"", MMLoginShellCommandKey,
177 @"", MMLoginShellArgumentKey,
178 [NSNumber numberWithBool:YES], MMDialogsTrackPwdKey,
179 #ifdef MM_ENABLE_PLUGINS
180 [NSNumber numberWithBool:YES], MMShowLeftPlugInContainerKey,
182 [NSNumber numberWithInt:3], MMOpenLayoutKey,
183 [NSNumber numberWithBool:NO], MMVerticalSplitKey,
184 [NSNumber numberWithInt:0], MMPreloadCacheSizeKey,
185 [NSNumber numberWithInt:0], MMLastWindowClosedBehaviorKey,
188 [[NSUserDefaults standardUserDefaults] registerDefaults:dict];
190 NSArray *types = [NSArray arrayWithObject:NSStringPboardType];
191 [NSApp registerServicesMenuSendTypes:types returnTypes:types];
193 // NOTE: Set the current directory to user's home directory, otherwise it
194 // will default to the root directory. (This matters since new Vim
195 // processes inherit MacVim's environment variables.)
196 [[NSFileManager defaultManager] changeCurrentDirectoryPath:
202 if (!(self = [super init])) return nil;
204 fontContainerRef = loadFonts();
206 vimControllers = [NSMutableArray new];
207 cachedVimControllers = [NSMutableArray new];
209 pidArguments = [NSMutableDictionary new];
211 #ifdef MM_ENABLE_PLUGINS
212 NSString *plugInTitle = NSLocalizedString(@"Plug-In",
213 @"Plug-In menu title");
214 plugInMenuItem = [[NSMenuItem alloc] initWithTitle:plugInTitle
217 NSMenu *submenu = [[NSMenu alloc] initWithTitle:plugInTitle];
218 [plugInMenuItem setSubmenu:submenu];
222 // NOTE: Do not use the default connection since the Logitech Control
223 // Center (LCC) input manager steals and this would cause MacVim to
224 // never open any windows. (This is a bug in LCC but since they are
225 // unlikely to fix it, we graciously give them the default connection.)
226 connection = [[NSConnection alloc] initWithReceivePort:[NSPort port]
228 [connection setRootObject:self];
229 [connection setRequestTimeout:MMRequestTimeout];
230 [connection setReplyTimeout:MMReplyTimeout];
232 // NOTE: When the user is resizing the window the AppKit puts the run
233 // loop in event tracking mode. Unless the connection listens to
234 // request in this mode, live resizing won't work.
235 [connection addRequestMode:NSEventTrackingRunLoopMode];
237 // NOTE! If the name of the connection changes here it must also be
238 // updated in MMBackend.m.
239 NSString *name = [NSString stringWithFormat:@"%@-connection",
240 [[NSBundle mainBundle] bundlePath]];
241 //NSLog(@"Registering connection with name '%@'", name);
242 if (![connection registerName:name]) {
243 NSLog(@"FATAL ERROR: Failed to register connection with name '%@'",
245 [connection release]; connection = nil;
253 //NSLog(@"MMAppController dealloc");
255 [connection release]; connection = nil;
256 [pidArguments release]; pidArguments = nil;
257 [vimControllers release]; vimControllers = nil;
258 [cachedVimControllers release]; cachedVimControllers = nil;
259 [openSelectionString release]; openSelectionString = nil;
260 [recentFilesMenuItem release]; recentFilesMenuItem = nil;
261 [defaultMainMenu release]; defaultMainMenu = nil;
262 #ifdef MM_ENABLE_PLUGINS
263 [plugInMenuItem release]; plugInMenuItem = nil;
265 [appMenuItemTemplate release]; appMenuItemTemplate = nil;
270 - (void)applicationWillFinishLaunching:(NSNotification *)notification
272 // Remember the default menu so that it can be restored if the user closes
273 // all editor windows.
274 defaultMainMenu = [[NSApp mainMenu] retain];
276 // Store a copy of the default app menu so we can use this as a template
277 // for all other menus. We make a copy here because the "Services" menu
278 // will not yet have been populated at this time. If we don't we get
279 // problems trying to set key equivalents later on because they might clash
280 // with items on the "Services" menu.
281 appMenuItemTemplate = [defaultMainMenu itemAtIndex:0];
282 appMenuItemTemplate = [appMenuItemTemplate copy];
284 // Set up the "Open Recent" menu. See
285 // http://lapcatsoftware.com/blog/2007/07/10/
286 // working-without-a-nib-part-5-open-recent-menu/
288 // http://www.cocoabuilder.com/archive/message/cocoa/2007/8/15/187793
289 // for more information.
291 // The menu itself is created in MainMenu.nib but we still seem to have to
292 // hack around a bit to get it to work. (This has to be done in
293 // applicationWillFinishLaunching at the latest, otherwise it doesn't
295 NSMenu *fileMenu = [defaultMainMenu findFileMenu];
297 int idx = [fileMenu indexOfItemWithAction:@selector(fileOpen:)];
298 if (idx >= 0 && idx+1 < [fileMenu numberOfItems])
300 recentFilesMenuItem = [fileMenu itemWithTitle:@"Open Recent"];
301 [[recentFilesMenuItem submenu] performSelector:@selector(_setMenuName:)
302 withObject:@"NSRecentDocumentsMenu"];
304 // Note: The "Recent Files" menu must be moved around since there is no
305 // -[NSApp setRecentFilesMenu:] method. We keep a reference to it to
306 // facilitate this move (see setMainMenu: below).
307 [recentFilesMenuItem retain];
310 #if MM_HANDLE_XCODE_MOD_EVENT
311 [[NSAppleEventManager sharedAppleEventManager]
313 andSelector:@selector(handleXcodeModEvent:replyEvent:)
318 // Register 'mvim://' URL handler
319 [[NSAppleEventManager sharedAppleEventManager]
321 andSelector:@selector(handleGetURLEvent:replyEvent:)
322 forEventClass:kInternetEventClass
323 andEventID:kAEGetURL];
326 - (void)applicationDidFinishLaunching:(NSNotification *)notification
328 [NSApp setServicesProvider:self];
329 #ifdef MM_ENABLE_PLUGINS
330 [[MMPlugInManager sharedManager] loadAllPlugIns];
333 if ([self maxPreloadCacheSize] > 0) {
334 [self scheduleVimControllerPreloadAfterDelay:2];
335 [self startWatchingVimDir];
339 - (BOOL)applicationShouldOpenUntitledFile:(NSApplication *)sender
341 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
342 NSAppleEventManager *aem = [NSAppleEventManager sharedAppleEventManager];
343 NSAppleEventDescriptor *desc = [aem currentAppleEvent];
345 // The user default MMUntitledWindow can be set to control whether an
346 // untitled window should open on 'Open' and 'Reopen' events.
347 int untitledWindowFlag = [ud integerForKey:MMUntitledWindowKey];
348 if ([desc eventID] == kAEOpenApplication
349 && (untitledWindowFlag & MMUntitledWindowOnOpen) == 0)
351 else if ([desc eventID] == kAEReopenApplication
352 && (untitledWindowFlag & MMUntitledWindowOnReopen) == 0)
355 // When a process is started from the command line, the 'Open' event will
356 // contain a parameter to surpress the opening of an untitled window.
357 desc = [desc paramDescriptorForKeyword:keyAEPropData];
358 desc = [desc paramDescriptorForKeyword:keyMMUntitledWindow];
359 if (desc && ![desc booleanValue])
362 // Never open an untitled window if there is at least one open window or if
363 // there are processes that are currently launching.
364 if ([vimControllers count] > 0 || [pidArguments count] > 0)
367 // NOTE! This way it possible to start the app with the command-line
368 // argument '-nowindow yes' and no window will be opened by default.
369 return ![ud boolForKey:MMNoWindowKey];
372 - (BOOL)applicationOpenUntitledFile:(NSApplication *)sender
374 [self newWindow:self];
378 - (void)application:(NSApplication *)sender openFiles:(NSArray *)filenames
380 // Extract ODB/Xcode/Spotlight parameters from the current Apple event,
381 // sort the filenames, and then let openFiles:withArguments: do the heavy
384 if (!(filenames && [filenames count] > 0))
387 // Sort filenames since the Finder doesn't take care in preserving the
388 // order in which files are selected anyway (and "sorted" is more
389 // predictable than "random").
390 if ([filenames count] > 1)
391 filenames = [filenames sortedArrayUsingSelector:
392 @selector(localizedCompare:)];
394 // Extract ODB/Xcode/Spotlight parameters from the current Apple event
395 NSMutableDictionary *arguments = [self extractArgumentsFromOdocEvent:
396 [[NSAppleEventManager sharedAppleEventManager] currentAppleEvent]];
398 if ([self openFiles:filenames withArguments:arguments]) {
399 [NSApp replyToOpenOrPrint:NSApplicationDelegateReplySuccess];
401 // TODO: Notify user of failure?
402 [NSApp replyToOpenOrPrint:NSApplicationDelegateReplyFailure];
406 - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender
408 return (MMTerminateWhenLastWindowClosed ==
409 [[NSUserDefaults standardUserDefaults]
410 integerForKey:MMLastWindowClosedBehaviorKey]);
413 - (NSApplicationTerminateReply)applicationShouldTerminate:
414 (NSApplication *)sender
416 // TODO: Follow Apple's guidelines for 'Graceful Application Termination'
417 // (in particular, allow user to review changes and save).
418 int reply = NSTerminateNow;
419 BOOL modifiedBuffers = NO;
421 // Go through windows, checking for modified buffers. (Each Vim process
422 // tells MacVim when any buffer has been modified and MacVim sets the
423 // 'documentEdited' flag of the window correspondingly.)
424 NSEnumerator *e = [[NSApp windows] objectEnumerator];
426 while ((window = [e nextObject])) {
427 if ([window isDocumentEdited]) {
428 modifiedBuffers = YES;
433 if (modifiedBuffers) {
434 NSAlert *alert = [[NSAlert alloc] init];
435 [alert setAlertStyle:NSWarningAlertStyle];
436 [alert addButtonWithTitle:NSLocalizedString(@"Quit",
438 [alert addButtonWithTitle:NSLocalizedString(@"Cancel",
440 [alert setMessageText:NSLocalizedString(@"Quit without saving?",
441 @"Quit dialog with changed buffers, title")];
442 [alert setInformativeText:NSLocalizedString(
443 @"There are modified buffers, "
444 "if you quit now all changes will be lost. Quit anyway?",
445 @"Quit dialog with changed buffers, text")];
447 if ([alert runModal] != NSAlertFirstButtonReturn)
448 reply = NSTerminateCancel;
452 // No unmodified buffers, but give a warning if there are multiple
453 // windows and/or tabs open.
454 int numWindows = [vimControllers count];
457 // Count the number of open tabs
458 e = [vimControllers objectEnumerator];
460 while ((vc = [e nextObject])) {
461 NSString *eval = [vc evaluateVimExpression:@"tabpagenr('$')"];
463 int count = [eval intValue];
464 if (count > 0 && count < INT_MAX)
469 if (numWindows > 1 || numTabs > 1) {
470 NSAlert *alert = [[NSAlert alloc] init];
471 [alert setAlertStyle:NSWarningAlertStyle];
472 [alert addButtonWithTitle:NSLocalizedString(@"Quit",
474 [alert addButtonWithTitle:NSLocalizedString(@"Cancel",
476 [alert setMessageText:NSLocalizedString(
477 @"Are you sure you want to quit MacVim?",
478 @"Quit dialog with no changed buffers, title")];
480 NSString *info = nil;
481 if (numWindows > 1) {
482 if (numTabs > numWindows)
483 info = [NSString stringWithFormat:NSLocalizedString(
484 @"There are %d windows open in MacVim, with a "
485 "total of %d tabs. Do you want to quit anyway?",
486 @"Quit dialog with no changed buffers, text"),
487 numWindows, numTabs];
489 info = [NSString stringWithFormat:NSLocalizedString(
490 @"There are %d windows open in MacVim. "
491 "Do you want to quit anyway?",
492 @"Quit dialog with no changed buffers, text"),
496 info = [NSString stringWithFormat:NSLocalizedString(
497 @"There are %d tabs open in MacVim. "
498 "Do you want to quit anyway?",
499 @"Quit dialog with no changed buffers, text"),
503 [alert setInformativeText:info];
505 if ([alert runModal] != NSAlertFirstButtonReturn)
506 reply = NSTerminateCancel;
513 // Tell all Vim processes to terminate now (otherwise they'll leave swap
515 if (NSTerminateNow == reply) {
516 e = [vimControllers objectEnumerator];
518 while ((vc = [e nextObject]))
519 [vc sendMessage:TerminateNowMsgID data:nil];
521 e = [cachedVimControllers objectEnumerator];
522 while ((vc = [e nextObject]))
523 [vc sendMessage:TerminateNowMsgID data:nil];
525 // Give Vim processes a chance to terminate before MacVim. If they
526 // haven't terminated by the time applicationWillTerminate: is sent,
527 // they may be forced to quit (see below).
528 usleep(MMTerminationSleepPeriod);
534 - (void)applicationWillTerminate:(NSNotification *)notification
536 [self stopWatchingVimDir];
538 #ifdef MM_ENABLE_PLUGINS
539 [[MMPlugInManager sharedManager] unloadAllPlugIns];
542 #if MM_HANDLE_XCODE_MOD_EVENT
543 [[NSAppleEventManager sharedAppleEventManager]
544 removeEventHandlerForEventClass:'KAHL'
548 // This will invalidate all connections (since they were spawned from this
550 [connection invalidate];
552 // Send a SIGINT to all running Vim processes, so that they are sure to
553 // receive the connectionDidDie: notification (a process has to be checking
554 // the run-loop for this to happen).
555 unsigned i, count = [vimControllers count];
556 for (i = 0; i < count; ++i) {
557 MMVimController *controller = [vimControllers objectAtIndex:i];
558 int pid = [controller pid];
563 if (fontContainerRef) {
564 ATSFontDeactivate(fontContainerRef, NULL, kATSOptionFlagsDefault);
565 fontContainerRef = 0;
568 [NSApp setDelegate:nil];
571 + (MMAppController *)sharedInstance
573 // Note: The app controller is a singleton which is instantiated in
574 // MainMenu.nib where it is also connected as the delegate of NSApp.
575 id delegate = [NSApp delegate];
576 return [delegate isKindOfClass:self] ? (MMAppController*)delegate : nil;
579 - (NSMenu *)defaultMainMenu
581 return defaultMainMenu;
584 - (NSMenuItem *)appMenuItemTemplate
586 return appMenuItemTemplate;
589 - (void)removeVimController:(id)controller
591 int idx = [vimControllers indexOfObject:controller];
592 if (NSNotFound == idx)
595 [controller cleanup];
597 [vimControllers removeObjectAtIndex:idx];
599 if (![vimControllers count]) {
600 // The last editor window just closed so restore the main menu back to
601 // its default state (which is defined in MainMenu.nib).
602 [self setMainMenu:defaultMainMenu];
604 BOOL hide = (MMHideWhenLastWindowClosed ==
605 [[NSUserDefaults standardUserDefaults]
606 integerForKey:MMLastWindowClosedBehaviorKey]);
612 - (void)windowControllerWillOpen:(MMWindowController *)windowController
614 NSPoint topLeft = NSZeroPoint;
615 NSWindow *topWin = [[[self topmostVimController] windowController] window];
616 NSWindow *win = [windowController window];
620 // If there is a window belonging to a Vim process, cascade from it,
621 // otherwise use the autosaved window position (if any).
623 NSRect frame = [topWin frame];
624 topLeft = NSMakePoint(frame.origin.x, NSMaxY(frame));
626 NSString *topLeftString = [[NSUserDefaults standardUserDefaults]
627 stringForKey:MMTopLeftPointKey];
629 topLeft = NSPointFromString(topLeftString);
632 if (!NSEqualPoints(topLeft, NSZeroPoint)) {
633 NSPoint oldTopLeft = topLeft;
635 topLeft = [win cascadeTopLeftFromPoint:topLeft];
637 [win setFrameTopLeftPoint:topLeft];
639 if ([win frame].origin.y < [[win screen] frame].origin.y) {
640 // Try to avoid shifting the new window downwards if it means that
641 // the bottom of the window will be off the screen. E.g. if the
642 // user has set windows to open maximized in the vertical direction
643 // then the new window will cascade horizontally only.
644 topLeft.y = oldTopLeft.y;
645 [win setFrameTopLeftPoint:topLeft];
649 if (1 == [vimControllers count]) {
650 // The first window autosaves its position. (The autosaving
651 // features of Cocoa are not used because we need more control over
652 // what is autosaved and when it is restored.)
653 [windowController setWindowAutosaveKey:MMTopLeftPointKey];
656 if (openSelectionString) {
657 // TODO: Pass this as a parameter instead! Get rid of
658 // 'openSelectionString' etc.
660 // There is some text to paste into this window as a result of the
661 // services menu "Open selection ..." being used.
662 [[windowController vimController] dropString:openSelectionString];
663 [openSelectionString release];
664 openSelectionString = nil;
667 if (shouldActivateWhenNextWindowOpens) {
668 [NSApp activateIgnoringOtherApps:YES];
669 shouldActivateWhenNextWindowOpens = NO;
673 - (void)setMainMenu:(NSMenu *)mainMenu
675 if ([NSApp mainMenu] == mainMenu) return;
677 // If the new menu has a "Recent Files" dummy item, then swap the real item
678 // for the dummy. We are forced to do this since Cocoa initializes the
679 // "Recent Files" menu and there is no way to simply point Cocoa to a new
680 // item each time the menus are swapped.
681 NSMenu *fileMenu = [mainMenu findFileMenu];
682 if (recentFilesMenuItem && fileMenu) {
684 [fileMenu indexOfItemWithAction:@selector(recentFilesDummy:)];
686 NSMenuItem *dummyItem = [[fileMenu itemAtIndex:dummyIdx] retain];
687 [fileMenu removeItemAtIndex:dummyIdx];
689 NSMenu *recentFilesParentMenu = [recentFilesMenuItem menu];
690 int idx = [recentFilesParentMenu indexOfItem:recentFilesMenuItem];
692 [[recentFilesMenuItem retain] autorelease];
693 [recentFilesParentMenu removeItemAtIndex:idx];
694 [recentFilesParentMenu insertItem:dummyItem atIndex:idx];
697 [fileMenu insertItem:recentFilesMenuItem atIndex:dummyIdx];
702 // Now set the new menu. Notice that we keep one menu for each editor
703 // window since each editor can have its own set of menus. When swapping
704 // menus we have to tell Cocoa where the new "MacVim", "Windows", and
705 // "Services" menu are.
706 [NSApp setMainMenu:mainMenu];
708 // Setting the "MacVim" (or "Application") menu ensures that it is typeset
709 // in boldface. (The setAppleMenu: method used to be public but is now
710 // private so this will have to be considered a bit of a hack!)
711 NSMenu *appMenu = [mainMenu findApplicationMenu];
712 [NSApp performSelector:@selector(setAppleMenu:) withObject:appMenu];
714 NSMenu *servicesMenu = [mainMenu findServicesMenu];
715 [NSApp setServicesMenu:servicesMenu];
717 NSMenu *windowsMenu = [mainMenu findWindowsMenu];
719 // Cocoa isn't clever enough to get rid of items it has added to the
720 // "Windows" menu so we have to do it ourselves otherwise there will be
721 // multiple menu items for each window in the "Windows" menu.
722 // This code assumes that the only items Cocoa add are ones which
723 // send off the action makeKeyAndOrderFront:. (Cocoa will not add
724 // another separator item if the last item on the "Windows" menu
725 // already is a separator, so we needen't worry about separators.)
726 int i, count = [windowsMenu numberOfItems];
727 for (i = count-1; i >= 0; --i) {
728 NSMenuItem *item = [windowsMenu itemAtIndex:i];
729 if ([item action] == @selector(makeKeyAndOrderFront:))
730 [windowsMenu removeItem:item];
733 [NSApp setWindowsMenu:windowsMenu];
735 #ifdef MM_ENABLE_PLUGINS
736 // Move plugin menu from old to new main menu.
737 [self removePlugInMenu];
738 [self addPlugInMenuToMenu:mainMenu];
742 - (NSArray *)filterOpenFiles:(NSArray *)filenames
744 return [self filterOpenFiles:filenames openFilesDict:nil];
747 - (BOOL)openFiles:(NSArray *)filenames withArguments:(NSDictionary *)args
749 // Opening files works like this:
750 // a) filter out any already open files
751 // b) open any remaining files
753 // A file is opened in an untitled window if there is one (it may be
754 // currently launching, or it may already be visible), otherwise a new
757 // Each launching Vim process has a dictionary of arguments that are passed
758 // to the process when in checks in (via connectBackend:pid:). The
759 // arguments for each launching process can be looked up by its PID (in the
760 // pidArguments dictionary).
762 NSMutableDictionary *arguments = (args ? [args mutableCopy]
763 : [NSMutableDictionary dictionary]);
766 // a) Filter out any already open files
768 NSString *firstFile = [filenames objectAtIndex:0];
769 MMVimController *firstController = nil;
770 NSDictionary *openFilesDict = nil;
771 filenames = [self filterOpenFiles:filenames openFilesDict:&openFilesDict];
773 // Pass arguments to vim controllers that had files open.
775 NSEnumerator *e = [openFilesDict keyEnumerator];
777 // (Indicate that we do not wish to open any files at the moment.)
778 [arguments setObject:[NSNumber numberWithBool:YES] forKey:@"dontOpen"];
780 while ((key = [e nextObject])) {
781 NSArray *files = [openFilesDict objectForKey:key];
782 [arguments setObject:files forKey:@"filenames"];
784 MMVimController *vc = [key pointerValue];
785 [vc passArguments:arguments];
787 // If this controller holds the first file, then remember it for later.
788 if ([files containsObject:firstFile])
789 firstController = vc;
792 if ([filenames count] == 0) {
793 // Raise the window containing the first file that was already open,
794 // and make sure that the tab containing that file is selected. Only
795 // do this when there are no more files to open, otherwise sometimes
796 // the window with 'firstFile' will be raised, other times it might be
797 // the window that will open with the files in the 'filenames' array.
798 firstFile = [firstFile stringByEscapingSpecialFilenameCharacters];
799 NSString *input = [NSString stringWithFormat:@"<C-\\><C-N>"
800 ":let oldswb=&swb|let &swb=\"useopen,usetab\"|"
801 "tab sb %@|let &swb=oldswb|unl oldswb|"
802 "cal foreground()|redr|f<CR>", firstFile];
804 [firstController addVimInput:input];
809 // Add filenames to "Recent Files" menu, unless they are being edited
810 // remotely (using ODB).
811 if ([arguments objectForKey:@"remoteID"] == nil) {
812 [[NSDocumentController sharedDocumentController]
813 noteNewRecentFilePaths:filenames];
817 // b) Open any remaining files
820 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
821 BOOL openInCurrentWindow = [ud boolForKey:MMOpenInCurrentWindowKey];
823 // The meaning of "layout" is defined by the WIN_* defines in main.c.
824 int layout = [ud integerForKey:MMOpenLayoutKey];
825 BOOL splitVert = [ud boolForKey:MMVerticalSplitKey];
826 if (splitVert && MMLayoutHorizontalSplit == layout)
827 layout = MMLayoutVerticalSplit;
828 if (layout < 0 || (layout > MMLayoutTabs && openInCurrentWindow))
829 layout = MMLayoutTabs;
831 [arguments setObject:[NSNumber numberWithInt:layout] forKey:@"layout"];
832 [arguments setObject:filenames forKey:@"filenames"];
833 // (Indicate that files should be opened from now on.)
834 [arguments setObject:[NSNumber numberWithBool:NO] forKey:@"dontOpen"];
836 if (openInCurrentWindow && (vc = [self topmostVimController])) {
837 // Open files in an already open window.
838 [[[vc windowController] window] makeKeyAndOrderFront:self];
839 [vc passArguments:arguments];
844 int numFiles = [filenames count];
845 if (MMLayoutWindows == layout && numFiles > 1) {
846 // Open one file at a time in a new window, but don't open too many at
847 // once (at most cap+1 windows will open). If the user has increased
848 // the preload cache size we'll take that as a hint that more windows
849 // should be able to open at once.
850 int cap = [self maxPreloadCacheSize] - 1;
851 if (cap < 4) cap = 4;
852 if (cap > numFiles) cap = numFiles;
855 for (i = 0; i < cap; ++i) {
856 NSArray *a = [NSArray arrayWithObject:[filenames objectAtIndex:i]];
857 [arguments setObject:a forKey:@"filenames"];
859 // NOTE: We have to copy the args since we'll mutate them in the
860 // next loop and the below call may retain the arguments while
861 // waiting for a process to start.
862 NSDictionary *args = [[arguments copy] autorelease];
864 openOk = [self openVimControllerWithArguments:args];
868 // Open remaining files in tabs in a new window.
869 if (openOk && numFiles > cap) {
870 NSRange range = { i, numFiles-cap };
871 NSArray *a = [filenames subarrayWithRange:range];
872 [arguments setObject:a forKey:@"filenames"];
873 [arguments setObject:[NSNumber numberWithInt:MMLayoutTabs]
876 openOk = [self openVimControllerWithArguments:arguments];
879 // Open all files at once.
880 openOk = [self openVimControllerWithArguments:arguments];
886 #ifdef MM_ENABLE_PLUGINS
887 - (void)addItemToPlugInMenu:(NSMenuItem *)item
889 NSMenu *menu = [plugInMenuItem submenu];
891 if ([menu numberOfItems] == 1)
892 [self addPlugInMenuToMenu:[NSApp mainMenu]];
895 - (void)removeItemFromPlugInMenu:(NSMenuItem *)item
897 NSMenu *menu = [plugInMenuItem submenu];
898 [menu removeItem:item];
899 if ([menu numberOfItems] == 0)
900 [self removePlugInMenu];
904 - (IBAction)newWindow:(id)sender
906 // A cached controller requires no loading times and results in the new
907 // window popping up instantaneously. If the cache is empty it may take
908 // 1-2 seconds to start a new Vim process.
909 MMVimController *vc = [self takeVimControllerFromCache];
911 [[vc backendProxy] acknowledgeConnection];
913 [self launchVimProcessWithArguments:nil];
917 - (IBAction)newWindowAndActivate:(id)sender
919 [self activateWhenNextWindowOpens];
920 [self newWindow:sender];
923 - (IBAction)fileOpen:(id)sender
926 BOOL trackPwd = [[NSUserDefaults standardUserDefaults]
927 boolForKey:MMDialogsTrackPwdKey];
929 MMVimController *vc = [self keyVimController];
930 if (vc) dir = [[vc vimState] objectForKey:@"pwd"];
933 NSOpenPanel *panel = [NSOpenPanel openPanel];
934 [panel setAllowsMultipleSelection:YES];
935 [panel setAccessoryView:openPanelAccessoryView()];
937 int result = [panel runModalForDirectory:dir file:nil types:nil];
938 if (NSOKButton == result)
939 [self application:NSApp openFiles:[panel filenames]];
942 - (IBAction)selectNextWindow:(id)sender
944 unsigned i, count = [vimControllers count];
947 NSWindow *keyWindow = [NSApp keyWindow];
948 for (i = 0; i < count; ++i) {
949 MMVimController *vc = [vimControllers objectAtIndex:i];
950 if ([[[vc windowController] window] isEqual:keyWindow])
957 MMVimController *vc = [vimControllers objectAtIndex:i];
958 [[vc windowController] showWindow:self];
962 - (IBAction)selectPreviousWindow:(id)sender
964 unsigned i, count = [vimControllers count];
967 NSWindow *keyWindow = [NSApp keyWindow];
968 for (i = 0; i < count; ++i) {
969 MMVimController *vc = [vimControllers objectAtIndex:i];
970 if ([[[vc windowController] window] isEqual:keyWindow])
980 MMVimController *vc = [vimControllers objectAtIndex:i];
981 [[vc windowController] showWindow:self];
985 - (IBAction)orderFrontPreferencePanel:(id)sender
987 [[MMPreferenceController sharedPrefsWindowController] showWindow:self];
990 - (IBAction)openWebsite:(id)sender
992 [[NSWorkspace sharedWorkspace] openURL:
993 [NSURL URLWithString:MMWebsiteString]];
996 - (IBAction)showVimHelp:(id)sender
998 // Open a new window with the help window maximized.
999 [self launchVimProcessWithArguments:[NSArray arrayWithObjects:
1000 @"-c", @":h gui_mac", @"-c", @":res", nil]];
1003 - (IBAction)zoomAll:(id)sender
1005 [NSApp makeWindowsPerform:@selector(performZoom:) inOrder:YES];
1008 - (IBAction)atsuiButtonClicked:(id)sender
1010 // This action is called when the user clicks the "use ATSUI renderer"
1011 // button in the advanced preferences pane.
1012 [self rebuildPreloadCache];
1015 - (IBAction)loginShellButtonClicked:(id)sender
1017 // This action is called when the user clicks the "use login shell" button
1018 // in the advanced preferences pane.
1019 [self rebuildPreloadCache];
1022 - (IBAction)quickstartButtonClicked:(id)sender
1024 if ([self maxPreloadCacheSize] > 0) {
1025 [self scheduleVimControllerPreloadAfterDelay:1.0];
1026 [self startWatchingVimDir];
1028 [self cancelVimControllerPreloadRequests];
1029 [self clearPreloadCacheWithCount:-1];
1030 [self stopWatchingVimDir];
1034 - (byref id <MMFrontendProtocol>)
1035 connectBackend:(byref in id <MMBackendProtocol>)backend
1038 //NSLog(@"Connect backend (pid=%d)", pid);
1039 NSNumber *pidKey = [NSNumber numberWithInt:pid];
1040 MMVimController *vc = nil;
1043 [(NSDistantObject*)backend
1044 setProtocolForProxy:@protocol(MMBackendProtocol)];
1046 vc = [[[MMVimController alloc] initWithBackend:backend pid:pid]
1049 if (preloadPid == pid) {
1050 // This backend was preloaded, so add it to the cache and schedule
1051 // another vim process to be preloaded.
1053 [vc setIsPreloading:YES];
1054 [cachedVimControllers addObject:vc];
1055 [self scheduleVimControllerPreloadAfterDelay:1];
1060 [vimControllers addObject:vc];
1062 id args = [pidArguments objectForKey:pidKey];
1063 if (args && [NSNull null] != args)
1064 [vc passArguments:args];
1066 // HACK! MacVim does not get activated if it is launched from the
1067 // terminal, so we forcibly activate here unless it is an untitled
1068 // window opening. Untitled windows are treated differently, else
1069 // MacVim would steal the focus if another app was activated while the
1070 // untitled window was loading.
1071 if (!args || args != [NSNull null])
1072 [NSApp activateIgnoringOtherApps:YES];
1075 [pidArguments removeObjectForKey:pidKey];
1080 @catch (NSException *e) {
1081 NSLog(@"Exception caught in %s: \"%@\"", _cmd, e);
1084 [vimControllers removeObject:vc];
1086 [pidArguments removeObjectForKey:pidKey];
1092 - (NSArray *)serverList
1094 NSMutableArray *array = [NSMutableArray array];
1096 unsigned i, count = [vimControllers count];
1097 for (i = 0; i < count; ++i) {
1098 MMVimController *controller = [vimControllers objectAtIndex:i];
1099 if ([controller serverName])
1100 [array addObject:[controller serverName]];
1106 - (MMVimController *)keyVimController
1108 NSWindow *keyWindow = [NSApp keyWindow];
1110 unsigned i, count = [vimControllers count];
1111 for (i = 0; i < count; ++i) {
1112 MMVimController *vc = [vimControllers objectAtIndex:i];
1113 if ([[[vc windowController] window] isEqual:keyWindow])
1121 @end // MMAppController
1126 @implementation MMAppController (MMServices)
1128 - (void)openSelection:(NSPasteboard *)pboard userData:(NSString *)userData
1129 error:(NSString **)error
1131 if (![[pboard types] containsObject:NSStringPboardType]) {
1132 NSLog(@"WARNING: Pasteboard contains no object of type "
1133 "NSStringPboardType");
1137 MMVimController *vc = [self topmostVimController];
1139 // Open a new tab first, since dropString: does not do this.
1140 [vc sendMessage:AddNewTabMsgID data:nil];
1141 [vc dropString:[pboard stringForType:NSStringPboardType]];
1143 // NOTE: There is no window to paste the selection into, so save the
1144 // text, open a new window, and paste the text when the next window
1145 // opens. (If this is called several times in a row, then all but the
1146 // last call might be ignored.)
1147 if (openSelectionString) [openSelectionString release];
1148 openSelectionString = [[pboard stringForType:NSStringPboardType] copy];
1150 [self newWindow:self];
1154 - (void)openFile:(NSPasteboard *)pboard userData:(NSString *)userData
1155 error:(NSString **)error
1157 if (![[pboard types] containsObject:NSStringPboardType]) {
1158 NSLog(@"WARNING: Pasteboard contains no object of type "
1159 "NSStringPboardType");
1163 // TODO: Parse multiple filenames and create array with names.
1164 NSString *string = [pboard stringForType:NSStringPboardType];
1165 string = [string stringByTrimmingCharactersInSet:
1166 [NSCharacterSet whitespaceAndNewlineCharacterSet]];
1167 string = [string stringByStandardizingPath];
1169 NSArray *filenames = [self filterFilesAndNotify:
1170 [NSArray arrayWithObject:string]];
1171 if ([filenames count] > 0) {
1172 MMVimController *vc = nil;
1173 if (userData && [userData isEqual:@"Tab"])
1174 vc = [self topmostVimController];
1177 [vc dropFiles:filenames forceOpen:YES];
1179 [self openFiles:filenames withArguments:nil];
1184 @end // MMAppController (MMServices)
1189 @implementation MMAppController (Private)
1191 - (MMVimController *)topmostVimController
1193 // Find the topmost visible window which has an associated vim controller.
1194 NSEnumerator *e = [[NSApp orderedWindows] objectEnumerator];
1196 while ((window = [e nextObject]) && [window isVisible]) {
1197 unsigned i, count = [vimControllers count];
1198 for (i = 0; i < count; ++i) {
1199 MMVimController *vc = [vimControllers objectAtIndex:i];
1200 if ([[[vc windowController] window] isEqual:window])
1208 - (int)launchVimProcessWithArguments:(NSArray *)args
1211 NSString *path = [[NSBundle mainBundle] pathForAuxiliaryExecutable:@"Vim"];
1214 NSLog(@"ERROR: Vim executable could not be found inside app bundle!");
1218 NSArray *taskArgs = [NSArray arrayWithObjects:@"-g", @"-f", nil];
1220 taskArgs = [taskArgs arrayByAddingObjectsFromArray:args];
1222 BOOL useLoginShell = [[NSUserDefaults standardUserDefaults]
1223 boolForKey:MMLoginShellKey];
1224 if (useLoginShell) {
1225 // Run process with a login shell, roughly:
1226 // echo "exec Vim -g -f args" | ARGV0=-`basename $SHELL` $SHELL [-l]
1227 pid = executeInLoginShell(path, taskArgs);
1229 // Run process directly:
1231 NSTask *task = [NSTask launchedTaskWithLaunchPath:path
1232 arguments:taskArgs];
1233 pid = task ? [task processIdentifier] : -1;
1237 // NOTE: If the process has no arguments, then add a null argument to
1238 // the pidArguments dictionary. This is later used to detect that a
1239 // process without arguments is being launched.
1241 [pidArguments setObject:[NSNull null]
1242 forKey:[NSNumber numberWithInt:pid]];
1244 NSLog(@"WARNING: %s%@ failed (useLoginShell=%d)", _cmd, args,
1251 - (NSArray *)filterFilesAndNotify:(NSArray *)filenames
1253 // Go trough 'filenames' array and make sure each file exists. Present
1254 // warning dialog if some file was missing.
1256 NSString *firstMissingFile = nil;
1257 NSMutableArray *files = [NSMutableArray array];
1258 unsigned i, count = [filenames count];
1260 for (i = 0; i < count; ++i) {
1261 NSString *name = [filenames objectAtIndex:i];
1262 if ([[NSFileManager defaultManager] fileExistsAtPath:name]) {
1263 [files addObject:name];
1264 } else if (!firstMissingFile) {
1265 firstMissingFile = name;
1269 if (firstMissingFile) {
1270 NSAlert *alert = [[NSAlert alloc] init];
1271 [alert addButtonWithTitle:NSLocalizedString(@"OK",
1275 if ([files count] >= count-1) {
1276 [alert setMessageText:NSLocalizedString(@"File not found",
1277 @"File not found dialog, title")];
1278 text = [NSString stringWithFormat:NSLocalizedString(
1279 @"Could not open file with name %@.",
1280 @"File not found dialog, text"), firstMissingFile];
1282 [alert setMessageText:NSLocalizedString(@"Multiple files not found",
1283 @"File not found dialog, title")];
1284 text = [NSString stringWithFormat:NSLocalizedString(
1285 @"Could not open file with name %@, and %d other files.",
1286 @"File not found dialog, text"),
1287 firstMissingFile, count-[files count]-1];
1290 [alert setInformativeText:text];
1291 [alert setAlertStyle:NSWarningAlertStyle];
1296 [NSApp replyToOpenOrPrint:NSApplicationDelegateReplyFailure];
1302 - (NSArray *)filterOpenFiles:(NSArray *)filenames
1303 openFilesDict:(NSDictionary **)openFiles
1305 // Filter out any files in the 'filenames' array that are open and return
1306 // all files that are not already open. On return, the 'openFiles'
1307 // parameter (if non-nil) will point to a dictionary of open files, indexed
1308 // by Vim controller.
1310 NSMutableDictionary *dict = [NSMutableDictionary dictionary];
1311 NSMutableArray *files = [filenames mutableCopy];
1313 // TODO: Escape special characters in 'files'?
1314 NSString *expr = [NSString stringWithFormat:
1315 @"map([\"%@\"],\"bufloaded(v:val)\")",
1316 [files componentsJoinedByString:@"\",\""]];
1318 unsigned i, count = [vimControllers count];
1319 for (i = 0; i < count && [files count] > 0; ++i) {
1320 MMVimController *vc = [vimControllers objectAtIndex:i];
1322 // Query Vim for which files in the 'files' array are open.
1323 NSString *eval = [vc evaluateVimExpression:expr];
1324 if (!eval) continue;
1326 NSIndexSet *idxSet = [NSIndexSet indexSetWithVimList:eval];
1327 if ([idxSet count] > 0) {
1328 [dict setObject:[files objectsAtIndexes:idxSet]
1329 forKey:[NSValue valueWithPointer:vc]];
1331 // Remove all the files that were open in this Vim process and
1332 // create a new expression to evaluate.
1333 [files removeObjectsAtIndexes:idxSet];
1334 expr = [NSString stringWithFormat:
1335 @"map([\"%@\"],\"bufloaded(v:val)\")",
1336 [files componentsJoinedByString:@"\",\""]];
1340 if (openFiles != nil)
1346 #if MM_HANDLE_XCODE_MOD_EVENT
1347 - (void)handleXcodeModEvent:(NSAppleEventDescriptor *)event
1348 replyEvent:(NSAppleEventDescriptor *)reply
1351 // Xcode sends this event to query MacVim which open files have been
1353 NSLog(@"reply:%@", reply);
1354 NSLog(@"event:%@", event);
1356 NSEnumerator *e = [vimControllers objectEnumerator];
1358 while ((vc = [e nextObject])) {
1359 DescType type = [reply descriptorType];
1360 unsigned len = [[type data] length];
1361 NSMutableData *data = [NSMutableData data];
1363 [data appendBytes:&type length:sizeof(DescType)];
1364 [data appendBytes:&len length:sizeof(unsigned)];
1365 [data appendBytes:[reply data] length:len];
1367 [vc sendMessage:XcodeModMsgID data:data];
1373 - (void)handleGetURLEvent:(NSAppleEventDescriptor *)event
1374 replyEvent:(NSAppleEventDescriptor *)reply
1376 NSString *urlString = [[event paramDescriptorForKeyword:keyDirectObject]
1378 NSURL *url = [NSURL URLWithString:urlString];
1380 // We try to be compatible with TextMate's URL scheme here, as documented
1381 // at http://blog.macromates.com/2007/the-textmate-url-scheme/ . Currently,
1384 // The format is: mvim://open?<arguments> where arguments can be:
1386 // * url — the actual file to open (i.e. a file://… URL), if you leave
1387 // out this argument, the frontmost document is implied.
1388 // * line — line number to go to (one based).
1389 // * column — column number to go to (one based).
1391 // Example: mvim://open?url=file:///etc/profile&line=20
1393 if ([[url host] isEqualToString:@"open"]) {
1394 NSMutableDictionary *dict = [NSMutableDictionary dictionary];
1396 // Parse query ("url=file://...&line=14") into a dictionary
1397 NSArray *queries = [[url query] componentsSeparatedByString:@"&"];
1398 NSEnumerator *enumerator = [queries objectEnumerator];
1400 while( param = [enumerator nextObject] ) {
1401 NSArray *arr = [param componentsSeparatedByString:@"="];
1402 if ([arr count] == 2) {
1403 [dict setValue:[[arr lastObject]
1404 stringByReplacingPercentEscapesUsingEncoding:
1405 NSUTF8StringEncoding]
1406 forKey:[[arr objectAtIndex:0]
1407 stringByReplacingPercentEscapesUsingEncoding:
1408 NSUTF8StringEncoding]];
1412 // Actually open the file.
1413 NSString *file = [dict objectForKey:@"url"];
1415 NSURL *fileUrl= [NSURL URLWithString:file];
1416 // TextMate only opens files that already exist.
1417 if ([fileUrl isFileURL]
1418 && [[NSFileManager defaultManager] fileExistsAtPath:
1420 // Strip 'file://' path, else application:openFiles: might think
1421 // the file is not yet open.
1422 NSArray *filenames = [NSArray arrayWithObject:[fileUrl path]];
1424 // Look for the line and column options.
1425 NSDictionary *args = nil;
1426 NSString *line = [dict objectForKey:@"line"];
1428 NSString *column = [dict objectForKey:@"column"];
1430 args = [NSDictionary dictionaryWithObjectsAndKeys:
1431 line, @"cursorLine",
1432 column, @"cursorColumn",
1435 args = [NSDictionary dictionaryWithObject:line
1436 forKey:@"cursorLine"];
1439 [self openFiles:filenames withArguments:args];
1443 NSAlert *alert = [[NSAlert alloc] init];
1444 [alert addButtonWithTitle:NSLocalizedString(@"OK",
1447 [alert setMessageText:NSLocalizedString(@"Unknown URL Scheme",
1448 @"Unknown URL Scheme dialog, title")];
1449 [alert setInformativeText:[NSString stringWithFormat:NSLocalizedString(
1450 @"This version of MacVim does not support \"%@\""
1451 @" in its URL scheme.",
1452 @"Unknown URL Scheme dialog, text"),
1455 [alert setAlertStyle:NSWarningAlertStyle];
1462 - (int)findLaunchingProcessWithoutArguments
1464 NSArray *keys = [pidArguments allKeysForObject:[NSNull null]];
1465 if ([keys count] > 0) {
1466 //NSLog(@"found launching process without arguments");
1467 return [[keys objectAtIndex:0] intValue];
1473 - (MMVimController *)findUnusedEditor
1475 NSEnumerator *e = [vimControllers objectEnumerator];
1477 while ((vc = [e nextObject])) {
1478 if ([[[vc vimState] objectForKey:@"unusedEditor"] boolValue])
1485 - (NSMutableDictionary *)extractArgumentsFromOdocEvent:
1486 (NSAppleEventDescriptor *)desc
1488 NSMutableDictionary *dict = [NSMutableDictionary dictionary];
1490 // 1. Extract ODB parameters (if any)
1491 NSAppleEventDescriptor *odbdesc = desc;
1492 if (![odbdesc paramDescriptorForKeyword:keyFileSender]) {
1493 // The ODB paramaters may hide inside the 'keyAEPropData' descriptor.
1494 odbdesc = [odbdesc paramDescriptorForKeyword:keyAEPropData];
1495 if (![odbdesc paramDescriptorForKeyword:keyFileSender])
1500 NSAppleEventDescriptor *p =
1501 [odbdesc paramDescriptorForKeyword:keyFileSender];
1503 [dict setObject:[NSNumber numberWithUnsignedInt:[p typeCodeValue]]
1504 forKey:@"remoteID"];
1506 p = [odbdesc paramDescriptorForKeyword:keyFileCustomPath];
1508 [dict setObject:[p stringValue] forKey:@"remotePath"];
1510 p = [odbdesc paramDescriptorForKeyword:keyFileSenderToken];
1512 [dict setObject:[NSNumber numberWithUnsignedLong:[p descriptorType]]
1513 forKey:@"remoteTokenDescType"];
1514 [dict setObject:[p data] forKey:@"remoteTokenData"];
1518 // 2. Extract Xcode parameters (if any)
1519 NSAppleEventDescriptor *xcodedesc =
1520 [desc paramDescriptorForKeyword:keyAEPosition];
1523 MMSelectionRange *sr = (MMSelectionRange*)[[xcodedesc data] bytes];
1525 if (sr->lineNum < 0) {
1526 // Should select a range of lines.
1527 range.location = sr->startRange + 1;
1528 range.length = sr->endRange - sr->startRange + 1;
1530 // Should only move cursor to a line.
1531 range.location = sr->lineNum + 1;
1535 [dict setObject:NSStringFromRange(range) forKey:@"selectionRange"];
1538 // 3. Extract Spotlight search text (if any)
1539 NSAppleEventDescriptor *spotlightdesc =
1540 [desc paramDescriptorForKeyword:keyAESearchText];
1542 [dict setObject:[spotlightdesc stringValue] forKey:@"searchText"];
1547 #ifdef MM_ENABLE_PLUGINS
1548 - (void)removePlugInMenu
1550 if ([plugInMenuItem menu])
1551 [[plugInMenuItem menu] removeItem:plugInMenuItem];
1554 - (void)addPlugInMenuToMenu:(NSMenu *)mainMenu
1556 NSMenu *windowsMenu = [mainMenu findWindowsMenu];
1558 if ([[plugInMenuItem submenu] numberOfItems] > 0) {
1559 int idx = windowsMenu ? [mainMenu indexOfItemWithSubmenu:windowsMenu]
1562 [mainMenu insertItem:plugInMenuItem atIndex:idx];
1564 [mainMenu addItem:plugInMenuItem];
1570 - (void)scheduleVimControllerPreloadAfterDelay:(NSTimeInterval)delay
1572 [self performSelector:@selector(preloadVimController:)
1577 - (void)cancelVimControllerPreloadRequests
1579 [NSObject cancelPreviousPerformRequestsWithTarget:self
1580 selector:@selector(preloadVimController:)
1584 - (void)preloadVimController:(id)sender
1586 // We only allow preloading of one Vim process at a time (to avoid hogging
1587 // CPU), so schedule another preload in a little while if necessary.
1588 if (-1 != preloadPid) {
1589 [self scheduleVimControllerPreloadAfterDelay:2];
1593 if ([cachedVimControllers count] >= [self maxPreloadCacheSize])
1596 preloadPid = [self launchVimProcessWithArguments:
1597 [NSArray arrayWithObject:@"--mmwaitforack"]];
1600 - (int)maxPreloadCacheSize
1602 // The maximum number of Vim processes to keep in the cache can be
1603 // controlled via the user default "MMPreloadCacheSize".
1604 int maxCacheSize = [[NSUserDefaults standardUserDefaults]
1605 integerForKey:MMPreloadCacheSizeKey];
1606 if (maxCacheSize < 0) maxCacheSize = 0;
1607 else if (maxCacheSize > 10) maxCacheSize = 10;
1609 return maxCacheSize;
1612 - (MMVimController *)takeVimControllerFromCache
1614 // NOTE: After calling this message the backend corresponding to the
1615 // returned vim controller must be sent an acknowledgeConnection message,
1616 // else the vim process will be stuck.
1618 // This method may return nil even though the cache might be non-empty; the
1619 // caller should handle this by starting a new Vim process.
1621 int i, count = [cachedVimControllers count];
1622 if (0 == count) return nil;
1624 // Locate the first Vim controller with up-to-date rc-files sourced.
1625 NSDate *rcDate = [self rcFilesModificationDate];
1626 for (i = 0; i < count; ++i) {
1627 MMVimController *vc = [cachedVimControllers objectAtIndex:i];
1628 NSDate *date = [vc creationDate];
1629 if ([date compare:rcDate] != NSOrderedAscending)
1634 // Clear out cache entries whose vimrc/gvimrc files were sourced before
1635 // the latest modification date for those files. This ensures that the
1636 // latest rc-files are always sourced for new windows.
1637 [self clearPreloadCacheWithCount:i];
1640 if ([cachedVimControllers count] == 0) {
1641 [self scheduleVimControllerPreloadAfterDelay:2.0];
1645 MMVimController *vc = [cachedVimControllers objectAtIndex:0];
1646 [vimControllers addObject:vc];
1647 [cachedVimControllers removeObjectAtIndex:0];
1648 [vc setIsPreloading:NO];
1650 // If the Vim process has finished loading then the window will displayed
1651 // now, otherwise it will be displayed when the OpenWindowMsgID message is
1653 [[vc windowController] showWindow];
1655 // Since we've taken one controller from the cache we take the opportunity
1656 // to preload another.
1657 [self scheduleVimControllerPreloadAfterDelay:1];
1662 - (void)clearPreloadCacheWithCount:(int)count
1664 // Remove the 'count' first entries in the preload cache. It is assumed
1665 // that objects are added/removed from the cache in a FIFO manner so that
1666 // this effectively clears the 'count' oldest entries.
1667 // If 'count' is negative, then the entire cache is cleared.
1669 if ([cachedVimControllers count] == 0 || count == 0)
1673 count = [cachedVimControllers count];
1675 // Make sure the preloaded Vim processes get killed or they'll just hang
1676 // around being useless until MacVim is terminated.
1677 NSEnumerator *e = [cachedVimControllers objectEnumerator];
1678 MMVimController *vc;
1680 while ((vc = [e nextObject]) && n-- > 0) {
1681 [[NSNotificationCenter defaultCenter] removeObserver:vc];
1682 [vc sendMessage:TerminateNowMsgID data:nil];
1684 // Since the preloaded processes were killed "prematurely" we have to
1685 // manually tell them to cleanup (it is not enough to simply release
1686 // them since deallocation and cleanup are separated).
1691 while (n-- > 0 && [cachedVimControllers count] > 0)
1692 [cachedVimControllers removeObjectAtIndex:0];
1695 - (void)rebuildPreloadCache
1697 if ([self maxPreloadCacheSize] > 0) {
1698 [self clearPreloadCacheWithCount:-1];
1699 [self cancelVimControllerPreloadRequests];
1700 [self scheduleVimControllerPreloadAfterDelay:1.0];
1704 - (NSDate *)rcFilesModificationDate
1706 // Check modification dates for ~/.vimrc and ~/.gvimrc and return the
1707 // latest modification date. If ~/.vimrc does not exist, check ~/_vimrc
1708 // and similarly for gvimrc.
1709 // Returns distantPath if no rc files were found.
1711 NSDate *date = [NSDate distantPast];
1712 NSFileManager *fm = [NSFileManager defaultManager];
1714 NSString *path = [@"~/.vimrc" stringByExpandingTildeInPath];
1715 NSDictionary *attr = [fm fileAttributesAtPath:path traverseLink:YES];
1717 path = [@"~/_vimrc" stringByExpandingTildeInPath];
1718 attr = [fm fileAttributesAtPath:path traverseLink:YES];
1720 NSDate *modDate = [attr objectForKey:NSFileModificationDate];
1724 path = [@"~/.gvimrc" stringByExpandingTildeInPath];
1725 attr = [fm fileAttributesAtPath:path traverseLink:YES];
1727 path = [@"~/_gvimrc" stringByExpandingTildeInPath];
1728 attr = [fm fileAttributesAtPath:path traverseLink:YES];
1730 modDate = [attr objectForKey:NSFileModificationDate];
1732 date = [date laterDate:modDate];
1737 - (BOOL)openVimControllerWithArguments:(NSDictionary *)arguments
1739 MMVimController *vc = [self findUnusedEditor];
1741 // Open files in an already open window.
1742 [[[vc windowController] window] makeKeyAndOrderFront:self];
1743 [vc passArguments:arguments];
1744 } else if ((vc = [self takeVimControllerFromCache])) {
1745 // Open files in a new window using a cached vim controller. This
1746 // requires virtually no loading time so the new window will pop up
1748 [vc passArguments:arguments];
1749 [[vc backendProxy] acknowledgeConnection];
1751 // Open files in a launching Vim process or start a new process. This
1752 // may take 1-2 seconds so there will be a visible delay before the
1753 // window appears on screen.
1754 int pid = [self findLaunchingProcessWithoutArguments];
1756 pid = [self launchVimProcessWithArguments:nil];
1761 // TODO: If the Vim process fails to start, or if it changes PID,
1762 // then the memory allocated for these parameters will leak.
1763 // Ensure that this cannot happen or somehow detect it.
1765 if ([arguments count] > 0)
1766 [pidArguments setObject:arguments
1767 forKey:[NSNumber numberWithInt:pid]];
1773 - (void)activateWhenNextWindowOpens
1775 shouldActivateWhenNextWindowOpens = YES;
1778 - (void)startWatchingVimDir
1780 //NSLog(@"%s", _cmd);
1781 #if (MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_4)
1784 if (NULL == FSEventStreamStart)
1785 return; // FSEvent functions are weakly linked
1787 NSString *path = [@"~/.vim" stringByExpandingTildeInPath];
1788 NSArray *pathsToWatch = [NSArray arrayWithObject:path];
1790 fsEventStream = FSEventStreamCreate(NULL, &fsEventCallback, NULL,
1791 (CFArrayRef)pathsToWatch, kFSEventStreamEventIdSinceNow,
1792 MMEventStreamLatency, kFSEventStreamCreateFlagNone);
1794 FSEventStreamScheduleWithRunLoop(fsEventStream,
1795 [[NSRunLoop currentRunLoop] getCFRunLoop],
1796 kCFRunLoopDefaultMode);
1798 FSEventStreamStart(fsEventStream);
1799 //NSLog(@"Started FS event stream");
1803 - (void)stopWatchingVimDir
1805 //NSLog(@"%s", _cmd);
1806 #if (MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_4)
1807 if (NULL == FSEventStreamStop)
1808 return; // FSEvent functions are weakly linked
1810 if (fsEventStream) {
1811 FSEventStreamStop(fsEventStream);
1812 FSEventStreamInvalidate(fsEventStream);
1813 FSEventStreamRelease(fsEventStream);
1814 fsEventStream = NULL;
1815 //NSLog(@"Stopped FS event stream");
1821 - (void)handleFSEvent
1823 //NSLog(@"%s", _cmd);
1824 [self clearPreloadCacheWithCount:-1];
1826 // Several FS events may arrive in quick succession so make sure to cancel
1827 // any previous preload requests before making a new one.
1828 [self cancelVimControllerPreloadRequests];
1829 [self scheduleVimControllerPreloadAfterDelay:0.5];
1832 @end // MMAppController (Private)
1838 executeInLoginShell(NSString *path, NSArray *args)
1840 // Start a login shell and execute the command 'path' with arguments 'args'
1841 // in the shell. This ensures that user environment variables are set even
1842 // when MacVim was started from the Finder.
1845 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
1847 // Determine which shell to use to execute the command. The user
1848 // may decide which shell to use by setting a user default or the
1849 // $SHELL environment variable.
1850 NSString *shell = [ud stringForKey:MMLoginShellCommandKey];
1851 if (!shell || [shell length] == 0)
1852 shell = [[[NSProcessInfo processInfo] environment]
1853 objectForKey:@"SHELL"];
1855 shell = @"/bin/bash";
1857 //NSLog(@"shell = %@", shell);
1859 // Bash needs the '-l' flag to launch a login shell. The user may add
1860 // flags by setting a user default.
1861 NSString *shellArgument = [ud stringForKey:MMLoginShellArgumentKey];
1862 if (!shellArgument || [shellArgument length] == 0) {
1863 if ([[shell lastPathComponent] isEqual:@"bash"])
1864 shellArgument = @"-l";
1866 shellArgument = nil;
1869 //NSLog(@"shellArgument = %@", shellArgument);
1871 // Build input string to pipe to the login shell.
1872 NSMutableString *input = [NSMutableString stringWithFormat:
1873 @"exec \"%@\"", path];
1875 // Append all arguments, making sure they are properly quoted, even
1876 // when they contain single quotes.
1877 NSEnumerator *e = [args objectEnumerator];
1880 while ((obj = [e nextObject])) {
1881 NSMutableString *arg = [NSMutableString stringWithString:obj];
1882 [arg replaceOccurrencesOfString:@"'" withString:@"'\"'\"'"
1883 options:NSLiteralSearch
1884 range:NSMakeRange(0, [arg length])];
1885 [input appendFormat:@" '%@'", arg];
1889 // Build the argument vector used to start the login shell.
1890 NSString *shellArg0 = [NSString stringWithFormat:@"-%@",
1891 [shell lastPathComponent]];
1892 char *shellArgv[3] = { (char *)[shellArg0 UTF8String], NULL, NULL };
1894 shellArgv[1] = (char *)[shellArgument UTF8String];
1896 // Get the C string representation of the shell path before the fork since
1897 // we must not call Foundation functions after a fork.
1898 const char *shellPath = [shell fileSystemRepresentation];
1900 // Fork and execute the process.
1902 if (pipe(ds)) return -1;
1907 } else if (pid == 0) {
1909 if (close(ds[1]) == -1) exit(255);
1910 if (dup2(ds[0], 0) == -1) exit(255);
1912 execv(shellPath, shellArgv);
1914 // Never reached unless execv fails
1918 if (close(ds[0]) == -1) return -1;
1920 // Send input to execute to the child process
1921 [input appendString:@"\n"];
1922 int bytes = [input lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
1924 if (write(ds[1], [input UTF8String], bytes) != bytes) return -1;
1925 if (close(ds[1]) == -1) return -1;