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.
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
29 #import "MMAppController.h"
30 #import "MMVimController.h"
31 #import "MMWindowController.h"
32 #import "MMPreferenceController.h"
36 #define MM_HANDLE_XCODE_MOD_EVENT 0
40 // Default timeout intervals on all connections.
41 static NSTimeInterval MMRequestTimeout = 5;
42 static NSTimeInterval MMReplyTimeout = 5;
44 static NSString *MMWebsiteString = @"http://code.google.com/p/macvim/";
47 #pragma options align=mac68k
50 short unused1; // 0 (not used)
51 short lineNum; // line to select (< 0 to specify range)
52 long startRange; // start of selection range (if line < 0)
53 long endRange; // end of selection range (if line < 0)
54 long unused2; // 0 (not used)
55 long theDate; // modification date/time
57 #pragma options align=reset
60 static int executeInLoginShell(NSString *path, NSArray *args);
63 @interface MMAppController (MMServices)
64 - (void)openSelection:(NSPasteboard *)pboard userData:(NSString *)userData
65 error:(NSString **)error;
66 - (void)openFile:(NSPasteboard *)pboard userData:(NSString *)userData
67 error:(NSString **)error;
71 @interface MMAppController (Private)
72 - (MMVimController *)keyVimController;
73 - (MMVimController *)topmostVimController;
74 - (int)launchVimProcessWithArguments:(NSArray *)args;
75 - (NSArray *)filterFilesAndNotify:(NSArray *)files;
76 - (NSArray *)filterOpenFiles:(NSArray *)filenames
77 arguments:(NSDictionary *)args;
78 #if MM_HANDLE_XCODE_MOD_EVENT
79 - (void)handleXcodeModEvent:(NSAppleEventDescriptor *)event
80 replyEvent:(NSAppleEventDescriptor *)reply;
82 - (int)findLaunchingProcessWithoutArguments;
83 - (MMVimController *)findUntitledWindow;
84 - (NSMutableDictionary *)extractArgumentsFromOdocEvent:
85 (NSAppleEventDescriptor *)desc;
86 - (void)passArguments:(NSDictionary *)args toVimController:(MMVimController*)vc;
90 @interface NSNumber (MMExtras)
95 @interface NSMenu (MMExtras)
96 - (int)indexOfItemWithAction:(SEL)action;
97 - (NSMenuItem *)itemWithAction:(SEL)action;
98 - (NSMenu *)findMenuContainingItemWithAction:(SEL)action;
99 - (NSMenu *)findWindowsMenu;
100 - (NSMenu *)findApplicationMenu;
101 - (NSMenu *)findServicesMenu;
102 - (NSMenu *)findFileMenu;
108 @implementation MMAppController
112 NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:
113 [NSNumber numberWithBool:NO], MMNoWindowKey,
114 [NSNumber numberWithInt:64], MMTabMinWidthKey,
115 [NSNumber numberWithInt:6*64], MMTabMaxWidthKey,
116 [NSNumber numberWithInt:132], MMTabOptimumWidthKey,
117 [NSNumber numberWithInt:2], MMTextInsetLeftKey,
118 [NSNumber numberWithInt:1], MMTextInsetRightKey,
119 [NSNumber numberWithInt:1], MMTextInsetTopKey,
120 [NSNumber numberWithInt:1], MMTextInsetBottomKey,
121 [NSNumber numberWithBool:NO], MMTerminateAfterLastWindowClosedKey,
122 @"MMTypesetter", MMTypesetterKey,
123 [NSNumber numberWithFloat:1], MMCellWidthMultiplierKey,
124 [NSNumber numberWithFloat:-1], MMBaselineOffsetKey,
125 [NSNumber numberWithBool:YES], MMTranslateCtrlClickKey,
126 [NSNumber numberWithBool:NO], MMOpenFilesInTabsKey,
127 [NSNumber numberWithBool:NO], MMNoFontSubstitutionKey,
128 [NSNumber numberWithBool:NO], MMLoginShellKey,
129 [NSNumber numberWithBool:NO], MMAtsuiRendererKey,
130 [NSNumber numberWithInt:MMUntitledWindowAlways],
132 [NSNumber numberWithBool:NO], MMTexturedWindowKey,
133 [NSNumber numberWithBool:NO], MMZoomBothKey,
134 @"", MMLoginShellCommandKey,
135 @"", MMLoginShellArgumentKey,
136 [NSNumber numberWithBool:YES], MMDialogsTrackPwdKey,
139 [[NSUserDefaults standardUserDefaults] registerDefaults:dict];
141 NSArray *types = [NSArray arrayWithObject:NSStringPboardType];
142 [NSApp registerServicesMenuSendTypes:types returnTypes:types];
147 if ((self = [super init])) {
148 fontContainerRef = loadFonts();
150 vimControllers = [NSMutableArray new];
151 pidArguments = [NSMutableDictionary new];
153 // NOTE: Do not use the default connection since the Logitech Control
154 // Center (LCC) input manager steals and this would cause MacVim to
155 // never open any windows. (This is a bug in LCC but since they are
156 // unlikely to fix it, we graciously give them the default connection.)
157 connection = [[NSConnection alloc] initWithReceivePort:[NSPort port]
159 [connection setRootObject:self];
160 [connection setRequestTimeout:MMRequestTimeout];
161 [connection setReplyTimeout:MMReplyTimeout];
163 // NOTE: When the user is resizing the window the AppKit puts the run
164 // loop in event tracking mode. Unless the connection listens to
165 // request in this mode, live resizing won't work.
166 [connection addRequestMode:NSEventTrackingRunLoopMode];
168 // NOTE! If the name of the connection changes here it must also be
169 // updated in MMBackend.m.
170 NSString *name = [NSString stringWithFormat:@"%@-connection",
171 [[NSBundle mainBundle] bundleIdentifier]];
172 //NSLog(@"Registering connection with name '%@'", name);
173 if (![connection registerName:name]) {
174 NSLog(@"FATAL ERROR: Failed to register connection with name '%@'",
176 [connection release]; connection = nil;
185 //NSLog(@"MMAppController dealloc");
187 [connection release]; connection = nil;
188 [pidArguments release]; pidArguments = nil;
189 [vimControllers release]; vimControllers = nil;
190 [openSelectionString release]; openSelectionString = nil;
191 [recentFilesMenuItem release]; recentFilesMenuItem = nil;
192 [defaultMainMenu release]; defaultMainMenu = nil;
197 - (void)applicationWillFinishLaunching:(NSNotification *)notification
199 // Remember the default menu so that it can be restored if the user closes
200 // all editor windows.
201 defaultMainMenu = [[NSApp mainMenu] retain];
203 // Set up the "Open Recent" menu. See
204 // http://lapcatsoftware.com/blog/2007/07/10/
205 // working-without-a-nib-part-5-open-recent-menu/
207 // http://www.cocoabuilder.com/archive/message/cocoa/2007/8/15/187793
208 // for more information.
210 // The menu itself is created in MainMenu.nib but we still seem to have to
211 // hack around a bit to get it to work. (This has to be done in
212 // applicationWillFinishLaunching at the latest, otherwise it doesn't
214 NSMenu *fileMenu = [defaultMainMenu findFileMenu];
216 int idx = [fileMenu indexOfItemWithAction:@selector(fileOpen:)];
217 if (idx >= 0 && idx+1 < [fileMenu numberOfItems])
219 recentFilesMenuItem = [fileMenu itemWithTitle:@"Open Recent"];
220 [[recentFilesMenuItem submenu] performSelector:@selector(_setMenuName:)
221 withObject:@"NSRecentDocumentsMenu"];
223 // Note: The "Recent Files" menu must be moved around since there is no
224 // -[NSApp setRecentFilesMenu:] method. We keep a reference to it to
225 // facilitate this move (see setMainMenu: below).
226 [recentFilesMenuItem retain];
229 #if MM_HANDLE_XCODE_MOD_EVENT
230 [[NSAppleEventManager sharedAppleEventManager]
232 andSelector:@selector(handleXcodeModEvent:replyEvent:)
238 - (void)applicationDidFinishLaunching:(NSNotification *)notification
240 [NSApp setServicesProvider:self];
243 - (BOOL)applicationShouldOpenUntitledFile:(NSApplication *)sender
245 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
246 NSAppleEventManager *aem = [NSAppleEventManager sharedAppleEventManager];
247 NSAppleEventDescriptor *desc = [aem currentAppleEvent];
249 // The user default MMUntitledWindow can be set to control whether an
250 // untitled window should open on 'Open' and 'Reopen' events.
251 int untitledWindowFlag = [ud integerForKey:MMUntitledWindowKey];
252 if ([desc eventID] == kAEOpenApplication
253 && (untitledWindowFlag & MMUntitledWindowOnOpen) == 0)
255 else if ([desc eventID] == kAEReopenApplication
256 && (untitledWindowFlag & MMUntitledWindowOnReopen) == 0)
259 // When a process is started from the command line, the 'Open' event will
260 // contain a parameter to surpress the opening of an untitled window.
261 desc = [desc paramDescriptorForKeyword:keyAEPropData];
262 desc = [desc paramDescriptorForKeyword:keyMMUntitledWindow];
263 if (desc && ![desc booleanValue])
266 // Never open an untitled window if there is at least one open window or if
267 // there are processes that are currently launching.
268 if ([vimControllers count] > 0 || [pidArguments count] > 0)
271 // NOTE! This way it possible to start the app with the command-line
272 // argument '-nowindow yes' and no window will be opened by default.
273 return ![ud boolForKey:MMNoWindowKey];
276 - (BOOL)applicationOpenUntitledFile:(NSApplication *)sender
278 [self newWindow:self];
282 - (void)application:(NSApplication *)sender openFiles:(NSArray *)filenames
284 // Opening files works like this:
285 // a) extract ODB/Xcode/Spotlight parameters from the current Apple event
286 // b) filter out any already open files (see filterOpenFiles::)
287 // c) open any remaining files
289 // A file is opened in an untitled window if there is one (it may be
290 // currently launching, or it may already be visible), otherwise a new
293 // Each launching Vim process has a dictionary of arguments that are passed
294 // to the process when in checks in (via connectBackend:pid:). The
295 // arguments for each launching process can be looked up by its PID (in the
296 // pidArguments dictionary).
298 NSMutableDictionary *arguments = [self extractArgumentsFromOdocEvent:
299 [[NSAppleEventManager sharedAppleEventManager] currentAppleEvent]];
301 // Filter out files that are already open
302 filenames = [self filterOpenFiles:filenames arguments:arguments];
304 // Open any files that remain
305 if ([filenames count]) {
307 BOOL openInTabs = [[NSUserDefaults standardUserDefaults]
308 boolForKey:MMOpenFilesInTabsKey];
310 [arguments setObject:filenames forKey:@"filenames"];
311 [arguments setObject:[NSNumber numberWithBool:YES] forKey:@"openFiles"];
313 // Add file names to "Recent Files" menu.
314 int i, count = [filenames count];
315 for (i = 0; i < count; ++i) {
316 // Don't add files that are being edited remotely (using ODB).
317 if ([arguments objectForKey:@"remoteID"]) continue;
319 [[NSDocumentController sharedDocumentController]
320 noteNewRecentFilePath:[filenames objectAtIndex:i]];
323 if ((openInTabs && (vc = [self topmostVimController]))
324 || (vc = [self findUntitledWindow])) {
325 // Open files in an already open window.
326 [[[vc windowController] window] makeKeyAndOrderFront:self];
327 [self passArguments:arguments toVimController:vc];
329 // Open files in a launching Vim process or start a new process.
330 int pid = [self findLaunchingProcessWithoutArguments];
332 // Pass the filenames to the process straight away.
334 // TODO: It would be nicer if all arguments were passed to the
335 // Vim process in connectBackend::, but if we don't pass the
336 // filename arguments here, the window 'flashes' once when it
337 // opens. This is due to the 'welcome' screen first being
338 // displayed, then quickly thereafter the files are opened.
339 NSArray *fileArgs = [NSArray arrayWithObject:@"-p"];
340 fileArgs = [fileArgs arrayByAddingObjectsFromArray:filenames];
342 pid = [self launchVimProcessWithArguments:fileArgs];
345 // TODO: Notify user of failure?
346 [NSApp replyToOpenOrPrint:
347 NSApplicationDelegateReplyFailure];
351 // Make sure these files aren't opened again when
352 // connectBackend:pid: is called.
353 [arguments setObject:[NSNumber numberWithBool:NO]
354 forKey:@"openFiles"];
357 // TODO: If the Vim process fails to start, or if it changes PID,
358 // then the memory allocated for these parameters will leak.
359 // Ensure that this cannot happen or somehow detect it.
361 if ([arguments count] > 0)
362 [pidArguments setObject:arguments
363 forKey:[NSNumber numberWithInt:pid]];
367 [NSApp replyToOpenOrPrint:NSApplicationDelegateReplySuccess];
368 // NSApplicationDelegateReplySuccess = 0,
369 // NSApplicationDelegateReplyCancel = 1,
370 // NSApplicationDelegateReplyFailure = 2
373 - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender
375 return [[NSUserDefaults standardUserDefaults]
376 boolForKey:MMTerminateAfterLastWindowClosedKey];
379 - (NSApplicationTerminateReply)applicationShouldTerminate:
380 (NSApplication *)sender
382 // TODO: Follow Apple's guidelines for 'Graceful Application Termination'
383 // (in particular, allow user to review changes and save).
384 int reply = NSTerminateNow;
385 BOOL modifiedBuffers = NO;
387 // Go through windows, checking for modified buffers. (Each Vim process
388 // tells MacVim when any buffer has been modified and MacVim sets the
389 // 'documentEdited' flag of the window correspondingly.)
390 NSEnumerator *e = [[NSApp windows] objectEnumerator];
392 while ((window = [e nextObject])) {
393 if ([window isDocumentEdited]) {
394 modifiedBuffers = YES;
399 if (modifiedBuffers) {
400 NSAlert *alert = [[NSAlert alloc] init];
401 [alert setAlertStyle:NSWarningAlertStyle];
402 [alert addButtonWithTitle:NSLocalizedString(@"Quit",
404 [alert addButtonWithTitle:NSLocalizedString(@"Cancel",
406 [alert setMessageText:NSLocalizedString(@"Quit without saving?",
407 @"Quit dialog with changed buffers, title")];
408 [alert setInformativeText:NSLocalizedString(
409 @"There are modified buffers, "
410 "if you quit now all changes will be lost. Quit anyway?",
411 @"Quit dialog with changed buffers, text")];
413 if ([alert runModal] != NSAlertFirstButtonReturn)
414 reply = NSTerminateCancel;
418 // No unmodified buffers, but give a warning if there are multiple
419 // windows and/or tabs open.
420 int numWindows = [vimControllers count];
423 // Count the number of open tabs
424 e = [vimControllers objectEnumerator];
426 while ((vc = [e nextObject])) {
427 NSString *eval = [vc evaluateVimExpression:@"tabpagenr('$')"];
429 int count = [eval intValue];
430 if (count > 0 && count < INT_MAX)
435 if (numWindows > 1 || numTabs > 1) {
436 NSAlert *alert = [[NSAlert alloc] init];
437 [alert setAlertStyle:NSWarningAlertStyle];
438 [alert addButtonWithTitle:NSLocalizedString(@"Quit",
440 [alert addButtonWithTitle:NSLocalizedString(@"Cancel",
442 [alert setMessageText:NSLocalizedString(
443 @"Are you sure you want to quit MacVim?",
444 @"Quit dialog with no changed buffers, title")];
446 NSString *info = nil;
447 if (numWindows > 1) {
448 if (numTabs > numWindows)
449 info = [NSString stringWithFormat:NSLocalizedString(
450 @"There are %d windows open in MacVim, with a "
451 "total of %d tabs. Do you want to quit anyway?",
452 @"Quit dialog with no changed buffers, text"),
453 numWindows, numTabs];
455 info = [NSString stringWithFormat:NSLocalizedString(
456 @"There are %d windows open in MacVim. "
457 "Do you want to quit anyway?",
458 @"Quit dialog with no changed buffers, text"),
462 info = [NSString stringWithFormat:NSLocalizedString(
463 @"There are %d tabs open in MacVim. "
464 "Do you want to quit anyway?",
465 @"Quit dialog with no changed buffers, text"),
469 [alert setInformativeText:info];
471 if ([alert runModal] != NSAlertFirstButtonReturn)
472 reply = NSTerminateCancel;
479 // Tell all Vim processes to terminate now (otherwise they'll leave swap
481 if (NSTerminateNow == reply) {
482 e = [vimControllers objectEnumerator];
484 while ((vc = [e nextObject]))
485 [vc sendMessage:TerminateNowMsgID data:nil];
491 - (void)applicationWillTerminate:(NSNotification *)notification
493 #if MM_HANDLE_XCODE_MOD_EVENT
494 [[NSAppleEventManager sharedAppleEventManager]
495 removeEventHandlerForEventClass:'KAHL'
499 // This will invalidate all connections (since they were spawned from this
501 [connection invalidate];
503 // Send a SIGINT to all running Vim processes, so that they are sure to
504 // receive the connectionDidDie: notification (a process has to be checking
505 // the run-loop for this to happen).
506 unsigned i, count = [vimControllers count];
507 for (i = 0; i < count; ++i) {
508 MMVimController *controller = [vimControllers objectAtIndex:i];
509 int pid = [controller pid];
514 if (fontContainerRef) {
515 ATSFontDeactivate(fontContainerRef, NULL, kATSOptionFlagsDefault);
516 fontContainerRef = 0;
519 [NSApp setDelegate:nil];
522 + (MMAppController *)sharedInstance
524 // Note: The app controller is a singleton which is instantiated in
525 // MainMenu.nib where it is also connected as the delegate of NSApp.
526 id delegate = [NSApp delegate];
527 return [delegate isKindOfClass:self] ? (MMAppController*)delegate : nil;
530 - (NSMenu *)defaultMainMenu
532 return defaultMainMenu;
535 - (void)removeVimController:(id)controller
537 //NSLog(@"%s%@", _cmd, controller);
539 [[controller windowController] close];
541 [vimControllers removeObject:controller];
543 if (![vimControllers count]) {
544 // The last editor window just closed so restore the main menu back to
545 // its default state (which is defined in MainMenu.nib).
546 [self setMainMenu:defaultMainMenu];
550 - (void)windowControllerWillOpen:(MMWindowController *)windowController
552 NSPoint topLeft = NSZeroPoint;
553 NSWindow *topWin = [[[self topmostVimController] windowController] window];
554 NSWindow *win = [windowController window];
558 // If there is a window belonging to a Vim process, cascade from it,
559 // otherwise use the autosaved window position (if any).
561 NSRect frame = [topWin frame];
562 topLeft = NSMakePoint(frame.origin.x, NSMaxY(frame));
564 NSString *topLeftString = [[NSUserDefaults standardUserDefaults]
565 stringForKey:MMTopLeftPointKey];
567 topLeft = NSPointFromString(topLeftString);
570 if (!NSEqualPoints(topLeft, NSZeroPoint)) {
572 topLeft = [win cascadeTopLeftFromPoint:topLeft];
574 [win setFrameTopLeftPoint:topLeft];
577 if (openSelectionString) {
578 // TODO: Pass this as a parameter instead! Get rid of
579 // 'openSelectionString' etc.
581 // There is some text to paste into this window as a result of the
582 // services menu "Open selection ..." being used.
583 [[windowController vimController] dropString:openSelectionString];
584 [openSelectionString release];
585 openSelectionString = nil;
589 - (void)setMainMenu:(NSMenu *)mainMenu
591 if ([NSApp mainMenu] == mainMenu) return;
593 // If the new menu has a "Recent Files" dummy item, then swap the real item
594 // for the dummy. We are forced to do this since Cocoa initializes the
595 // "Recent Files" menu and there is no way to simply point Cocoa to a new
596 // item each time the menus are swapped.
597 NSMenu *fileMenu = [mainMenu findFileMenu];
599 [fileMenu indexOfItemWithAction:@selector(recentFilesDummy:)];
600 if (dummyIdx >= 0 && recentFilesMenuItem) {
601 NSMenuItem *dummyItem = [[fileMenu itemAtIndex:dummyIdx] retain];
602 [fileMenu removeItemAtIndex:dummyIdx];
604 NSMenu *recentFilesParentMenu = [recentFilesMenuItem menu];
605 int idx = [recentFilesParentMenu indexOfItem:recentFilesMenuItem];
607 [[recentFilesMenuItem retain] autorelease];
608 [recentFilesParentMenu removeItemAtIndex:idx];
609 [recentFilesParentMenu insertItem:dummyItem atIndex:idx];
612 [fileMenu insertItem:recentFilesMenuItem atIndex:dummyIdx];
616 // Now set the new menu. Notice that we keep one menu for each editor
617 // window since each editor can have its own set of menus. When swapping
618 // menus we have to tell Cocoa where the new "MacVim", "Windows", and
619 // "Services" menu are.
620 [NSApp setMainMenu:mainMenu];
622 // Setting the "MacVim" (or "Application") menu ensures that it is typeset
623 // in boldface. (The setAppleMenu: method used to be public but is now
624 // private so this will have to be considered a bit of a hack!)
625 NSMenu *appMenu = [mainMenu findApplicationMenu];
626 [NSApp performSelector:@selector(setAppleMenu:) withObject:appMenu];
628 NSMenu *servicesMenu = [mainMenu findServicesMenu];
629 [NSApp setServicesMenu:servicesMenu];
631 NSMenu *windowsMenu = [mainMenu findWindowsMenu];
633 // Cocoa isn't clever enough to get rid of items it has added to the
634 // "Windows" menu so we have to do it ourselves otherwise there will be
635 // multiple menu items for each window in the "Windows" menu.
636 // This code assumes that the only items Cocoa add are ones which
637 // send off the action makeKeyAndOrderFront:. (Cocoa will not add
638 // another separator item if the last item on the "Windows" menu
639 // already is a separator, so we needen't worry about separators.)
640 int i, count = [windowsMenu numberOfItems];
641 for (i = count-1; i >= 0; --i) {
642 NSMenuItem *item = [windowsMenu itemAtIndex:i];
643 if ([item action] == @selector(makeKeyAndOrderFront:))
644 [windowsMenu removeItem:item];
647 [NSApp setWindowsMenu:windowsMenu];
650 - (IBAction)newWindow:(id)sender
652 [self launchVimProcessWithArguments:nil];
655 - (IBAction)fileOpen:(id)sender
658 BOOL trackPwd = [[NSUserDefaults standardUserDefaults]
659 boolForKey:MMDialogsTrackPwdKey];
661 MMVimController *vc = [self keyVimController];
662 if (vc) dir = [[vc vimState] objectForKey:@"pwd"];
665 NSOpenPanel *panel = [NSOpenPanel openPanel];
666 [panel setAllowsMultipleSelection:YES];
667 int result = [panel runModalForDirectory:dir file:nil types:nil];
668 if (NSOKButton == result)
669 [self application:NSApp openFiles:[panel filenames]];
672 - (IBAction)selectNextWindow:(id)sender
674 unsigned i, count = [vimControllers count];
677 NSWindow *keyWindow = [NSApp keyWindow];
678 for (i = 0; i < count; ++i) {
679 MMVimController *vc = [vimControllers objectAtIndex:i];
680 if ([[[vc windowController] window] isEqual:keyWindow])
687 MMVimController *vc = [vimControllers objectAtIndex:i];
688 [[vc windowController] showWindow:self];
692 - (IBAction)selectPreviousWindow:(id)sender
694 unsigned i, count = [vimControllers count];
697 NSWindow *keyWindow = [NSApp keyWindow];
698 for (i = 0; i < count; ++i) {
699 MMVimController *vc = [vimControllers objectAtIndex:i];
700 if ([[[vc windowController] window] isEqual:keyWindow])
710 MMVimController *vc = [vimControllers objectAtIndex:i];
711 [[vc windowController] showWindow:self];
715 - (IBAction)fontSizeUp:(id)sender
717 [[NSFontManager sharedFontManager] modifyFont:
718 [NSNumber numberWithInt:NSSizeUpFontAction]];
721 - (IBAction)fontSizeDown:(id)sender
723 [[NSFontManager sharedFontManager] modifyFont:
724 [NSNumber numberWithInt:NSSizeDownFontAction]];
727 - (IBAction)orderFrontPreferencePanel:(id)sender
729 [[MMPreferenceController sharedPrefsWindowController] showWindow:self];
732 - (IBAction)openWebsite:(id)sender
734 [[NSWorkspace sharedWorkspace] openURL:
735 [NSURL URLWithString:MMWebsiteString]];
738 - (IBAction)showVimHelp:(id)sender
740 // Open a new window with the help window maximized.
741 [self launchVimProcessWithArguments:[NSArray arrayWithObjects:
742 @"-c", @":h gui_mac", @"-c", @":res", nil]];
745 - (IBAction)zoomAll:(id)sender
747 [NSApp makeWindowsPerform:@selector(performZoom:) inOrder:YES];
750 - (byref id <MMFrontendProtocol>)
751 connectBackend:(byref in id <MMBackendProtocol>)backend
754 //NSLog(@"Connect backend (pid=%d)", pid);
755 NSNumber *pidKey = [NSNumber numberWithInt:pid];
756 MMVimController *vc = nil;
759 [(NSDistantObject*)backend
760 setProtocolForProxy:@protocol(MMBackendProtocol)];
762 vc = [[[MMVimController alloc]
763 initWithBackend:backend pid:pid]
766 if (![vimControllers count]) {
767 // The first window autosaves its position. (The autosaving
768 // features of Cocoa are not used because we need more control over
769 // what is autosaved and when it is restored.)
770 [[vc windowController] setWindowAutosaveKey:MMTopLeftPointKey];
773 [vimControllers addObject:vc];
775 id args = [pidArguments objectForKey:pidKey];
776 if (args && [NSNull null] != args)
777 [self passArguments:args toVimController:vc];
779 // HACK! MacVim does not get activated if it is launched from the
780 // terminal, so we forcibly activate here unless it is an untitled
781 // window opening. Untitled windows are treated differently, else
782 // MacVim would steal the focus if another app was activated while the
783 // untitled window was loading.
784 if (!args || args != [NSNull null])
785 [NSApp activateIgnoringOtherApps:YES];
788 [pidArguments removeObjectForKey:pidKey];
793 @catch (NSException *e) {
794 NSLog(@"Exception caught in %s: \"%@\"", _cmd, e);
797 [vimControllers removeObject:vc];
799 [pidArguments removeObjectForKey:pidKey];
805 - (NSArray *)serverList
807 NSMutableArray *array = [NSMutableArray array];
809 unsigned i, count = [vimControllers count];
810 for (i = 0; i < count; ++i) {
811 MMVimController *controller = [vimControllers objectAtIndex:i];
812 if ([controller serverName])
813 [array addObject:[controller serverName]];
819 @end // MMAppController
824 @implementation MMAppController (MMServices)
826 - (void)openSelection:(NSPasteboard *)pboard userData:(NSString *)userData
827 error:(NSString **)error
829 if (![[pboard types] containsObject:NSStringPboardType]) {
830 NSLog(@"WARNING: Pasteboard contains no object of type "
831 "NSStringPboardType");
835 MMVimController *vc = [self topmostVimController];
837 // Open a new tab first, since dropString: does not do this.
838 [vc sendMessage:AddNewTabMsgID data:nil];
839 [vc dropString:[pboard stringForType:NSStringPboardType]];
841 // NOTE: There is no window to paste the selection into, so save the
842 // text, open a new window, and paste the text when the next window
843 // opens. (If this is called several times in a row, then all but the
844 // last call might be ignored.)
845 if (openSelectionString) [openSelectionString release];
846 openSelectionString = [[pboard stringForType:NSStringPboardType] copy];
848 [self newWindow:self];
852 - (void)openFile:(NSPasteboard *)pboard userData:(NSString *)userData
853 error:(NSString **)error
855 if (![[pboard types] containsObject:NSStringPboardType]) {
856 NSLog(@"WARNING: Pasteboard contains no object of type "
857 "NSStringPboardType");
861 // TODO: Parse multiple filenames and create array with names.
862 NSString *string = [pboard stringForType:NSStringPboardType];
863 string = [string stringByTrimmingCharactersInSet:
864 [NSCharacterSet whitespaceAndNewlineCharacterSet]];
865 string = [string stringByStandardizingPath];
867 NSArray *filenames = [self filterFilesAndNotify:
868 [NSArray arrayWithObject:string]];
869 if ([filenames count] > 0) {
870 MMVimController *vc = nil;
871 if (userData && [userData isEqual:@"Tab"])
872 vc = [self topmostVimController];
875 [vc dropFiles:filenames forceOpen:YES];
877 [self application:NSApp openFiles:filenames];
882 @end // MMAppController (MMServices)
887 @implementation MMAppController (Private)
889 - (MMVimController *)keyVimController
891 NSWindow *keyWindow = [NSApp keyWindow];
893 unsigned i, count = [vimControllers count];
894 for (i = 0; i < count; ++i) {
895 MMVimController *vc = [vimControllers objectAtIndex:i];
896 if ([[[vc windowController] window] isEqual:keyWindow])
904 - (MMVimController *)topmostVimController
906 // Find the topmost visible window which has an associated vim controller.
907 NSEnumerator *e = [[NSApp orderedWindows] objectEnumerator];
909 while ((window = [e nextObject]) && [window isVisible]) {
910 unsigned i, count = [vimControllers count];
911 for (i = 0; i < count; ++i) {
912 MMVimController *vc = [vimControllers objectAtIndex:i];
913 if ([[[vc windowController] window] isEqual:window])
921 - (int)launchVimProcessWithArguments:(NSArray *)args
924 NSString *path = [[NSBundle mainBundle] pathForAuxiliaryExecutable:@"Vim"];
927 NSLog(@"ERROR: Vim executable could not be found inside app bundle!");
931 NSArray *taskArgs = [NSArray arrayWithObjects:@"-g", @"-f", nil];
933 taskArgs = [taskArgs arrayByAddingObjectsFromArray:args];
935 BOOL useLoginShell = [[NSUserDefaults standardUserDefaults]
936 boolForKey:MMLoginShellKey];
938 // Run process with a login shell, roughly:
939 // echo "exec Vim -g -f args" | ARGV0=-`basename $SHELL` $SHELL [-l]
940 pid = executeInLoginShell(path, taskArgs);
942 // Run process directly:
944 NSTask *task = [NSTask launchedTaskWithLaunchPath:path
946 pid = task ? [task processIdentifier] : -1;
950 // NOTE: If the process has no arguments, then add a null argument to
951 // the pidArguments dictionary. This is later used to detect that a
952 // process without arguments is being launched.
954 [pidArguments setObject:[NSNull null]
955 forKey:[NSNumber numberWithInt:pid]];
957 NSLog(@"WARNING: %s%@ failed (useLoginShell=%d)", _cmd, args,
964 - (NSArray *)filterFilesAndNotify:(NSArray *)filenames
966 // Go trough 'filenames' array and make sure each file exists. Present
967 // warning dialog if some file was missing.
969 NSString *firstMissingFile = nil;
970 NSMutableArray *files = [NSMutableArray array];
971 unsigned i, count = [filenames count];
973 for (i = 0; i < count; ++i) {
974 NSString *name = [filenames objectAtIndex:i];
975 if ([[NSFileManager defaultManager] fileExistsAtPath:name]) {
976 [files addObject:name];
977 } else if (!firstMissingFile) {
978 firstMissingFile = name;
982 if (firstMissingFile) {
983 NSAlert *alert = [[NSAlert alloc] init];
984 [alert addButtonWithTitle:NSLocalizedString(@"OK",
988 if ([files count] >= count-1) {
989 [alert setMessageText:NSLocalizedString(@"File not found",
990 @"File not found dialog, title")];
991 text = [NSString stringWithFormat:NSLocalizedString(
992 @"Could not open file with name %@.",
993 @"File not found dialog, text"), firstMissingFile];
995 [alert setMessageText:NSLocalizedString(@"Multiple files not found",
996 @"File not found dialog, title")];
997 text = [NSString stringWithFormat:NSLocalizedString(
998 @"Could not open file with name %@, and %d other files.",
999 @"File not found dialog, text"),
1000 firstMissingFile, count-[files count]-1];
1003 [alert setInformativeText:text];
1004 [alert setAlertStyle:NSWarningAlertStyle];
1009 [NSApp replyToOpenOrPrint:NSApplicationDelegateReplyFailure];
1015 - (NSArray *)filterOpenFiles:(NSArray *)filenames
1016 arguments:(NSDictionary *)args
1018 // Check if any of the files in the 'filenames' array are open in any Vim
1019 // process. Remove the files that are open from the 'filenames' array and
1020 // return it. If all files were filtered out, then raise the first file in
1021 // the Vim process it is open. Files that are filtered are sent an odb
1022 // open event in case theID is not zero.
1024 NSMutableDictionary *localArgs =
1025 [NSMutableDictionary dictionaryWithDictionary:args];
1026 MMVimController *raiseController = nil;
1027 NSString *raiseFile = nil;
1028 NSMutableArray *files = [filenames mutableCopy];
1029 NSString *expr = [NSString stringWithFormat:
1030 @"map([\"%@\"],\"bufloaded(v:val)\")",
1031 [files componentsJoinedByString:@"\",\""]];
1032 unsigned i, count = [vimControllers count];
1034 // Ensure that the files aren't opened when passing arguments.
1035 [localArgs setObject:[NSNumber numberWithBool:NO] forKey:@"openFiles"];
1037 for (i = 0; i < count && [files count]; ++i) {
1038 MMVimController *controller = [vimControllers objectAtIndex:i];
1040 // Query Vim for which files in the 'files' array are open.
1041 NSString *eval = [controller evaluateVimExpression:expr];
1042 if (!eval) continue;
1044 NSIndexSet *idxSet = [NSIndexSet indexSetWithVimList:eval];
1045 if ([idxSet count]) {
1047 // Remember the file and which Vim that has it open so that
1048 // we can raise it later on.
1049 raiseController = controller;
1050 raiseFile = [files objectAtIndex:[idxSet firstIndex]];
1051 [[raiseFile retain] autorelease];
1054 // Pass (ODB/Xcode/Spotlight) arguments to this process.
1055 [localArgs setObject:[files objectsAtIndexes:idxSet]
1056 forKey:@"filenames"];
1057 [self passArguments:localArgs toVimController:controller];
1059 // Remove all the files that were open in this Vim process and
1060 // create a new expression to evaluate.
1061 [files removeObjectsAtIndexes:idxSet];
1062 expr = [NSString stringWithFormat:
1063 @"map([\"%@\"],\"bufloaded(v:val)\")",
1064 [files componentsJoinedByString:@"\",\""]];
1068 if (![files count] && raiseFile) {
1069 // Raise the window containing the first file that was already open,
1070 // and make sure that the tab containing that file is selected. Only
1071 // do this if there are no more files to open, otherwise sometimes the
1072 // window with 'raiseFile' will be raised, other times it might be the
1073 // window that will open with the files in the 'files' array.
1074 raiseFile = [raiseFile stringByEscapingSpecialFilenameCharacters];
1075 NSString *input = [NSString stringWithFormat:@"<C-\\><C-N>"
1076 ":let oldswb=&swb|let &swb=\"useopen,usetab\"|"
1077 "tab sb %@|let &swb=oldswb|unl oldswb|"
1078 "cal foreground()|redr|f<CR>", raiseFile];
1080 [raiseController addVimInput:input];
1086 #if MM_HANDLE_XCODE_MOD_EVENT
1087 - (void)handleXcodeModEvent:(NSAppleEventDescriptor *)event
1088 replyEvent:(NSAppleEventDescriptor *)reply
1091 // Xcode sends this event to query MacVim which open files have been
1093 NSLog(@"reply:%@", reply);
1094 NSLog(@"event:%@", event);
1096 NSEnumerator *e = [vimControllers objectEnumerator];
1098 while ((vc = [e nextObject])) {
1099 DescType type = [reply descriptorType];
1100 unsigned len = [[type data] length];
1101 NSMutableData *data = [NSMutableData data];
1103 [data appendBytes:&type length:sizeof(DescType)];
1104 [data appendBytes:&len length:sizeof(unsigned)];
1105 [data appendBytes:[reply data] length:len];
1107 [vc sendMessage:XcodeModMsgID data:data];
1113 - (int)findLaunchingProcessWithoutArguments
1115 NSArray *keys = [pidArguments allKeysForObject:[NSNull null]];
1116 if ([keys count] > 0) {
1117 //NSLog(@"found launching process without arguments");
1118 return [[keys objectAtIndex:0] intValue];
1124 - (MMVimController *)findUntitledWindow
1126 NSEnumerator *e = [vimControllers objectEnumerator];
1128 while ((vc = [e nextObject])) {
1129 // TODO: This is a moronic test...should query the Vim process if there
1130 // are any open buffers or something like that instead.
1131 NSString *title = [[[vc windowController] window] title];
1133 // TODO: this will not work in a localized MacVim
1134 if ([title hasPrefix:@"[No Name] - VIM"]) {
1135 //NSLog(@"found untitled window");
1143 - (NSMutableDictionary *)extractArgumentsFromOdocEvent:
1144 (NSAppleEventDescriptor *)desc
1146 NSMutableDictionary *dict = [NSMutableDictionary dictionary];
1148 // 1. Extract ODB parameters (if any)
1149 NSAppleEventDescriptor *odbdesc = desc;
1150 if (![odbdesc paramDescriptorForKeyword:keyFileSender]) {
1151 // The ODB paramaters may hide inside the 'keyAEPropData' descriptor.
1152 odbdesc = [odbdesc paramDescriptorForKeyword:keyAEPropData];
1153 if (![odbdesc paramDescriptorForKeyword:keyFileSender])
1158 NSAppleEventDescriptor *p =
1159 [odbdesc paramDescriptorForKeyword:keyFileSender];
1161 [dict setObject:[NSNumber numberWithUnsignedInt:[p typeCodeValue]]
1162 forKey:@"remoteID"];
1164 p = [odbdesc paramDescriptorForKeyword:keyFileCustomPath];
1166 [dict setObject:[p stringValue] forKey:@"remotePath"];
1168 p = [odbdesc paramDescriptorForKeyword:keyFileSenderToken];
1170 [dict setObject:p forKey:@"remotePath"];
1173 // 2. Extract Xcode parameters (if any)
1174 NSAppleEventDescriptor *xcodedesc =
1175 [desc paramDescriptorForKeyword:keyAEPosition];
1178 MMSelectionRange *sr = (MMSelectionRange*)[[xcodedesc data] bytes];
1180 if (sr->lineNum < 0) {
1181 // Should select a range of lines.
1182 range.location = sr->startRange + 1;
1183 range.length = sr->endRange - sr->startRange + 1;
1185 // Should only move cursor to a line.
1186 range.location = sr->lineNum + 1;
1190 [dict setObject:NSStringFromRange(range) forKey:@"selectionRange"];
1193 // 3. Extract Spotlight search text (if any)
1194 NSAppleEventDescriptor *spotlightdesc =
1195 [desc paramDescriptorForKeyword:keyAESearchText];
1197 [dict setObject:[spotlightdesc stringValue] forKey:@"searchText"];
1202 - (void)passArguments:(NSDictionary *)args toVimController:(MMVimController*)vc
1206 // Pass filenames to open if required (the 'openFiles' argument can be used
1207 // to disallow opening of the files).
1208 NSArray *filenames = [args objectForKey:@"filenames"];
1209 if (filenames && [[args objectForKey:@"openFiles"] boolValue]) {
1210 NSString *tabDrop = buildTabDropCommand(filenames);
1211 [vc addVimInput:tabDrop];
1215 if (filenames && [args objectForKey:@"remoteID"]) {
1216 [vc odbEdit:filenames
1217 server:[[args objectForKey:@"remoteID"] unsignedIntValue]
1218 path:[args objectForKey:@"remotePath"]
1219 token:[args objectForKey:@"remoteToken"]];
1222 // Pass range of lines to select
1223 if ([args objectForKey:@"selectionRange"]) {
1224 NSRange selectionRange = NSRangeFromString(
1225 [args objectForKey:@"selectionRange"]);
1226 [vc addVimInput:buildSelectRangeCommand(selectionRange)];
1230 NSString *searchText = [args objectForKey:@"searchText"];
1232 [vc addVimInput:buildSearchTextCommand(searchText)];
1235 @end // MMAppController (Private)
1240 @implementation NSNumber (MMExtras)
1243 return [self intValue];
1245 @end // NSNumber (MMExtras)
1250 @implementation NSMenu (MMExtras)
1252 - (int)indexOfItemWithAction:(SEL)action
1254 int i, count = [self numberOfItems];
1255 for (i = 0; i < count; ++i) {
1256 NSMenuItem *item = [self itemAtIndex:i];
1257 if ([item action] == action)
1264 - (NSMenuItem *)itemWithAction:(SEL)action
1266 int idx = [self indexOfItemWithAction:action];
1267 return idx >= 0 ? [self itemAtIndex:idx] : nil;
1270 - (NSMenu *)findMenuContainingItemWithAction:(SEL)action
1272 // NOTE: We only look for the action in the submenus of 'self'
1273 int i, count = [self numberOfItems];
1274 for (i = 0; i < count; ++i) {
1275 NSMenu *menu = [[self itemAtIndex:i] submenu];
1276 NSMenuItem *item = [menu itemWithAction:action];
1277 if (item) return menu;
1283 - (NSMenu *)findWindowsMenu
1285 return [self findMenuContainingItemWithAction:
1286 @selector(performMiniaturize:)];
1289 - (NSMenu *)findApplicationMenu
1291 // TODO: Just return [self itemAtIndex:0]?
1292 return [self findMenuContainingItemWithAction:@selector(terminate:)];
1295 - (NSMenu *)findServicesMenu
1297 // NOTE! Our heuristic for finding the "Services" menu is to look for the
1298 // second item before the "Hide MacVim" menu item on the "MacVim" menu.
1299 // (The item before "Hide MacVim" should be a separator, but this is not
1300 // important as long as the item before that is the "Services" menu.)
1302 NSMenu *appMenu = [self findApplicationMenu];
1303 if (!appMenu) return nil;
1305 int idx = [appMenu indexOfItemWithAction: @selector(hide:)];
1306 if (idx-2 < 0) return nil; // idx == -1, if selector not found
1308 return [[appMenu itemAtIndex:idx-2] submenu];
1311 - (NSMenu *)findFileMenu
1313 return [self findMenuContainingItemWithAction:@selector(performClose:)];
1316 @end // NSMenu (MMExtras)
1322 executeInLoginShell(NSString *path, NSArray *args)
1324 // Start a login shell and execute the command 'path' with arguments 'args'
1325 // in the shell. This ensures that user environment variables are set even
1326 // when MacVim was started from the Finder.
1329 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
1331 // Determine which shell to use to execute the command. The user
1332 // may decide which shell to use by setting a user default or the
1333 // $SHELL environment variable.
1334 NSString *shell = [ud stringForKey:MMLoginShellCommandKey];
1335 if (!shell || [shell length] == 0)
1336 shell = [[[NSProcessInfo processInfo] environment]
1337 objectForKey:@"SHELL"];
1339 shell = @"/bin/bash";
1341 //NSLog(@"shell = %@", shell);
1343 // Bash needs the '-l' flag to launch a login shell. The user may add
1344 // flags by setting a user default.
1345 NSString *shellArgument = [ud stringForKey:MMLoginShellArgumentKey];
1346 if (!shellArgument || [shellArgument length] == 0) {
1347 if ([[shell lastPathComponent] isEqual:@"bash"])
1348 shellArgument = @"-l";
1350 shellArgument = nil;
1353 //NSLog(@"shellArgument = %@", shellArgument);
1355 // Build input string to pipe to the login shell.
1356 NSMutableString *input = [NSMutableString stringWithFormat:
1357 @"exec \"%@\"", path];
1359 // Append all arguments, making sure they are properly quoted, even
1360 // when they contain single quotes.
1361 NSEnumerator *e = [args objectEnumerator];
1364 while ((obj = [e nextObject])) {
1365 NSMutableString *arg = [NSMutableString stringWithString:obj];
1366 [arg replaceOccurrencesOfString:@"'" withString:@"'\"'\"'"
1367 options:NSLiteralSearch
1368 range:NSMakeRange(0, [arg length])];
1369 [input appendFormat:@" '%@'", arg];
1373 // Build the argument vector used to start the login shell.
1374 NSString *shellArg0 = [NSString stringWithFormat:@"-%@",
1375 [shell lastPathComponent]];
1376 char *shellArgv[3] = { (char *)[shellArg0 UTF8String], NULL, NULL };
1378 shellArgv[1] = (char *)[shellArgument UTF8String];
1380 // Get the C string representation of the shell path before the fork since
1381 // we must not call Foundation functions after a fork.
1382 const char *shellPath = [shell fileSystemRepresentation];
1384 // Fork and execute the process.
1386 if (pipe(ds)) return -1;
1391 } else if (pid == 0) {
1393 if (close(ds[1]) == -1) exit(255);
1394 if (dup2(ds[0], 0) == -1) exit(255);
1396 execv(shellPath, shellArgv);
1398 // Never reached unless execv fails
1402 if (close(ds[0]) == -1) return -1;
1404 // Send input to execute to the child process
1405 [input appendString:@"\n"];
1406 int bytes = [input lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
1408 if (write(ds[1], [input UTF8String], bytes) != bytes) return -1;
1409 if (close(ds[1]) == -1) return -1;