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 "MMVimController.h"
42 #import "MMWindowController.h"
43 #import "MMPreferenceController.h"
47 #define MM_HANDLE_XCODE_MOD_EVENT 0
51 // Default timeout intervals on all connections.
52 static NSTimeInterval MMRequestTimeout = 5;
53 static NSTimeInterval MMReplyTimeout = 5;
55 static NSString *MMWebsiteString = @"http://code.google.com/p/macvim/";
58 #pragma options align=mac68k
61 short unused1; // 0 (not used)
62 short lineNum; // line to select (< 0 to specify range)
63 long startRange; // start of selection range (if line < 0)
64 long endRange; // end of selection range (if line < 0)
65 long unused2; // 0 (not used)
66 long theDate; // modification date/time
68 #pragma options align=reset
71 static int executeInLoginShell(NSString *path, NSArray *args);
74 @interface MMAppController (MMServices)
75 - (void)openSelection:(NSPasteboard *)pboard userData:(NSString *)userData
76 error:(NSString **)error;
77 - (void)openFile:(NSPasteboard *)pboard userData:(NSString *)userData
78 error:(NSString **)error;
82 @interface MMAppController (Private)
83 - (MMVimController *)keyVimController;
84 - (MMVimController *)topmostVimController;
85 - (int)launchVimProcessWithArguments:(NSArray *)args;
86 - (NSArray *)filterFilesAndNotify:(NSArray *)files;
87 - (NSArray *)filterOpenFiles:(NSArray *)filenames
88 arguments:(NSDictionary *)args;
89 #if MM_HANDLE_XCODE_MOD_EVENT
90 - (void)handleXcodeModEvent:(NSAppleEventDescriptor *)event
91 replyEvent:(NSAppleEventDescriptor *)reply;
93 - (int)findLaunchingProcessWithoutArguments;
94 - (MMVimController *)findUntitledWindow;
95 - (NSMutableDictionary *)extractArgumentsFromOdocEvent:
96 (NSAppleEventDescriptor *)desc;
97 - (void)passArguments:(NSDictionary *)args toVimController:(MMVimController*)vc;
101 @interface NSNumber (MMExtras)
106 @interface NSMenu (MMExtras)
107 - (int)indexOfItemWithAction:(SEL)action;
108 - (NSMenuItem *)itemWithAction:(SEL)action;
109 - (NSMenu *)findMenuContainingItemWithAction:(SEL)action;
110 - (NSMenu *)findWindowsMenu;
111 - (NSMenu *)findApplicationMenu;
112 - (NSMenu *)findServicesMenu;
113 - (NSMenu *)findFileMenu;
119 @implementation MMAppController
123 NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:
124 [NSNumber numberWithBool:NO], MMNoWindowKey,
125 [NSNumber numberWithInt:64], MMTabMinWidthKey,
126 [NSNumber numberWithInt:6*64], MMTabMaxWidthKey,
127 [NSNumber numberWithInt:132], MMTabOptimumWidthKey,
128 [NSNumber numberWithInt:2], MMTextInsetLeftKey,
129 [NSNumber numberWithInt:1], MMTextInsetRightKey,
130 [NSNumber numberWithInt:1], MMTextInsetTopKey,
131 [NSNumber numberWithInt:1], MMTextInsetBottomKey,
132 [NSNumber numberWithBool:NO], MMTerminateAfterLastWindowClosedKey,
133 @"MMTypesetter", MMTypesetterKey,
134 [NSNumber numberWithFloat:1], MMCellWidthMultiplierKey,
135 [NSNumber numberWithFloat:-1], MMBaselineOffsetKey,
136 [NSNumber numberWithBool:YES], MMTranslateCtrlClickKey,
137 [NSNumber numberWithBool:NO], MMOpenFilesInTabsKey,
138 [NSNumber numberWithBool:NO], MMNoFontSubstitutionKey,
139 [NSNumber numberWithBool:NO], MMLoginShellKey,
140 [NSNumber numberWithBool:NO], MMAtsuiRendererKey,
141 [NSNumber numberWithInt:MMUntitledWindowAlways],
143 [NSNumber numberWithBool:NO], MMTexturedWindowKey,
144 [NSNumber numberWithBool:NO], MMZoomBothKey,
145 @"", MMLoginShellCommandKey,
146 @"", MMLoginShellArgumentKey,
147 [NSNumber numberWithBool:YES], MMDialogsTrackPwdKey,
150 [[NSUserDefaults standardUserDefaults] registerDefaults:dict];
152 NSArray *types = [NSArray arrayWithObject:NSStringPboardType];
153 [NSApp registerServicesMenuSendTypes:types returnTypes:types];
158 if ((self = [super init])) {
159 fontContainerRef = loadFonts();
161 vimControllers = [NSMutableArray new];
162 pidArguments = [NSMutableDictionary new];
164 // NOTE: Do not use the default connection since the Logitech Control
165 // Center (LCC) input manager steals and this would cause MacVim to
166 // never open any windows. (This is a bug in LCC but since they are
167 // unlikely to fix it, we graciously give them the default connection.)
168 connection = [[NSConnection alloc] initWithReceivePort:[NSPort port]
170 [connection setRootObject:self];
171 [connection setRequestTimeout:MMRequestTimeout];
172 [connection setReplyTimeout:MMReplyTimeout];
174 // NOTE: When the user is resizing the window the AppKit puts the run
175 // loop in event tracking mode. Unless the connection listens to
176 // request in this mode, live resizing won't work.
177 [connection addRequestMode:NSEventTrackingRunLoopMode];
179 // NOTE! If the name of the connection changes here it must also be
180 // updated in MMBackend.m.
181 NSString *name = [NSString stringWithFormat:@"%@-connection",
182 [[NSBundle mainBundle] bundleIdentifier]];
183 //NSLog(@"Registering connection with name '%@'", name);
184 if (![connection registerName:name]) {
185 NSLog(@"FATAL ERROR: Failed to register connection with name '%@'",
187 [connection release]; connection = nil;
196 //NSLog(@"MMAppController dealloc");
198 [connection release]; connection = nil;
199 [pidArguments release]; pidArguments = nil;
200 [vimControllers release]; vimControllers = nil;
201 [openSelectionString release]; openSelectionString = nil;
202 [recentFilesMenuItem release]; recentFilesMenuItem = nil;
203 [defaultMainMenu release]; defaultMainMenu = nil;
208 - (void)applicationWillFinishLaunching:(NSNotification *)notification
210 // Remember the default menu so that it can be restored if the user closes
211 // all editor windows.
212 defaultMainMenu = [[NSApp mainMenu] retain];
214 // Set up the "Open Recent" menu. See
215 // http://lapcatsoftware.com/blog/2007/07/10/
216 // working-without-a-nib-part-5-open-recent-menu/
218 // http://www.cocoabuilder.com/archive/message/cocoa/2007/8/15/187793
219 // for more information.
221 // The menu itself is created in MainMenu.nib but we still seem to have to
222 // hack around a bit to get it to work. (This has to be done in
223 // applicationWillFinishLaunching at the latest, otherwise it doesn't
225 NSMenu *fileMenu = [defaultMainMenu findFileMenu];
227 int idx = [fileMenu indexOfItemWithAction:@selector(fileOpen:)];
228 if (idx >= 0 && idx+1 < [fileMenu numberOfItems])
230 recentFilesMenuItem = [fileMenu itemWithTitle:@"Open Recent"];
231 [[recentFilesMenuItem submenu] performSelector:@selector(_setMenuName:)
232 withObject:@"NSRecentDocumentsMenu"];
234 // Note: The "Recent Files" menu must be moved around since there is no
235 // -[NSApp setRecentFilesMenu:] method. We keep a reference to it to
236 // facilitate this move (see setMainMenu: below).
237 [recentFilesMenuItem retain];
240 #if MM_HANDLE_XCODE_MOD_EVENT
241 [[NSAppleEventManager sharedAppleEventManager]
243 andSelector:@selector(handleXcodeModEvent:replyEvent:)
249 - (void)applicationDidFinishLaunching:(NSNotification *)notification
251 [NSApp setServicesProvider:self];
254 - (BOOL)applicationShouldOpenUntitledFile:(NSApplication *)sender
256 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
257 NSAppleEventManager *aem = [NSAppleEventManager sharedAppleEventManager];
258 NSAppleEventDescriptor *desc = [aem currentAppleEvent];
260 // The user default MMUntitledWindow can be set to control whether an
261 // untitled window should open on 'Open' and 'Reopen' events.
262 int untitledWindowFlag = [ud integerForKey:MMUntitledWindowKey];
263 if ([desc eventID] == kAEOpenApplication
264 && (untitledWindowFlag & MMUntitledWindowOnOpen) == 0)
266 else if ([desc eventID] == kAEReopenApplication
267 && (untitledWindowFlag & MMUntitledWindowOnReopen) == 0)
270 // When a process is started from the command line, the 'Open' event will
271 // contain a parameter to surpress the opening of an untitled window.
272 desc = [desc paramDescriptorForKeyword:keyAEPropData];
273 desc = [desc paramDescriptorForKeyword:keyMMUntitledWindow];
274 if (desc && ![desc booleanValue])
277 // Never open an untitled window if there is at least one open window or if
278 // there are processes that are currently launching.
279 if ([vimControllers count] > 0 || [pidArguments count] > 0)
282 // NOTE! This way it possible to start the app with the command-line
283 // argument '-nowindow yes' and no window will be opened by default.
284 return ![ud boolForKey:MMNoWindowKey];
287 - (BOOL)applicationOpenUntitledFile:(NSApplication *)sender
289 [self newWindow:self];
293 - (void)application:(NSApplication *)sender openFiles:(NSArray *)filenames
295 // Opening files works like this:
296 // a) extract ODB/Xcode/Spotlight parameters from the current Apple event
297 // b) filter out any already open files (see filterOpenFiles::)
298 // c) open any remaining files
300 // A file is opened in an untitled window if there is one (it may be
301 // currently launching, or it may already be visible), otherwise a new
304 // Each launching Vim process has a dictionary of arguments that are passed
305 // to the process when in checks in (via connectBackend:pid:). The
306 // arguments for each launching process can be looked up by its PID (in the
307 // pidArguments dictionary).
309 NSMutableDictionary *arguments = [self extractArgumentsFromOdocEvent:
310 [[NSAppleEventManager sharedAppleEventManager] currentAppleEvent]];
312 // Filter out files that are already open
313 filenames = [self filterOpenFiles:filenames arguments:arguments];
315 // Open any files that remain
316 if ([filenames count]) {
318 BOOL openInTabs = [[NSUserDefaults standardUserDefaults]
319 boolForKey:MMOpenFilesInTabsKey];
321 [arguments setObject:filenames forKey:@"filenames"];
322 [arguments setObject:[NSNumber numberWithBool:YES] forKey:@"openFiles"];
324 // Add file names to "Recent Files" menu.
325 int i, count = [filenames count];
326 for (i = 0; i < count; ++i) {
327 // Don't add files that are being edited remotely (using ODB).
328 if ([arguments objectForKey:@"remoteID"]) continue;
330 [[NSDocumentController sharedDocumentController]
331 noteNewRecentFilePath:[filenames objectAtIndex:i]];
334 if ((openInTabs && (vc = [self topmostVimController]))
335 || (vc = [self findUntitledWindow])) {
336 // Open files in an already open window.
337 [[[vc windowController] window] makeKeyAndOrderFront:self];
338 [self passArguments:arguments toVimController:vc];
340 // Open files in a launching Vim process or start a new process.
341 int pid = [self findLaunchingProcessWithoutArguments];
343 // Pass the filenames to the process straight away.
345 // TODO: It would be nicer if all arguments were passed to the
346 // Vim process in connectBackend::, but if we don't pass the
347 // filename arguments here, the window 'flashes' once when it
348 // opens. This is due to the 'welcome' screen first being
349 // displayed, then quickly thereafter the files are opened.
350 NSArray *fileArgs = [NSArray arrayWithObject:@"-p"];
351 fileArgs = [fileArgs arrayByAddingObjectsFromArray:filenames];
353 pid = [self launchVimProcessWithArguments:fileArgs];
356 // TODO: Notify user of failure?
357 [NSApp replyToOpenOrPrint:
358 NSApplicationDelegateReplyFailure];
362 // Make sure these files aren't opened again when
363 // connectBackend:pid: is called.
364 [arguments setObject:[NSNumber numberWithBool:NO]
365 forKey:@"openFiles"];
368 // TODO: If the Vim process fails to start, or if it changes PID,
369 // then the memory allocated for these parameters will leak.
370 // Ensure that this cannot happen or somehow detect it.
372 if ([arguments count] > 0)
373 [pidArguments setObject:arguments
374 forKey:[NSNumber numberWithInt:pid]];
378 [NSApp replyToOpenOrPrint:NSApplicationDelegateReplySuccess];
379 // NSApplicationDelegateReplySuccess = 0,
380 // NSApplicationDelegateReplyCancel = 1,
381 // NSApplicationDelegateReplyFailure = 2
384 - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender
386 return [[NSUserDefaults standardUserDefaults]
387 boolForKey:MMTerminateAfterLastWindowClosedKey];
390 - (NSApplicationTerminateReply)applicationShouldTerminate:
391 (NSApplication *)sender
393 // TODO: Follow Apple's guidelines for 'Graceful Application Termination'
394 // (in particular, allow user to review changes and save).
395 int reply = NSTerminateNow;
396 BOOL modifiedBuffers = NO;
398 // Go through windows, checking for modified buffers. (Each Vim process
399 // tells MacVim when any buffer has been modified and MacVim sets the
400 // 'documentEdited' flag of the window correspondingly.)
401 NSEnumerator *e = [[NSApp windows] objectEnumerator];
403 while ((window = [e nextObject])) {
404 if ([window isDocumentEdited]) {
405 modifiedBuffers = YES;
410 if (modifiedBuffers) {
411 NSAlert *alert = [[NSAlert alloc] init];
412 [alert setAlertStyle:NSWarningAlertStyle];
413 [alert addButtonWithTitle:NSLocalizedString(@"Quit",
415 [alert addButtonWithTitle:NSLocalizedString(@"Cancel",
417 [alert setMessageText:NSLocalizedString(@"Quit without saving?",
418 @"Quit dialog with changed buffers, title")];
419 [alert setInformativeText:NSLocalizedString(
420 @"There are modified buffers, "
421 "if you quit now all changes will be lost. Quit anyway?",
422 @"Quit dialog with changed buffers, text")];
424 if ([alert runModal] != NSAlertFirstButtonReturn)
425 reply = NSTerminateCancel;
429 // No unmodified buffers, but give a warning if there are multiple
430 // windows and/or tabs open.
431 int numWindows = [vimControllers count];
434 // Count the number of open tabs
435 e = [vimControllers objectEnumerator];
437 while ((vc = [e nextObject])) {
438 NSString *eval = [vc evaluateVimExpression:@"tabpagenr('$')"];
440 int count = [eval intValue];
441 if (count > 0 && count < INT_MAX)
446 if (numWindows > 1 || numTabs > 1) {
447 NSAlert *alert = [[NSAlert alloc] init];
448 [alert setAlertStyle:NSWarningAlertStyle];
449 [alert addButtonWithTitle:NSLocalizedString(@"Quit",
451 [alert addButtonWithTitle:NSLocalizedString(@"Cancel",
453 [alert setMessageText:NSLocalizedString(
454 @"Are you sure you want to quit MacVim?",
455 @"Quit dialog with no changed buffers, title")];
457 NSString *info = nil;
458 if (numWindows > 1) {
459 if (numTabs > numWindows)
460 info = [NSString stringWithFormat:NSLocalizedString(
461 @"There are %d windows open in MacVim, with a "
462 "total of %d tabs. Do you want to quit anyway?",
463 @"Quit dialog with no changed buffers, text"),
464 numWindows, numTabs];
466 info = [NSString stringWithFormat:NSLocalizedString(
467 @"There are %d windows open in MacVim. "
468 "Do you want to quit anyway?",
469 @"Quit dialog with no changed buffers, text"),
473 info = [NSString stringWithFormat:NSLocalizedString(
474 @"There are %d tabs open in MacVim. "
475 "Do you want to quit anyway?",
476 @"Quit dialog with no changed buffers, text"),
480 [alert setInformativeText:info];
482 if ([alert runModal] != NSAlertFirstButtonReturn)
483 reply = NSTerminateCancel;
490 // Tell all Vim processes to terminate now (otherwise they'll leave swap
492 if (NSTerminateNow == reply) {
493 e = [vimControllers objectEnumerator];
495 while ((vc = [e nextObject]))
496 [vc sendMessage:TerminateNowMsgID data:nil];
502 - (void)applicationWillTerminate:(NSNotification *)notification
504 #if MM_HANDLE_XCODE_MOD_EVENT
505 [[NSAppleEventManager sharedAppleEventManager]
506 removeEventHandlerForEventClass:'KAHL'
510 // This will invalidate all connections (since they were spawned from this
512 [connection invalidate];
514 // Send a SIGINT to all running Vim processes, so that they are sure to
515 // receive the connectionDidDie: notification (a process has to be checking
516 // the run-loop for this to happen).
517 unsigned i, count = [vimControllers count];
518 for (i = 0; i < count; ++i) {
519 MMVimController *controller = [vimControllers objectAtIndex:i];
520 int pid = [controller pid];
525 if (fontContainerRef) {
526 ATSFontDeactivate(fontContainerRef, NULL, kATSOptionFlagsDefault);
527 fontContainerRef = 0;
530 [NSApp setDelegate:nil];
533 + (MMAppController *)sharedInstance
535 // Note: The app controller is a singleton which is instantiated in
536 // MainMenu.nib where it is also connected as the delegate of NSApp.
537 id delegate = [NSApp delegate];
538 return [delegate isKindOfClass:self] ? (MMAppController*)delegate : nil;
541 - (NSMenu *)defaultMainMenu
543 return defaultMainMenu;
546 - (void)removeVimController:(id)controller
548 //NSLog(@"%s%@", _cmd, controller);
550 [[controller windowController] close];
552 [vimControllers removeObject:controller];
554 if (![vimControllers count]) {
555 // The last editor window just closed so restore the main menu back to
556 // its default state (which is defined in MainMenu.nib).
557 [self setMainMenu:defaultMainMenu];
561 - (void)windowControllerWillOpen:(MMWindowController *)windowController
563 NSPoint topLeft = NSZeroPoint;
564 NSWindow *topWin = [[[self topmostVimController] windowController] window];
565 NSWindow *win = [windowController window];
569 // If there is a window belonging to a Vim process, cascade from it,
570 // otherwise use the autosaved window position (if any).
572 NSRect frame = [topWin frame];
573 topLeft = NSMakePoint(frame.origin.x, NSMaxY(frame));
575 NSString *topLeftString = [[NSUserDefaults standardUserDefaults]
576 stringForKey:MMTopLeftPointKey];
578 topLeft = NSPointFromString(topLeftString);
581 if (!NSEqualPoints(topLeft, NSZeroPoint)) {
583 topLeft = [win cascadeTopLeftFromPoint:topLeft];
585 [win setFrameTopLeftPoint:topLeft];
588 if (openSelectionString) {
589 // TODO: Pass this as a parameter instead! Get rid of
590 // 'openSelectionString' etc.
592 // There is some text to paste into this window as a result of the
593 // services menu "Open selection ..." being used.
594 [[windowController vimController] dropString:openSelectionString];
595 [openSelectionString release];
596 openSelectionString = nil;
600 - (void)setMainMenu:(NSMenu *)mainMenu
602 if ([NSApp mainMenu] == mainMenu) return;
604 // If the new menu has a "Recent Files" dummy item, then swap the real item
605 // for the dummy. We are forced to do this since Cocoa initializes the
606 // "Recent Files" menu and there is no way to simply point Cocoa to a new
607 // item each time the menus are swapped.
608 NSMenu *fileMenu = [mainMenu findFileMenu];
609 if (recentFilesMenuItem && fileMenu) {
611 [fileMenu indexOfItemWithAction:@selector(recentFilesDummy:)];
613 NSMenuItem *dummyItem = [[fileMenu itemAtIndex:dummyIdx] retain];
614 [fileMenu removeItemAtIndex:dummyIdx];
616 NSMenu *recentFilesParentMenu = [recentFilesMenuItem menu];
617 int idx = [recentFilesParentMenu indexOfItem:recentFilesMenuItem];
619 [[recentFilesMenuItem retain] autorelease];
620 [recentFilesParentMenu removeItemAtIndex:idx];
621 [recentFilesParentMenu insertItem:dummyItem atIndex:idx];
624 [fileMenu insertItem:recentFilesMenuItem atIndex:dummyIdx];
629 // Now set the new menu. Notice that we keep one menu for each editor
630 // window since each editor can have its own set of menus. When swapping
631 // menus we have to tell Cocoa where the new "MacVim", "Windows", and
632 // "Services" menu are.
633 [NSApp setMainMenu:mainMenu];
635 // Setting the "MacVim" (or "Application") menu ensures that it is typeset
636 // in boldface. (The setAppleMenu: method used to be public but is now
637 // private so this will have to be considered a bit of a hack!)
638 NSMenu *appMenu = [mainMenu findApplicationMenu];
639 [NSApp performSelector:@selector(setAppleMenu:) withObject:appMenu];
641 NSMenu *servicesMenu = [mainMenu findServicesMenu];
642 [NSApp setServicesMenu:servicesMenu];
644 NSMenu *windowsMenu = [mainMenu findWindowsMenu];
646 // Cocoa isn't clever enough to get rid of items it has added to the
647 // "Windows" menu so we have to do it ourselves otherwise there will be
648 // multiple menu items for each window in the "Windows" menu.
649 // This code assumes that the only items Cocoa add are ones which
650 // send off the action makeKeyAndOrderFront:. (Cocoa will not add
651 // another separator item if the last item on the "Windows" menu
652 // already is a separator, so we needen't worry about separators.)
653 int i, count = [windowsMenu numberOfItems];
654 for (i = count-1; i >= 0; --i) {
655 NSMenuItem *item = [windowsMenu itemAtIndex:i];
656 if ([item action] == @selector(makeKeyAndOrderFront:))
657 [windowsMenu removeItem:item];
660 [NSApp setWindowsMenu:windowsMenu];
663 - (IBAction)newWindow:(id)sender
665 [self launchVimProcessWithArguments:nil];
668 - (IBAction)fileOpen:(id)sender
671 BOOL trackPwd = [[NSUserDefaults standardUserDefaults]
672 boolForKey:MMDialogsTrackPwdKey];
674 MMVimController *vc = [self keyVimController];
675 if (vc) dir = [[vc vimState] objectForKey:@"pwd"];
678 NSOpenPanel *panel = [NSOpenPanel openPanel];
679 [panel setAllowsMultipleSelection:YES];
680 int result = [panel runModalForDirectory:dir file:nil types:nil];
681 if (NSOKButton == result)
682 [self application:NSApp openFiles:[panel filenames]];
685 - (IBAction)selectNextWindow:(id)sender
687 unsigned i, count = [vimControllers count];
690 NSWindow *keyWindow = [NSApp keyWindow];
691 for (i = 0; i < count; ++i) {
692 MMVimController *vc = [vimControllers objectAtIndex:i];
693 if ([[[vc windowController] window] isEqual:keyWindow])
700 MMVimController *vc = [vimControllers objectAtIndex:i];
701 [[vc windowController] showWindow:self];
705 - (IBAction)selectPreviousWindow:(id)sender
707 unsigned i, count = [vimControllers count];
710 NSWindow *keyWindow = [NSApp keyWindow];
711 for (i = 0; i < count; ++i) {
712 MMVimController *vc = [vimControllers objectAtIndex:i];
713 if ([[[vc windowController] window] isEqual:keyWindow])
723 MMVimController *vc = [vimControllers objectAtIndex:i];
724 [[vc windowController] showWindow:self];
728 - (IBAction)fontSizeUp:(id)sender
730 [[NSFontManager sharedFontManager] modifyFont:
731 [NSNumber numberWithInt:NSSizeUpFontAction]];
734 - (IBAction)fontSizeDown:(id)sender
736 [[NSFontManager sharedFontManager] modifyFont:
737 [NSNumber numberWithInt:NSSizeDownFontAction]];
740 - (IBAction)orderFrontPreferencePanel:(id)sender
742 [[MMPreferenceController sharedPrefsWindowController] showWindow:self];
745 - (IBAction)openWebsite:(id)sender
747 [[NSWorkspace sharedWorkspace] openURL:
748 [NSURL URLWithString:MMWebsiteString]];
751 - (IBAction)showVimHelp:(id)sender
753 // Open a new window with the help window maximized.
754 [self launchVimProcessWithArguments:[NSArray arrayWithObjects:
755 @"-c", @":h gui_mac", @"-c", @":res", nil]];
758 - (IBAction)zoomAll:(id)sender
760 [NSApp makeWindowsPerform:@selector(performZoom:) inOrder:YES];
763 - (byref id <MMFrontendProtocol>)
764 connectBackend:(byref in id <MMBackendProtocol>)backend
767 //NSLog(@"Connect backend (pid=%d)", pid);
768 NSNumber *pidKey = [NSNumber numberWithInt:pid];
769 MMVimController *vc = nil;
772 [(NSDistantObject*)backend
773 setProtocolForProxy:@protocol(MMBackendProtocol)];
775 vc = [[[MMVimController alloc]
776 initWithBackend:backend pid:pid]
779 if (![vimControllers count]) {
780 // The first window autosaves its position. (The autosaving
781 // features of Cocoa are not used because we need more control over
782 // what is autosaved and when it is restored.)
783 [[vc windowController] setWindowAutosaveKey:MMTopLeftPointKey];
786 [vimControllers addObject:vc];
788 id args = [pidArguments objectForKey:pidKey];
789 if (args && [NSNull null] != args)
790 [self passArguments:args toVimController:vc];
792 // HACK! MacVim does not get activated if it is launched from the
793 // terminal, so we forcibly activate here unless it is an untitled
794 // window opening. Untitled windows are treated differently, else
795 // MacVim would steal the focus if another app was activated while the
796 // untitled window was loading.
797 if (!args || args != [NSNull null])
798 [NSApp activateIgnoringOtherApps:YES];
801 [pidArguments removeObjectForKey:pidKey];
806 @catch (NSException *e) {
807 NSLog(@"Exception caught in %s: \"%@\"", _cmd, e);
810 [vimControllers removeObject:vc];
812 [pidArguments removeObjectForKey:pidKey];
818 - (NSArray *)serverList
820 NSMutableArray *array = [NSMutableArray array];
822 unsigned i, count = [vimControllers count];
823 for (i = 0; i < count; ++i) {
824 MMVimController *controller = [vimControllers objectAtIndex:i];
825 if ([controller serverName])
826 [array addObject:[controller serverName]];
832 @end // MMAppController
837 @implementation MMAppController (MMServices)
839 - (void)openSelection:(NSPasteboard *)pboard userData:(NSString *)userData
840 error:(NSString **)error
842 if (![[pboard types] containsObject:NSStringPboardType]) {
843 NSLog(@"WARNING: Pasteboard contains no object of type "
844 "NSStringPboardType");
848 MMVimController *vc = [self topmostVimController];
850 // Open a new tab first, since dropString: does not do this.
851 [vc sendMessage:AddNewTabMsgID data:nil];
852 [vc dropString:[pboard stringForType:NSStringPboardType]];
854 // NOTE: There is no window to paste the selection into, so save the
855 // text, open a new window, and paste the text when the next window
856 // opens. (If this is called several times in a row, then all but the
857 // last call might be ignored.)
858 if (openSelectionString) [openSelectionString release];
859 openSelectionString = [[pboard stringForType:NSStringPboardType] copy];
861 [self newWindow:self];
865 - (void)openFile:(NSPasteboard *)pboard userData:(NSString *)userData
866 error:(NSString **)error
868 if (![[pboard types] containsObject:NSStringPboardType]) {
869 NSLog(@"WARNING: Pasteboard contains no object of type "
870 "NSStringPboardType");
874 // TODO: Parse multiple filenames and create array with names.
875 NSString *string = [pboard stringForType:NSStringPboardType];
876 string = [string stringByTrimmingCharactersInSet:
877 [NSCharacterSet whitespaceAndNewlineCharacterSet]];
878 string = [string stringByStandardizingPath];
880 NSArray *filenames = [self filterFilesAndNotify:
881 [NSArray arrayWithObject:string]];
882 if ([filenames count] > 0) {
883 MMVimController *vc = nil;
884 if (userData && [userData isEqual:@"Tab"])
885 vc = [self topmostVimController];
888 [vc dropFiles:filenames forceOpen:YES];
890 [self application:NSApp openFiles:filenames];
895 @end // MMAppController (MMServices)
900 @implementation MMAppController (Private)
902 - (MMVimController *)keyVimController
904 NSWindow *keyWindow = [NSApp keyWindow];
906 unsigned i, count = [vimControllers count];
907 for (i = 0; i < count; ++i) {
908 MMVimController *vc = [vimControllers objectAtIndex:i];
909 if ([[[vc windowController] window] isEqual:keyWindow])
917 - (MMVimController *)topmostVimController
919 // Find the topmost visible window which has an associated vim controller.
920 NSEnumerator *e = [[NSApp orderedWindows] objectEnumerator];
922 while ((window = [e nextObject]) && [window isVisible]) {
923 unsigned i, count = [vimControllers count];
924 for (i = 0; i < count; ++i) {
925 MMVimController *vc = [vimControllers objectAtIndex:i];
926 if ([[[vc windowController] window] isEqual:window])
934 - (int)launchVimProcessWithArguments:(NSArray *)args
937 NSString *path = [[NSBundle mainBundle] pathForAuxiliaryExecutable:@"Vim"];
940 NSLog(@"ERROR: Vim executable could not be found inside app bundle!");
944 NSArray *taskArgs = [NSArray arrayWithObjects:@"-g", @"-f", nil];
946 taskArgs = [taskArgs arrayByAddingObjectsFromArray:args];
948 BOOL useLoginShell = [[NSUserDefaults standardUserDefaults]
949 boolForKey:MMLoginShellKey];
951 // Run process with a login shell, roughly:
952 // echo "exec Vim -g -f args" | ARGV0=-`basename $SHELL` $SHELL [-l]
953 pid = executeInLoginShell(path, taskArgs);
955 // Run process directly:
957 NSTask *task = [NSTask launchedTaskWithLaunchPath:path
959 pid = task ? [task processIdentifier] : -1;
963 // NOTE: If the process has no arguments, then add a null argument to
964 // the pidArguments dictionary. This is later used to detect that a
965 // process without arguments is being launched.
967 [pidArguments setObject:[NSNull null]
968 forKey:[NSNumber numberWithInt:pid]];
970 NSLog(@"WARNING: %s%@ failed (useLoginShell=%d)", _cmd, args,
977 - (NSArray *)filterFilesAndNotify:(NSArray *)filenames
979 // Go trough 'filenames' array and make sure each file exists. Present
980 // warning dialog if some file was missing.
982 NSString *firstMissingFile = nil;
983 NSMutableArray *files = [NSMutableArray array];
984 unsigned i, count = [filenames count];
986 for (i = 0; i < count; ++i) {
987 NSString *name = [filenames objectAtIndex:i];
988 if ([[NSFileManager defaultManager] fileExistsAtPath:name]) {
989 [files addObject:name];
990 } else if (!firstMissingFile) {
991 firstMissingFile = name;
995 if (firstMissingFile) {
996 NSAlert *alert = [[NSAlert alloc] init];
997 [alert addButtonWithTitle:NSLocalizedString(@"OK",
1001 if ([files count] >= count-1) {
1002 [alert setMessageText:NSLocalizedString(@"File not found",
1003 @"File not found dialog, title")];
1004 text = [NSString stringWithFormat:NSLocalizedString(
1005 @"Could not open file with name %@.",
1006 @"File not found dialog, text"), firstMissingFile];
1008 [alert setMessageText:NSLocalizedString(@"Multiple files not found",
1009 @"File not found dialog, title")];
1010 text = [NSString stringWithFormat:NSLocalizedString(
1011 @"Could not open file with name %@, and %d other files.",
1012 @"File not found dialog, text"),
1013 firstMissingFile, count-[files count]-1];
1016 [alert setInformativeText:text];
1017 [alert setAlertStyle:NSWarningAlertStyle];
1022 [NSApp replyToOpenOrPrint:NSApplicationDelegateReplyFailure];
1028 - (NSArray *)filterOpenFiles:(NSArray *)filenames
1029 arguments:(NSDictionary *)args
1031 // Check if any of the files in the 'filenames' array are open in any Vim
1032 // process. Remove the files that are open from the 'filenames' array and
1033 // return it. If all files were filtered out, then raise the first file in
1034 // the Vim process it is open. Files that are filtered are sent an odb
1035 // open event in case theID is not zero.
1037 NSMutableDictionary *localArgs =
1038 [NSMutableDictionary dictionaryWithDictionary:args];
1039 MMVimController *raiseController = nil;
1040 NSString *raiseFile = nil;
1041 NSMutableArray *files = [filenames mutableCopy];
1042 NSString *expr = [NSString stringWithFormat:
1043 @"map([\"%@\"],\"bufloaded(v:val)\")",
1044 [files componentsJoinedByString:@"\",\""]];
1045 unsigned i, count = [vimControllers count];
1047 // Ensure that the files aren't opened when passing arguments.
1048 [localArgs setObject:[NSNumber numberWithBool:NO] forKey:@"openFiles"];
1050 for (i = 0; i < count && [files count]; ++i) {
1051 MMVimController *controller = [vimControllers objectAtIndex:i];
1053 // Query Vim for which files in the 'files' array are open.
1054 NSString *eval = [controller evaluateVimExpression:expr];
1055 if (!eval) continue;
1057 NSIndexSet *idxSet = [NSIndexSet indexSetWithVimList:eval];
1058 if ([idxSet count]) {
1060 // Remember the file and which Vim that has it open so that
1061 // we can raise it later on.
1062 raiseController = controller;
1063 raiseFile = [files objectAtIndex:[idxSet firstIndex]];
1064 [[raiseFile retain] autorelease];
1067 // Pass (ODB/Xcode/Spotlight) arguments to this process.
1068 [localArgs setObject:[files objectsAtIndexes:idxSet]
1069 forKey:@"filenames"];
1070 [self passArguments:localArgs toVimController:controller];
1072 // Remove all the files that were open in this Vim process and
1073 // create a new expression to evaluate.
1074 [files removeObjectsAtIndexes:idxSet];
1075 expr = [NSString stringWithFormat:
1076 @"map([\"%@\"],\"bufloaded(v:val)\")",
1077 [files componentsJoinedByString:@"\",\""]];
1081 if (![files count] && raiseFile) {
1082 // Raise the window containing the first file that was already open,
1083 // and make sure that the tab containing that file is selected. Only
1084 // do this if there are no more files to open, otherwise sometimes the
1085 // window with 'raiseFile' will be raised, other times it might be the
1086 // window that will open with the files in the 'files' array.
1087 raiseFile = [raiseFile stringByEscapingSpecialFilenameCharacters];
1088 NSString *input = [NSString stringWithFormat:@"<C-\\><C-N>"
1089 ":let oldswb=&swb|let &swb=\"useopen,usetab\"|"
1090 "tab sb %@|let &swb=oldswb|unl oldswb|"
1091 "cal foreground()|redr|f<CR>", raiseFile];
1093 [raiseController addVimInput:input];
1099 #if MM_HANDLE_XCODE_MOD_EVENT
1100 - (void)handleXcodeModEvent:(NSAppleEventDescriptor *)event
1101 replyEvent:(NSAppleEventDescriptor *)reply
1104 // Xcode sends this event to query MacVim which open files have been
1106 NSLog(@"reply:%@", reply);
1107 NSLog(@"event:%@", event);
1109 NSEnumerator *e = [vimControllers objectEnumerator];
1111 while ((vc = [e nextObject])) {
1112 DescType type = [reply descriptorType];
1113 unsigned len = [[type data] length];
1114 NSMutableData *data = [NSMutableData data];
1116 [data appendBytes:&type length:sizeof(DescType)];
1117 [data appendBytes:&len length:sizeof(unsigned)];
1118 [data appendBytes:[reply data] length:len];
1120 [vc sendMessage:XcodeModMsgID data:data];
1126 - (int)findLaunchingProcessWithoutArguments
1128 NSArray *keys = [pidArguments allKeysForObject:[NSNull null]];
1129 if ([keys count] > 0) {
1130 //NSLog(@"found launching process without arguments");
1131 return [[keys objectAtIndex:0] intValue];
1137 - (MMVimController *)findUntitledWindow
1139 NSEnumerator *e = [vimControllers objectEnumerator];
1141 while ((vc = [e nextObject])) {
1142 // TODO: This is a moronic test...should query the Vim process if there
1143 // are any open buffers or something like that instead.
1144 NSString *title = [[[vc windowController] window] title];
1146 // TODO: this will not work in a localized MacVim
1147 if ([title hasPrefix:@"[No Name] - VIM"]) {
1148 //NSLog(@"found untitled window");
1156 - (NSMutableDictionary *)extractArgumentsFromOdocEvent:
1157 (NSAppleEventDescriptor *)desc
1159 NSMutableDictionary *dict = [NSMutableDictionary dictionary];
1161 // 1. Extract ODB parameters (if any)
1162 NSAppleEventDescriptor *odbdesc = desc;
1163 if (![odbdesc paramDescriptorForKeyword:keyFileSender]) {
1164 // The ODB paramaters may hide inside the 'keyAEPropData' descriptor.
1165 odbdesc = [odbdesc paramDescriptorForKeyword:keyAEPropData];
1166 if (![odbdesc paramDescriptorForKeyword:keyFileSender])
1171 NSAppleEventDescriptor *p =
1172 [odbdesc paramDescriptorForKeyword:keyFileSender];
1174 [dict setObject:[NSNumber numberWithUnsignedInt:[p typeCodeValue]]
1175 forKey:@"remoteID"];
1177 p = [odbdesc paramDescriptorForKeyword:keyFileCustomPath];
1179 [dict setObject:[p stringValue] forKey:@"remotePath"];
1181 p = [odbdesc paramDescriptorForKeyword:keyFileSenderToken];
1183 [dict setObject:p forKey:@"remotePath"];
1186 // 2. Extract Xcode parameters (if any)
1187 NSAppleEventDescriptor *xcodedesc =
1188 [desc paramDescriptorForKeyword:keyAEPosition];
1191 MMSelectionRange *sr = (MMSelectionRange*)[[xcodedesc data] bytes];
1193 if (sr->lineNum < 0) {
1194 // Should select a range of lines.
1195 range.location = sr->startRange + 1;
1196 range.length = sr->endRange - sr->startRange + 1;
1198 // Should only move cursor to a line.
1199 range.location = sr->lineNum + 1;
1203 [dict setObject:NSStringFromRange(range) forKey:@"selectionRange"];
1206 // 3. Extract Spotlight search text (if any)
1207 NSAppleEventDescriptor *spotlightdesc =
1208 [desc paramDescriptorForKeyword:keyAESearchText];
1210 [dict setObject:[spotlightdesc stringValue] forKey:@"searchText"];
1215 - (void)passArguments:(NSDictionary *)args toVimController:(MMVimController*)vc
1219 // Pass filenames to open if required (the 'openFiles' argument can be used
1220 // to disallow opening of the files).
1221 NSArray *filenames = [args objectForKey:@"filenames"];
1222 if (filenames && [[args objectForKey:@"openFiles"] boolValue]) {
1223 NSString *tabDrop = buildTabDropCommand(filenames);
1224 [vc addVimInput:tabDrop];
1228 if (filenames && [args objectForKey:@"remoteID"]) {
1229 [vc odbEdit:filenames
1230 server:[[args objectForKey:@"remoteID"] unsignedIntValue]
1231 path:[args objectForKey:@"remotePath"]
1232 token:[args objectForKey:@"remoteToken"]];
1235 // Pass range of lines to select
1236 if ([args objectForKey:@"selectionRange"]) {
1237 NSRange selectionRange = NSRangeFromString(
1238 [args objectForKey:@"selectionRange"]);
1239 [vc addVimInput:buildSelectRangeCommand(selectionRange)];
1243 NSString *searchText = [args objectForKey:@"searchText"];
1245 [vc addVimInput:buildSearchTextCommand(searchText)];
1248 @end // MMAppController (Private)
1253 @implementation NSNumber (MMExtras)
1256 return [self intValue];
1258 @end // NSNumber (MMExtras)
1263 @implementation NSMenu (MMExtras)
1265 - (int)indexOfItemWithAction:(SEL)action
1267 int i, count = [self numberOfItems];
1268 for (i = 0; i < count; ++i) {
1269 NSMenuItem *item = [self itemAtIndex:i];
1270 if ([item action] == action)
1277 - (NSMenuItem *)itemWithAction:(SEL)action
1279 int idx = [self indexOfItemWithAction:action];
1280 return idx >= 0 ? [self itemAtIndex:idx] : nil;
1283 - (NSMenu *)findMenuContainingItemWithAction:(SEL)action
1285 // NOTE: We only look for the action in the submenus of 'self'
1286 int i, count = [self numberOfItems];
1287 for (i = 0; i < count; ++i) {
1288 NSMenu *menu = [[self itemAtIndex:i] submenu];
1289 NSMenuItem *item = [menu itemWithAction:action];
1290 if (item) return menu;
1296 - (NSMenu *)findWindowsMenu
1298 return [self findMenuContainingItemWithAction:
1299 @selector(performMiniaturize:)];
1302 - (NSMenu *)findApplicationMenu
1304 // TODO: Just return [self itemAtIndex:0]?
1305 return [self findMenuContainingItemWithAction:@selector(terminate:)];
1308 - (NSMenu *)findServicesMenu
1310 // NOTE! Our heuristic for finding the "Services" menu is to look for the
1311 // second item before the "Hide MacVim" menu item on the "MacVim" menu.
1312 // (The item before "Hide MacVim" should be a separator, but this is not
1313 // important as long as the item before that is the "Services" menu.)
1315 NSMenu *appMenu = [self findApplicationMenu];
1316 if (!appMenu) return nil;
1318 int idx = [appMenu indexOfItemWithAction: @selector(hide:)];
1319 if (idx-2 < 0) return nil; // idx == -1, if selector not found
1321 return [[appMenu itemAtIndex:idx-2] submenu];
1324 - (NSMenu *)findFileMenu
1326 return [self findMenuContainingItemWithAction:@selector(performClose:)];
1329 @end // NSMenu (MMExtras)
1335 executeInLoginShell(NSString *path, NSArray *args)
1337 // Start a login shell and execute the command 'path' with arguments 'args'
1338 // in the shell. This ensures that user environment variables are set even
1339 // when MacVim was started from the Finder.
1342 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
1344 // Determine which shell to use to execute the command. The user
1345 // may decide which shell to use by setting a user default or the
1346 // $SHELL environment variable.
1347 NSString *shell = [ud stringForKey:MMLoginShellCommandKey];
1348 if (!shell || [shell length] == 0)
1349 shell = [[[NSProcessInfo processInfo] environment]
1350 objectForKey:@"SHELL"];
1352 shell = @"/bin/bash";
1354 //NSLog(@"shell = %@", shell);
1356 // Bash needs the '-l' flag to launch a login shell. The user may add
1357 // flags by setting a user default.
1358 NSString *shellArgument = [ud stringForKey:MMLoginShellArgumentKey];
1359 if (!shellArgument || [shellArgument length] == 0) {
1360 if ([[shell lastPathComponent] isEqual:@"bash"])
1361 shellArgument = @"-l";
1363 shellArgument = nil;
1366 //NSLog(@"shellArgument = %@", shellArgument);
1368 // Build input string to pipe to the login shell.
1369 NSMutableString *input = [NSMutableString stringWithFormat:
1370 @"exec \"%@\"", path];
1372 // Append all arguments, making sure they are properly quoted, even
1373 // when they contain single quotes.
1374 NSEnumerator *e = [args objectEnumerator];
1377 while ((obj = [e nextObject])) {
1378 NSMutableString *arg = [NSMutableString stringWithString:obj];
1379 [arg replaceOccurrencesOfString:@"'" withString:@"'\"'\"'"
1380 options:NSLiteralSearch
1381 range:NSMakeRange(0, [arg length])];
1382 [input appendFormat:@" '%@'", arg];
1386 // Build the argument vector used to start the login shell.
1387 NSString *shellArg0 = [NSString stringWithFormat:@"-%@",
1388 [shell lastPathComponent]];
1389 char *shellArgv[3] = { (char *)[shellArg0 UTF8String], NULL, NULL };
1391 shellArgv[1] = (char *)[shellArgument UTF8String];
1393 // Get the C string representation of the shell path before the fork since
1394 // we must not call Foundation functions after a fork.
1395 const char *shellPath = [shell fileSystemRepresentation];
1397 // Fork and execute the process.
1399 if (pipe(ds)) return -1;
1404 } else if (pid == 0) {
1406 if (close(ds[1]) == -1) exit(255);
1407 if (dup2(ds[0], 0) == -1) exit(255);
1409 execv(shellPath, shellArgv);
1411 // Never reached unless execv fails
1415 if (close(ds[0]) == -1) return -1;
1417 // Send input to execute to the child process
1418 [input appendString:@"\n"];
1419 int bytes = [input lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
1421 if (write(ds[1], [input UTF8String], bytes) != bytes) return -1;
1422 if (close(ds[1]) == -1) return -1;