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"
53 #define MM_HANDLE_XCODE_MOD_EVENT 0
57 // Default timeout intervals on all connections.
58 static NSTimeInterval MMRequestTimeout = 5;
59 static NSTimeInterval MMReplyTimeout = 5;
61 static NSString *MMWebsiteString = @"http://code.google.com/p/macvim/";
63 // When terminating, notify Vim processes then sleep for these many
65 static useconds_t MMTerminationSleepPeriod = 10000;
68 #pragma options align=mac68k
71 short unused1; // 0 (not used)
72 short lineNum; // line to select (< 0 to specify range)
73 long startRange; // start of selection range (if line < 0)
74 long endRange; // end of selection range (if line < 0)
75 long unused2; // 0 (not used)
76 long theDate; // modification date/time
78 #pragma options align=reset
81 static int executeInLoginShell(NSString *path, NSArray *args);
84 @interface MMAppController (MMServices)
85 - (void)openSelection:(NSPasteboard *)pboard userData:(NSString *)userData
86 error:(NSString **)error;
87 - (void)openFile:(NSPasteboard *)pboard userData:(NSString *)userData
88 error:(NSString **)error;
92 @interface MMAppController (Private)
93 - (MMVimController *)topmostVimController;
94 - (int)launchVimProcessWithArguments:(NSArray *)args;
95 - (NSArray *)filterFilesAndNotify:(NSArray *)files;
96 - (NSArray *)filterOpenFiles:(NSArray *)filenames
97 arguments:(NSDictionary *)args;
98 #if MM_HANDLE_XCODE_MOD_EVENT
99 - (void)handleXcodeModEvent:(NSAppleEventDescriptor *)event
100 replyEvent:(NSAppleEventDescriptor *)reply;
102 - (int)findLaunchingProcessWithoutArguments;
103 - (MMVimController *)findUntitledWindow;
104 - (NSMutableDictionary *)extractArgumentsFromOdocEvent:
105 (NSAppleEventDescriptor *)desc;
106 - (void)passArguments:(NSDictionary *)args toVimController:(MMVimController*)vc;
108 #ifdef MM_ENABLE_PLUGINS
109 - (void)removePlugInMenu;
110 - (void)addPlugInMenuToMenu:(NSMenu *)mainMenu;
116 @implementation MMAppController
120 NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:
121 [NSNumber numberWithBool:NO], MMNoWindowKey,
122 [NSNumber numberWithInt:64], MMTabMinWidthKey,
123 [NSNumber numberWithInt:6*64], MMTabMaxWidthKey,
124 [NSNumber numberWithInt:132], MMTabOptimumWidthKey,
125 [NSNumber numberWithInt:2], MMTextInsetLeftKey,
126 [NSNumber numberWithInt:1], MMTextInsetRightKey,
127 [NSNumber numberWithInt:1], MMTextInsetTopKey,
128 [NSNumber numberWithInt:1], MMTextInsetBottomKey,
129 [NSNumber numberWithBool:NO], MMTerminateAfterLastWindowClosedKey,
130 @"MMTypesetter", MMTypesetterKey,
131 [NSNumber numberWithFloat:1], MMCellWidthMultiplierKey,
132 [NSNumber numberWithFloat:-1], MMBaselineOffsetKey,
133 [NSNumber numberWithBool:YES], MMTranslateCtrlClickKey,
134 [NSNumber numberWithBool:NO], MMOpenFilesInTabsKey,
135 [NSNumber numberWithBool:NO], MMNoFontSubstitutionKey,
136 [NSNumber numberWithBool:NO], MMLoginShellKey,
137 [NSNumber numberWithBool:NO], MMAtsuiRendererKey,
138 [NSNumber numberWithInt:MMUntitledWindowAlways],
140 [NSNumber numberWithBool:NO], MMTexturedWindowKey,
141 [NSNumber numberWithBool:NO], MMZoomBothKey,
142 @"", MMLoginShellCommandKey,
143 @"", MMLoginShellArgumentKey,
144 [NSNumber numberWithBool:YES], MMDialogsTrackPwdKey,
147 [[NSUserDefaults standardUserDefaults] registerDefaults:dict];
149 NSArray *types = [NSArray arrayWithObject:NSStringPboardType];
150 [NSApp registerServicesMenuSendTypes:types returnTypes:types];
155 if ((self = [super init])) {
156 fontContainerRef = loadFonts();
158 vimControllers = [NSMutableArray new];
159 pidArguments = [NSMutableDictionary new];
161 #ifdef MM_ENABLE_PLUGINS
162 NSString *plugInTitle = NSLocalizedString(@"Plug-In",
163 @"Plug-In menu title");
164 plugInMenuItem = [[NSMenuItem alloc] initWithTitle:plugInTitle
167 NSMenu *submenu = [[NSMenu alloc] initWithTitle:plugInTitle];
168 [plugInMenuItem setSubmenu:submenu];
172 // NOTE: Do not use the default connection since the Logitech Control
173 // Center (LCC) input manager steals and this would cause MacVim to
174 // never open any windows. (This is a bug in LCC but since they are
175 // unlikely to fix it, we graciously give them the default connection.)
176 connection = [[NSConnection alloc] initWithReceivePort:[NSPort port]
178 [connection setRootObject:self];
179 [connection setRequestTimeout:MMRequestTimeout];
180 [connection setReplyTimeout:MMReplyTimeout];
182 // NOTE: When the user is resizing the window the AppKit puts the run
183 // loop in event tracking mode. Unless the connection listens to
184 // request in this mode, live resizing won't work.
185 [connection addRequestMode:NSEventTrackingRunLoopMode];
187 // NOTE! If the name of the connection changes here it must also be
188 // updated in MMBackend.m.
189 NSString *name = [NSString stringWithFormat:@"%@-connection",
190 [[NSBundle mainBundle] bundleIdentifier]];
191 //NSLog(@"Registering connection with name '%@'", name);
192 if (![connection registerName:name]) {
193 NSLog(@"FATAL ERROR: Failed to register connection with name '%@'",
195 [connection release]; connection = nil;
204 //NSLog(@"MMAppController dealloc");
206 [connection release]; connection = nil;
207 [pidArguments release]; pidArguments = nil;
208 [vimControllers release]; vimControllers = nil;
209 [openSelectionString release]; openSelectionString = nil;
210 [recentFilesMenuItem release]; recentFilesMenuItem = nil;
211 [defaultMainMenu release]; defaultMainMenu = nil;
212 #ifdef MM_ENABLE_PLUGINS
213 [plugInMenuItem release]; plugInMenuItem = nil;
215 [appMenuItemTemplate release]; appMenuItemTemplate = nil;
220 - (void)applicationWillFinishLaunching:(NSNotification *)notification
222 // Remember the default menu so that it can be restored if the user closes
223 // all editor windows.
224 defaultMainMenu = [[NSApp mainMenu] retain];
226 // Store a copy of the default app menu so we can use this as a template
227 // for all other menus. We make a copy here because the "Services" menu
228 // will not yet have been populated at this time. If we don't we get
229 // problems trying to set key equivalents later on because they might clash
230 // with items on the "Services" menu.
231 appMenuItemTemplate = [defaultMainMenu itemAtIndex:0];
232 appMenuItemTemplate = [appMenuItemTemplate copy];
234 // Set up the "Open Recent" menu. See
235 // http://lapcatsoftware.com/blog/2007/07/10/
236 // working-without-a-nib-part-5-open-recent-menu/
238 // http://www.cocoabuilder.com/archive/message/cocoa/2007/8/15/187793
239 // for more information.
241 // The menu itself is created in MainMenu.nib but we still seem to have to
242 // hack around a bit to get it to work. (This has to be done in
243 // applicationWillFinishLaunching at the latest, otherwise it doesn't
245 NSMenu *fileMenu = [defaultMainMenu findFileMenu];
247 int idx = [fileMenu indexOfItemWithAction:@selector(fileOpen:)];
248 if (idx >= 0 && idx+1 < [fileMenu numberOfItems])
250 recentFilesMenuItem = [fileMenu itemWithTitle:@"Open Recent"];
251 [[recentFilesMenuItem submenu] performSelector:@selector(_setMenuName:)
252 withObject:@"NSRecentDocumentsMenu"];
254 // Note: The "Recent Files" menu must be moved around since there is no
255 // -[NSApp setRecentFilesMenu:] method. We keep a reference to it to
256 // facilitate this move (see setMainMenu: below).
257 [recentFilesMenuItem retain];
260 #if MM_HANDLE_XCODE_MOD_EVENT
261 [[NSAppleEventManager sharedAppleEventManager]
263 andSelector:@selector(handleXcodeModEvent:replyEvent:)
269 - (void)applicationDidFinishLaunching:(NSNotification *)notification
271 [NSApp setServicesProvider:self];
272 #ifdef MM_ENABLE_PLUGINS
273 [[MMPlugInManager sharedManager] loadAllPlugIns];
277 - (BOOL)applicationShouldOpenUntitledFile:(NSApplication *)sender
279 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
280 NSAppleEventManager *aem = [NSAppleEventManager sharedAppleEventManager];
281 NSAppleEventDescriptor *desc = [aem currentAppleEvent];
283 // The user default MMUntitledWindow can be set to control whether an
284 // untitled window should open on 'Open' and 'Reopen' events.
285 int untitledWindowFlag = [ud integerForKey:MMUntitledWindowKey];
286 if ([desc eventID] == kAEOpenApplication
287 && (untitledWindowFlag & MMUntitledWindowOnOpen) == 0)
289 else if ([desc eventID] == kAEReopenApplication
290 && (untitledWindowFlag & MMUntitledWindowOnReopen) == 0)
293 // When a process is started from the command line, the 'Open' event will
294 // contain a parameter to surpress the opening of an untitled window.
295 desc = [desc paramDescriptorForKeyword:keyAEPropData];
296 desc = [desc paramDescriptorForKeyword:keyMMUntitledWindow];
297 if (desc && ![desc booleanValue])
300 // Never open an untitled window if there is at least one open window or if
301 // there are processes that are currently launching.
302 if ([vimControllers count] > 0 || [pidArguments count] > 0)
305 // NOTE! This way it possible to start the app with the command-line
306 // argument '-nowindow yes' and no window will be opened by default.
307 return ![ud boolForKey:MMNoWindowKey];
310 - (BOOL)applicationOpenUntitledFile:(NSApplication *)sender
312 [self newWindow:self];
316 - (void)application:(NSApplication *)sender openFiles:(NSArray *)filenames
318 // Opening files works like this:
319 // a) extract ODB/Xcode/Spotlight parameters from the current Apple event
320 // b) filter out any already open files (see filterOpenFiles::)
321 // c) open any remaining files
323 // A file is opened in an untitled window if there is one (it may be
324 // currently launching, or it may already be visible), otherwise a new
327 // Each launching Vim process has a dictionary of arguments that are passed
328 // to the process when in checks in (via connectBackend:pid:). The
329 // arguments for each launching process can be looked up by its PID (in the
330 // pidArguments dictionary).
332 NSMutableDictionary *arguments = [self extractArgumentsFromOdocEvent:
333 [[NSAppleEventManager sharedAppleEventManager] currentAppleEvent]];
335 // Filter out files that are already open
336 filenames = [self filterOpenFiles:filenames arguments:arguments];
338 // Open any files that remain
339 if ([filenames count]) {
341 BOOL openInTabs = [[NSUserDefaults standardUserDefaults]
342 boolForKey:MMOpenFilesInTabsKey];
344 [arguments setObject:filenames forKey:@"filenames"];
345 [arguments setObject:[NSNumber numberWithBool:YES] forKey:@"openFiles"];
347 // Add file names to "Recent Files" menu.
348 int i, count = [filenames count];
349 for (i = 0; i < count; ++i) {
350 // Don't add files that are being edited remotely (using ODB).
351 if ([arguments objectForKey:@"remoteID"]) continue;
353 [[NSDocumentController sharedDocumentController]
354 noteNewRecentFilePath:[filenames objectAtIndex:i]];
357 if ((openInTabs && (vc = [self topmostVimController]))
358 || (vc = [self findUntitledWindow])) {
359 // Open files in an already open window.
360 [[[vc windowController] window] makeKeyAndOrderFront:self];
361 [self passArguments:arguments toVimController:vc];
363 // Open files in a launching Vim process or start a new process.
364 int pid = [self findLaunchingProcessWithoutArguments];
366 // Pass the filenames to the process straight away.
368 // TODO: It would be nicer if all arguments were passed to the
369 // Vim process in connectBackend::, but if we don't pass the
370 // filename arguments here, the window 'flashes' once when it
371 // opens. This is due to the 'welcome' screen first being
372 // displayed, then quickly thereafter the files are opened.
373 NSArray *fileArgs = [NSArray arrayWithObject:@"-p"];
374 fileArgs = [fileArgs arrayByAddingObjectsFromArray:filenames];
376 pid = [self launchVimProcessWithArguments:fileArgs];
379 // TODO: Notify user of failure?
380 [NSApp replyToOpenOrPrint:
381 NSApplicationDelegateReplyFailure];
385 // Make sure these files aren't opened again when
386 // connectBackend:pid: is called.
387 [arguments setObject:[NSNumber numberWithBool:NO]
388 forKey:@"openFiles"];
391 // TODO: If the Vim process fails to start, or if it changes PID,
392 // then the memory allocated for these parameters will leak.
393 // Ensure that this cannot happen or somehow detect it.
395 if ([arguments count] > 0)
396 [pidArguments setObject:arguments
397 forKey:[NSNumber numberWithInt:pid]];
401 [NSApp replyToOpenOrPrint:NSApplicationDelegateReplySuccess];
402 // NSApplicationDelegateReplySuccess = 0,
403 // NSApplicationDelegateReplyCancel = 1,
404 // NSApplicationDelegateReplyFailure = 2
407 - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender
409 return [[NSUserDefaults standardUserDefaults]
410 boolForKey:MMTerminateAfterLastWindowClosedKey];
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 // Give Vim processes a chance to terminate before MacVim. If they
522 // haven't terminated by the time applicationWillTerminate: is sent,
523 // they may be forced to quit (see below).
524 usleep(MMTerminationSleepPeriod);
530 - (void)applicationWillTerminate:(NSNotification *)notification
532 #ifdef MM_ENABLE_PLUGINS
533 [[MMPlugInManager sharedManager] unloadAllPlugIns];
536 #if MM_HANDLE_XCODE_MOD_EVENT
537 [[NSAppleEventManager sharedAppleEventManager]
538 removeEventHandlerForEventClass:'KAHL'
542 // This will invalidate all connections (since they were spawned from this
544 [connection invalidate];
546 // Send a SIGINT to all running Vim processes, so that they are sure to
547 // receive the connectionDidDie: notification (a process has to be checking
548 // the run-loop for this to happen).
549 unsigned i, count = [vimControllers count];
550 for (i = 0; i < count; ++i) {
551 MMVimController *controller = [vimControllers objectAtIndex:i];
552 int pid = [controller pid];
557 if (fontContainerRef) {
558 ATSFontDeactivate(fontContainerRef, NULL, kATSOptionFlagsDefault);
559 fontContainerRef = 0;
562 [NSApp setDelegate:nil];
565 + (MMAppController *)sharedInstance
567 // Note: The app controller is a singleton which is instantiated in
568 // MainMenu.nib where it is also connected as the delegate of NSApp.
569 id delegate = [NSApp delegate];
570 return [delegate isKindOfClass:self] ? (MMAppController*)delegate : nil;
573 - (NSMenu *)defaultMainMenu
575 return defaultMainMenu;
578 - (NSMenuItem *)appMenuItemTemplate
580 return appMenuItemTemplate;
583 - (void)removeVimController:(id)controller
585 //NSLog(@"%s%@", _cmd, controller);
587 [controller cleanup];
588 [[controller windowController] close];
590 [vimControllers removeObject:controller];
592 if (![vimControllers count]) {
593 // The last editor window just closed so restore the main menu back to
594 // its default state (which is defined in MainMenu.nib).
595 [self setMainMenu:defaultMainMenu];
599 - (void)windowControllerWillOpen:(MMWindowController *)windowController
601 NSPoint topLeft = NSZeroPoint;
602 NSWindow *topWin = [[[self topmostVimController] windowController] window];
603 NSWindow *win = [windowController window];
607 // If there is a window belonging to a Vim process, cascade from it,
608 // otherwise use the autosaved window position (if any).
610 NSRect frame = [topWin frame];
611 topLeft = NSMakePoint(frame.origin.x, NSMaxY(frame));
613 NSString *topLeftString = [[NSUserDefaults standardUserDefaults]
614 stringForKey:MMTopLeftPointKey];
616 topLeft = NSPointFromString(topLeftString);
619 if (!NSEqualPoints(topLeft, NSZeroPoint)) {
621 topLeft = [win cascadeTopLeftFromPoint:topLeft];
623 [win setFrameTopLeftPoint:topLeft];
626 if (openSelectionString) {
627 // TODO: Pass this as a parameter instead! Get rid of
628 // 'openSelectionString' etc.
630 // There is some text to paste into this window as a result of the
631 // services menu "Open selection ..." being used.
632 [[windowController vimController] dropString:openSelectionString];
633 [openSelectionString release];
634 openSelectionString = nil;
638 - (void)setMainMenu:(NSMenu *)mainMenu
640 if ([NSApp mainMenu] == mainMenu) return;
642 // If the new menu has a "Recent Files" dummy item, then swap the real item
643 // for the dummy. We are forced to do this since Cocoa initializes the
644 // "Recent Files" menu and there is no way to simply point Cocoa to a new
645 // item each time the menus are swapped.
646 NSMenu *fileMenu = [mainMenu findFileMenu];
647 if (recentFilesMenuItem && fileMenu) {
649 [fileMenu indexOfItemWithAction:@selector(recentFilesDummy:)];
651 NSMenuItem *dummyItem = [[fileMenu itemAtIndex:dummyIdx] retain];
652 [fileMenu removeItemAtIndex:dummyIdx];
654 NSMenu *recentFilesParentMenu = [recentFilesMenuItem menu];
655 int idx = [recentFilesParentMenu indexOfItem:recentFilesMenuItem];
657 [[recentFilesMenuItem retain] autorelease];
658 [recentFilesParentMenu removeItemAtIndex:idx];
659 [recentFilesParentMenu insertItem:dummyItem atIndex:idx];
662 [fileMenu insertItem:recentFilesMenuItem atIndex:dummyIdx];
667 // Now set the new menu. Notice that we keep one menu for each editor
668 // window since each editor can have its own set of menus. When swapping
669 // menus we have to tell Cocoa where the new "MacVim", "Windows", and
670 // "Services" menu are.
671 [NSApp setMainMenu:mainMenu];
673 // Setting the "MacVim" (or "Application") menu ensures that it is typeset
674 // in boldface. (The setAppleMenu: method used to be public but is now
675 // private so this will have to be considered a bit of a hack!)
676 NSMenu *appMenu = [mainMenu findApplicationMenu];
677 [NSApp performSelector:@selector(setAppleMenu:) withObject:appMenu];
679 NSMenu *servicesMenu = [mainMenu findServicesMenu];
680 [NSApp setServicesMenu:servicesMenu];
682 NSMenu *windowsMenu = [mainMenu findWindowsMenu];
684 // Cocoa isn't clever enough to get rid of items it has added to the
685 // "Windows" menu so we have to do it ourselves otherwise there will be
686 // multiple menu items for each window in the "Windows" menu.
687 // This code assumes that the only items Cocoa add are ones which
688 // send off the action makeKeyAndOrderFront:. (Cocoa will not add
689 // another separator item if the last item on the "Windows" menu
690 // already is a separator, so we needen't worry about separators.)
691 int i, count = [windowsMenu numberOfItems];
692 for (i = count-1; i >= 0; --i) {
693 NSMenuItem *item = [windowsMenu itemAtIndex:i];
694 if ([item action] == @selector(makeKeyAndOrderFront:))
695 [windowsMenu removeItem:item];
698 [NSApp setWindowsMenu:windowsMenu];
700 #ifdef MM_ENABLE_PLUGINS
701 // Move plugin menu from old to new main menu.
702 [self removePlugInMenu];
703 [self addPlugInMenuToMenu:mainMenu];
707 #ifdef MM_ENABLE_PLUGINS
708 - (void)addItemToPlugInMenu:(NSMenuItem *)item
710 NSMenu *menu = [plugInMenuItem submenu];
712 if ([menu numberOfItems] == 1)
713 [self addPlugInMenuToMenu:[NSApp mainMenu]];
716 - (void)removeItemFromPlugInMenu:(NSMenuItem *)item
718 NSMenu *menu = [plugInMenuItem submenu];
719 [menu removeItem:item];
720 if ([menu numberOfItems] == 0)
721 [self removePlugInMenu];
725 - (IBAction)newWindow:(id)sender
727 [self launchVimProcessWithArguments:nil];
730 - (IBAction)fileOpen:(id)sender
733 BOOL trackPwd = [[NSUserDefaults standardUserDefaults]
734 boolForKey:MMDialogsTrackPwdKey];
736 MMVimController *vc = [self keyVimController];
737 if (vc) dir = [[vc vimState] objectForKey:@"pwd"];
740 NSOpenPanel *panel = [NSOpenPanel openPanel];
741 [panel setAllowsMultipleSelection:YES];
742 [panel setAccessoryView:openPanelAccessoryView()];
744 int result = [panel runModalForDirectory:dir file:nil types:nil];
745 if (NSOKButton == result)
746 [self application:NSApp openFiles:[panel filenames]];
749 - (IBAction)selectNextWindow:(id)sender
751 unsigned i, count = [vimControllers count];
754 NSWindow *keyWindow = [NSApp keyWindow];
755 for (i = 0; i < count; ++i) {
756 MMVimController *vc = [vimControllers objectAtIndex:i];
757 if ([[[vc windowController] window] isEqual:keyWindow])
764 MMVimController *vc = [vimControllers objectAtIndex:i];
765 [[vc windowController] showWindow:self];
769 - (IBAction)selectPreviousWindow:(id)sender
771 unsigned i, count = [vimControllers count];
774 NSWindow *keyWindow = [NSApp keyWindow];
775 for (i = 0; i < count; ++i) {
776 MMVimController *vc = [vimControllers objectAtIndex:i];
777 if ([[[vc windowController] window] isEqual:keyWindow])
787 MMVimController *vc = [vimControllers objectAtIndex:i];
788 [[vc windowController] showWindow:self];
792 - (IBAction)orderFrontPreferencePanel:(id)sender
794 [[MMPreferenceController sharedPrefsWindowController] showWindow:self];
797 - (IBAction)openWebsite:(id)sender
799 [[NSWorkspace sharedWorkspace] openURL:
800 [NSURL URLWithString:MMWebsiteString]];
803 - (IBAction)showVimHelp:(id)sender
805 // Open a new window with the help window maximized.
806 [self launchVimProcessWithArguments:[NSArray arrayWithObjects:
807 @"-c", @":h gui_mac", @"-c", @":res", nil]];
810 - (IBAction)zoomAll:(id)sender
812 [NSApp makeWindowsPerform:@selector(performZoom:) inOrder:YES];
815 - (byref id <MMFrontendProtocol>)
816 connectBackend:(byref in id <MMBackendProtocol>)backend
819 //NSLog(@"Connect backend (pid=%d)", pid);
820 NSNumber *pidKey = [NSNumber numberWithInt:pid];
821 MMVimController *vc = nil;
824 [(NSDistantObject*)backend
825 setProtocolForProxy:@protocol(MMBackendProtocol)];
827 vc = [[[MMVimController alloc]
828 initWithBackend:backend pid:pid]
831 if (![vimControllers count]) {
832 // The first window autosaves its position. (The autosaving
833 // features of Cocoa are not used because we need more control over
834 // what is autosaved and when it is restored.)
835 [[vc windowController] setWindowAutosaveKey:MMTopLeftPointKey];
838 [vimControllers addObject:vc];
840 id args = [pidArguments objectForKey:pidKey];
841 if (args && [NSNull null] != args)
842 [self passArguments:args toVimController:vc];
844 // HACK! MacVim does not get activated if it is launched from the
845 // terminal, so we forcibly activate here unless it is an untitled
846 // window opening. Untitled windows are treated differently, else
847 // MacVim would steal the focus if another app was activated while the
848 // untitled window was loading.
849 if (!args || args != [NSNull null])
850 [NSApp activateIgnoringOtherApps:YES];
853 [pidArguments removeObjectForKey:pidKey];
858 @catch (NSException *e) {
859 NSLog(@"Exception caught in %s: \"%@\"", _cmd, e);
862 [vimControllers removeObject:vc];
864 [pidArguments removeObjectForKey:pidKey];
870 - (NSArray *)serverList
872 NSMutableArray *array = [NSMutableArray array];
874 unsigned i, count = [vimControllers count];
875 for (i = 0; i < count; ++i) {
876 MMVimController *controller = [vimControllers objectAtIndex:i];
877 if ([controller serverName])
878 [array addObject:[controller serverName]];
884 - (MMVimController *)keyVimController
886 NSWindow *keyWindow = [NSApp keyWindow];
888 unsigned i, count = [vimControllers count];
889 for (i = 0; i < count; ++i) {
890 MMVimController *vc = [vimControllers objectAtIndex:i];
891 if ([[[vc windowController] window] isEqual:keyWindow])
899 @end // MMAppController
904 @implementation MMAppController (MMServices)
906 - (void)openSelection:(NSPasteboard *)pboard userData:(NSString *)userData
907 error:(NSString **)error
909 if (![[pboard types] containsObject:NSStringPboardType]) {
910 NSLog(@"WARNING: Pasteboard contains no object of type "
911 "NSStringPboardType");
915 MMVimController *vc = [self topmostVimController];
917 // Open a new tab first, since dropString: does not do this.
918 [vc sendMessage:AddNewTabMsgID data:nil];
919 [vc dropString:[pboard stringForType:NSStringPboardType]];
921 // NOTE: There is no window to paste the selection into, so save the
922 // text, open a new window, and paste the text when the next window
923 // opens. (If this is called several times in a row, then all but the
924 // last call might be ignored.)
925 if (openSelectionString) [openSelectionString release];
926 openSelectionString = [[pboard stringForType:NSStringPboardType] copy];
928 [self newWindow:self];
932 - (void)openFile:(NSPasteboard *)pboard userData:(NSString *)userData
933 error:(NSString **)error
935 if (![[pboard types] containsObject:NSStringPboardType]) {
936 NSLog(@"WARNING: Pasteboard contains no object of type "
937 "NSStringPboardType");
941 // TODO: Parse multiple filenames and create array with names.
942 NSString *string = [pboard stringForType:NSStringPboardType];
943 string = [string stringByTrimmingCharactersInSet:
944 [NSCharacterSet whitespaceAndNewlineCharacterSet]];
945 string = [string stringByStandardizingPath];
947 NSArray *filenames = [self filterFilesAndNotify:
948 [NSArray arrayWithObject:string]];
949 if ([filenames count] > 0) {
950 MMVimController *vc = nil;
951 if (userData && [userData isEqual:@"Tab"])
952 vc = [self topmostVimController];
955 [vc dropFiles:filenames forceOpen:YES];
957 [self application:NSApp openFiles:filenames];
962 @end // MMAppController (MMServices)
967 @implementation MMAppController (Private)
969 - (MMVimController *)topmostVimController
971 // Find the topmost visible window which has an associated vim controller.
972 NSEnumerator *e = [[NSApp orderedWindows] objectEnumerator];
974 while ((window = [e nextObject]) && [window isVisible]) {
975 unsigned i, count = [vimControllers count];
976 for (i = 0; i < count; ++i) {
977 MMVimController *vc = [vimControllers objectAtIndex:i];
978 if ([[[vc windowController] window] isEqual:window])
986 - (int)launchVimProcessWithArguments:(NSArray *)args
989 NSString *path = [[NSBundle mainBundle] pathForAuxiliaryExecutable:@"Vim"];
992 NSLog(@"ERROR: Vim executable could not be found inside app bundle!");
996 NSArray *taskArgs = [NSArray arrayWithObjects:@"-g", @"-f", nil];
998 taskArgs = [taskArgs arrayByAddingObjectsFromArray:args];
1000 BOOL useLoginShell = [[NSUserDefaults standardUserDefaults]
1001 boolForKey:MMLoginShellKey];
1002 if (useLoginShell) {
1003 // Run process with a login shell, roughly:
1004 // echo "exec Vim -g -f args" | ARGV0=-`basename $SHELL` $SHELL [-l]
1005 pid = executeInLoginShell(path, taskArgs);
1007 // Run process directly:
1009 NSTask *task = [NSTask launchedTaskWithLaunchPath:path
1010 arguments:taskArgs];
1011 pid = task ? [task processIdentifier] : -1;
1015 // NOTE: If the process has no arguments, then add a null argument to
1016 // the pidArguments dictionary. This is later used to detect that a
1017 // process without arguments is being launched.
1019 [pidArguments setObject:[NSNull null]
1020 forKey:[NSNumber numberWithInt:pid]];
1022 NSLog(@"WARNING: %s%@ failed (useLoginShell=%d)", _cmd, args,
1029 - (NSArray *)filterFilesAndNotify:(NSArray *)filenames
1031 // Go trough 'filenames' array and make sure each file exists. Present
1032 // warning dialog if some file was missing.
1034 NSString *firstMissingFile = nil;
1035 NSMutableArray *files = [NSMutableArray array];
1036 unsigned i, count = [filenames count];
1038 for (i = 0; i < count; ++i) {
1039 NSString *name = [filenames objectAtIndex:i];
1040 if ([[NSFileManager defaultManager] fileExistsAtPath:name]) {
1041 [files addObject:name];
1042 } else if (!firstMissingFile) {
1043 firstMissingFile = name;
1047 if (firstMissingFile) {
1048 NSAlert *alert = [[NSAlert alloc] init];
1049 [alert addButtonWithTitle:NSLocalizedString(@"OK",
1053 if ([files count] >= count-1) {
1054 [alert setMessageText:NSLocalizedString(@"File not found",
1055 @"File not found dialog, title")];
1056 text = [NSString stringWithFormat:NSLocalizedString(
1057 @"Could not open file with name %@.",
1058 @"File not found dialog, text"), firstMissingFile];
1060 [alert setMessageText:NSLocalizedString(@"Multiple files not found",
1061 @"File not found dialog, title")];
1062 text = [NSString stringWithFormat:NSLocalizedString(
1063 @"Could not open file with name %@, and %d other files.",
1064 @"File not found dialog, text"),
1065 firstMissingFile, count-[files count]-1];
1068 [alert setInformativeText:text];
1069 [alert setAlertStyle:NSWarningAlertStyle];
1074 [NSApp replyToOpenOrPrint:NSApplicationDelegateReplyFailure];
1080 - (NSArray *)filterOpenFiles:(NSArray *)filenames
1081 arguments:(NSDictionary *)args
1083 // Check if any of the files in the 'filenames' array are open in any Vim
1084 // process. Remove the files that are open from the 'filenames' array and
1085 // return it. If all files were filtered out, then raise the first file in
1086 // the Vim process it is open. Files that are filtered are sent an odb
1087 // open event in case theID is not zero.
1089 NSMutableDictionary *localArgs =
1090 [NSMutableDictionary dictionaryWithDictionary:args];
1091 MMVimController *raiseController = nil;
1092 NSString *raiseFile = nil;
1093 NSMutableArray *files = [filenames mutableCopy];
1094 NSString *expr = [NSString stringWithFormat:
1095 @"map([\"%@\"],\"bufloaded(v:val)\")",
1096 [files componentsJoinedByString:@"\",\""]];
1097 unsigned i, count = [vimControllers count];
1099 // Ensure that the files aren't opened when passing arguments.
1100 [localArgs setObject:[NSNumber numberWithBool:NO] forKey:@"openFiles"];
1102 for (i = 0; i < count && [files count]; ++i) {
1103 MMVimController *controller = [vimControllers objectAtIndex:i];
1105 // Query Vim for which files in the 'files' array are open.
1106 NSString *eval = [controller evaluateVimExpression:expr];
1107 if (!eval) continue;
1109 NSIndexSet *idxSet = [NSIndexSet indexSetWithVimList:eval];
1110 if ([idxSet count]) {
1112 // Remember the file and which Vim that has it open so that
1113 // we can raise it later on.
1114 raiseController = controller;
1115 raiseFile = [files objectAtIndex:[idxSet firstIndex]];
1116 [[raiseFile retain] autorelease];
1119 // Pass (ODB/Xcode/Spotlight) arguments to this process.
1120 [localArgs setObject:[files objectsAtIndexes:idxSet]
1121 forKey:@"filenames"];
1122 [self passArguments:localArgs toVimController:controller];
1124 // Remove all the files that were open in this Vim process and
1125 // create a new expression to evaluate.
1126 [files removeObjectsAtIndexes:idxSet];
1127 expr = [NSString stringWithFormat:
1128 @"map([\"%@\"],\"bufloaded(v:val)\")",
1129 [files componentsJoinedByString:@"\",\""]];
1133 if (![files count] && raiseFile) {
1134 // Raise the window containing the first file that was already open,
1135 // and make sure that the tab containing that file is selected. Only
1136 // do this if there are no more files to open, otherwise sometimes the
1137 // window with 'raiseFile' will be raised, other times it might be the
1138 // window that will open with the files in the 'files' array.
1139 raiseFile = [raiseFile stringByEscapingSpecialFilenameCharacters];
1140 NSString *input = [NSString stringWithFormat:@"<C-\\><C-N>"
1141 ":let oldswb=&swb|let &swb=\"useopen,usetab\"|"
1142 "tab sb %@|let &swb=oldswb|unl oldswb|"
1143 "cal foreground()|redr|f<CR>", raiseFile];
1145 [raiseController addVimInput:input];
1151 #if MM_HANDLE_XCODE_MOD_EVENT
1152 - (void)handleXcodeModEvent:(NSAppleEventDescriptor *)event
1153 replyEvent:(NSAppleEventDescriptor *)reply
1156 // Xcode sends this event to query MacVim which open files have been
1158 NSLog(@"reply:%@", reply);
1159 NSLog(@"event:%@", event);
1161 NSEnumerator *e = [vimControllers objectEnumerator];
1163 while ((vc = [e nextObject])) {
1164 DescType type = [reply descriptorType];
1165 unsigned len = [[type data] length];
1166 NSMutableData *data = [NSMutableData data];
1168 [data appendBytes:&type length:sizeof(DescType)];
1169 [data appendBytes:&len length:sizeof(unsigned)];
1170 [data appendBytes:[reply data] length:len];
1172 [vc sendMessage:XcodeModMsgID data:data];
1178 - (int)findLaunchingProcessWithoutArguments
1180 NSArray *keys = [pidArguments allKeysForObject:[NSNull null]];
1181 if ([keys count] > 0) {
1182 //NSLog(@"found launching process without arguments");
1183 return [[keys objectAtIndex:0] intValue];
1189 - (MMVimController *)findUntitledWindow
1191 NSEnumerator *e = [vimControllers objectEnumerator];
1193 while ((vc = [e nextObject])) {
1194 // TODO: This is a moronic test...should query the Vim process if there
1195 // are any open buffers or something like that instead.
1196 NSString *title = [[[vc windowController] window] title];
1198 // TODO: this will not work in a localized MacVim
1199 if ([title hasPrefix:@"[No Name] - VIM"]) {
1200 //NSLog(@"found untitled window");
1208 - (NSMutableDictionary *)extractArgumentsFromOdocEvent:
1209 (NSAppleEventDescriptor *)desc
1211 NSMutableDictionary *dict = [NSMutableDictionary dictionary];
1213 // 1. Extract ODB parameters (if any)
1214 NSAppleEventDescriptor *odbdesc = desc;
1215 if (![odbdesc paramDescriptorForKeyword:keyFileSender]) {
1216 // The ODB paramaters may hide inside the 'keyAEPropData' descriptor.
1217 odbdesc = [odbdesc paramDescriptorForKeyword:keyAEPropData];
1218 if (![odbdesc paramDescriptorForKeyword:keyFileSender])
1223 NSAppleEventDescriptor *p =
1224 [odbdesc paramDescriptorForKeyword:keyFileSender];
1226 [dict setObject:[NSNumber numberWithUnsignedInt:[p typeCodeValue]]
1227 forKey:@"remoteID"];
1229 p = [odbdesc paramDescriptorForKeyword:keyFileCustomPath];
1231 [dict setObject:[p stringValue] forKey:@"remotePath"];
1233 p = [odbdesc paramDescriptorForKeyword:keyFileSenderToken];
1235 [dict setObject:p forKey:@"remotePath"];
1238 // 2. Extract Xcode parameters (if any)
1239 NSAppleEventDescriptor *xcodedesc =
1240 [desc paramDescriptorForKeyword:keyAEPosition];
1243 MMSelectionRange *sr = (MMSelectionRange*)[[xcodedesc data] bytes];
1245 if (sr->lineNum < 0) {
1246 // Should select a range of lines.
1247 range.location = sr->startRange + 1;
1248 range.length = sr->endRange - sr->startRange + 1;
1250 // Should only move cursor to a line.
1251 range.location = sr->lineNum + 1;
1255 [dict setObject:NSStringFromRange(range) forKey:@"selectionRange"];
1258 // 3. Extract Spotlight search text (if any)
1259 NSAppleEventDescriptor *spotlightdesc =
1260 [desc paramDescriptorForKeyword:keyAESearchText];
1262 [dict setObject:[spotlightdesc stringValue] forKey:@"searchText"];
1267 - (void)passArguments:(NSDictionary *)args toVimController:(MMVimController*)vc
1271 // Pass filenames to open if required (the 'openFiles' argument can be used
1272 // to disallow opening of the files).
1273 NSArray *filenames = [args objectForKey:@"filenames"];
1274 if (filenames && [[args objectForKey:@"openFiles"] boolValue]) {
1275 NSString *tabDrop = buildTabDropCommand(filenames);
1276 [vc addVimInput:tabDrop];
1280 if (filenames && [args objectForKey:@"remoteID"]) {
1281 [vc odbEdit:filenames
1282 server:[[args objectForKey:@"remoteID"] unsignedIntValue]
1283 path:[args objectForKey:@"remotePath"]
1284 token:[args objectForKey:@"remoteToken"]];
1287 // Pass range of lines to select
1288 if ([args objectForKey:@"selectionRange"]) {
1289 NSRange selectionRange = NSRangeFromString(
1290 [args objectForKey:@"selectionRange"]);
1291 [vc addVimInput:buildSelectRangeCommand(selectionRange)];
1295 NSString *searchText = [args objectForKey:@"searchText"];
1297 [vc addVimInput:buildSearchTextCommand(searchText)];
1300 #ifdef MM_ENABLE_PLUGINS
1301 - (void)removePlugInMenu
1303 if ([plugInMenuItem menu])
1304 [[plugInMenuItem menu] removeItem:plugInMenuItem];
1307 - (void)addPlugInMenuToMenu:(NSMenu *)mainMenu
1309 NSMenu *windowsMenu = [mainMenu findWindowsMenu];
1311 if ([[plugInMenuItem submenu] numberOfItems] > 0) {
1312 int idx = windowsMenu ? [mainMenu indexOfItemWithSubmenu:windowsMenu]
1315 [mainMenu insertItem:plugInMenuItem atIndex:idx];
1317 [mainMenu addItem:plugInMenuItem];
1323 @end // MMAppController (Private)
1329 executeInLoginShell(NSString *path, NSArray *args)
1331 // Start a login shell and execute the command 'path' with arguments 'args'
1332 // in the shell. This ensures that user environment variables are set even
1333 // when MacVim was started from the Finder.
1336 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
1338 // Determine which shell to use to execute the command. The user
1339 // may decide which shell to use by setting a user default or the
1340 // $SHELL environment variable.
1341 NSString *shell = [ud stringForKey:MMLoginShellCommandKey];
1342 if (!shell || [shell length] == 0)
1343 shell = [[[NSProcessInfo processInfo] environment]
1344 objectForKey:@"SHELL"];
1346 shell = @"/bin/bash";
1348 //NSLog(@"shell = %@", shell);
1350 // Bash needs the '-l' flag to launch a login shell. The user may add
1351 // flags by setting a user default.
1352 NSString *shellArgument = [ud stringForKey:MMLoginShellArgumentKey];
1353 if (!shellArgument || [shellArgument length] == 0) {
1354 if ([[shell lastPathComponent] isEqual:@"bash"])
1355 shellArgument = @"-l";
1357 shellArgument = nil;
1360 //NSLog(@"shellArgument = %@", shellArgument);
1362 // Build input string to pipe to the login shell.
1363 NSMutableString *input = [NSMutableString stringWithFormat:
1364 @"exec \"%@\"", path];
1366 // Append all arguments, making sure they are properly quoted, even
1367 // when they contain single quotes.
1368 NSEnumerator *e = [args objectEnumerator];
1371 while ((obj = [e nextObject])) {
1372 NSMutableString *arg = [NSMutableString stringWithString:obj];
1373 [arg replaceOccurrencesOfString:@"'" withString:@"'\"'\"'"
1374 options:NSLiteralSearch
1375 range:NSMakeRange(0, [arg length])];
1376 [input appendFormat:@" '%@'", arg];
1380 // Build the argument vector used to start the login shell.
1381 NSString *shellArg0 = [NSString stringWithFormat:@"-%@",
1382 [shell lastPathComponent]];
1383 char *shellArgv[3] = { (char *)[shellArg0 UTF8String], NULL, NULL };
1385 shellArgv[1] = (char *)[shellArgument UTF8String];
1387 // Get the C string representation of the shell path before the fork since
1388 // we must not call Foundation functions after a fork.
1389 const char *shellPath = [shell fileSystemRepresentation];
1391 // Fork and execute the process.
1393 if (pipe(ds)) return -1;
1398 } else if (pid == 0) {
1400 if (close(ds[1]) == -1) exit(255);
1401 if (dup2(ds[0], 0) == -1) exit(255);
1403 execv(shellPath, shellArgv);
1405 // Never reached unless execv fails
1409 if (close(ds[0]) == -1) return -1;
1411 // Send input to execute to the child process
1412 [input appendString:@"\n"];
1413 int bytes = [input lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
1415 if (write(ds[1], [input UTF8String], bytes) != bytes) return -1;
1416 if (close(ds[1]) == -1) return -1;