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];
651 - (IBAction)newWindow:(id)sender
653 [self launchVimProcessWithArguments:nil];
656 - (IBAction)fileOpen:(id)sender
659 BOOL trackPwd = [[NSUserDefaults standardUserDefaults]
660 boolForKey:MMDialogsTrackPwdKey];
662 MMVimController *vc = [self keyVimController];
663 if (vc) dir = [[vc vimState] objectForKey:@"pwd"];
666 NSOpenPanel *panel = [NSOpenPanel openPanel];
667 [panel setAllowsMultipleSelection:YES];
668 int result = [panel runModalForDirectory:dir file:nil types:nil];
669 if (NSOKButton == result)
670 [self application:NSApp openFiles:[panel filenames]];
673 - (IBAction)selectNextWindow:(id)sender
675 unsigned i, count = [vimControllers count];
678 NSWindow *keyWindow = [NSApp keyWindow];
679 for (i = 0; i < count; ++i) {
680 MMVimController *vc = [vimControllers objectAtIndex:i];
681 if ([[[vc windowController] window] isEqual:keyWindow])
688 MMVimController *vc = [vimControllers objectAtIndex:i];
689 [[vc windowController] showWindow:self];
693 - (IBAction)selectPreviousWindow:(id)sender
695 unsigned i, count = [vimControllers count];
698 NSWindow *keyWindow = [NSApp keyWindow];
699 for (i = 0; i < count; ++i) {
700 MMVimController *vc = [vimControllers objectAtIndex:i];
701 if ([[[vc windowController] window] isEqual:keyWindow])
711 MMVimController *vc = [vimControllers objectAtIndex:i];
712 [[vc windowController] showWindow:self];
716 - (IBAction)fontSizeUp:(id)sender
718 [[NSFontManager sharedFontManager] modifyFont:
719 [NSNumber numberWithInt:NSSizeUpFontAction]];
722 - (IBAction)fontSizeDown:(id)sender
724 [[NSFontManager sharedFontManager] modifyFont:
725 [NSNumber numberWithInt:NSSizeDownFontAction]];
728 - (IBAction)orderFrontPreferencePanel:(id)sender
730 [[MMPreferenceController sharedPrefsWindowController] showWindow:self];
733 - (IBAction)openWebsite:(id)sender
735 [[NSWorkspace sharedWorkspace] openURL:
736 [NSURL URLWithString:MMWebsiteString]];
739 - (IBAction)showVimHelp:(id)sender
741 // Open a new window with the help window maximized.
742 [self launchVimProcessWithArguments:[NSArray arrayWithObjects:
743 @"-c", @":h gui_mac", @"-c", @":res", nil]];
746 - (IBAction)zoomAll:(id)sender
748 [NSApp makeWindowsPerform:@selector(performZoom:) inOrder:YES];
751 - (byref id <MMFrontendProtocol>)
752 connectBackend:(byref in id <MMBackendProtocol>)backend
755 //NSLog(@"Connect backend (pid=%d)", pid);
756 NSNumber *pidKey = [NSNumber numberWithInt:pid];
757 MMVimController *vc = nil;
760 [(NSDistantObject*)backend
761 setProtocolForProxy:@protocol(MMBackendProtocol)];
763 vc = [[[MMVimController alloc]
764 initWithBackend:backend pid:pid]
767 if (![vimControllers count]) {
768 // The first window autosaves its position. (The autosaving
769 // features of Cocoa are not used because we need more control over
770 // what is autosaved and when it is restored.)
771 [[vc windowController] setWindowAutosaveKey:MMTopLeftPointKey];
774 [vimControllers addObject:vc];
776 id args = [pidArguments objectForKey:pidKey];
777 if (args && [NSNull null] != args)
778 [self passArguments:args toVimController:vc];
780 // HACK! MacVim does not get activated if it is launched from the
781 // terminal, so we forcibly activate here unless it is an untitled
782 // window opening. Untitled windows are treated differently, else
783 // MacVim would steal the focus if another app was activated while the
784 // untitled window was loading.
785 if (!args || args != [NSNull null])
786 [NSApp activateIgnoringOtherApps:YES];
789 [pidArguments removeObjectForKey:pidKey];
794 @catch (NSException *e) {
795 NSLog(@"Exception caught in %s: \"%@\"", _cmd, e);
798 [vimControllers removeObject:vc];
800 [pidArguments removeObjectForKey:pidKey];
806 - (NSArray *)serverList
808 NSMutableArray *array = [NSMutableArray array];
810 unsigned i, count = [vimControllers count];
811 for (i = 0; i < count; ++i) {
812 MMVimController *controller = [vimControllers objectAtIndex:i];
813 if ([controller serverName])
814 [array addObject:[controller serverName]];
820 @end // MMAppController
825 @implementation MMAppController (MMServices)
827 - (void)openSelection:(NSPasteboard *)pboard userData:(NSString *)userData
828 error:(NSString **)error
830 if (![[pboard types] containsObject:NSStringPboardType]) {
831 NSLog(@"WARNING: Pasteboard contains no object of type "
832 "NSStringPboardType");
836 MMVimController *vc = [self topmostVimController];
838 // Open a new tab first, since dropString: does not do this.
839 [vc sendMessage:AddNewTabMsgID data:nil];
840 [vc dropString:[pboard stringForType:NSStringPboardType]];
842 // NOTE: There is no window to paste the selection into, so save the
843 // text, open a new window, and paste the text when the next window
844 // opens. (If this is called several times in a row, then all but the
845 // last call might be ignored.)
846 if (openSelectionString) [openSelectionString release];
847 openSelectionString = [[pboard stringForType:NSStringPboardType] copy];
849 [self newWindow:self];
853 - (void)openFile:(NSPasteboard *)pboard userData:(NSString *)userData
854 error:(NSString **)error
856 if (![[pboard types] containsObject:NSStringPboardType]) {
857 NSLog(@"WARNING: Pasteboard contains no object of type "
858 "NSStringPboardType");
862 // TODO: Parse multiple filenames and create array with names.
863 NSString *string = [pboard stringForType:NSStringPboardType];
864 string = [string stringByTrimmingCharactersInSet:
865 [NSCharacterSet whitespaceAndNewlineCharacterSet]];
866 string = [string stringByStandardizingPath];
868 NSArray *filenames = [self filterFilesAndNotify:
869 [NSArray arrayWithObject:string]];
870 if ([filenames count] > 0) {
871 MMVimController *vc = nil;
872 if (userData && [userData isEqual:@"Tab"])
873 vc = [self topmostVimController];
876 [vc dropFiles:filenames forceOpen:YES];
878 [self application:NSApp openFiles:filenames];
883 @end // MMAppController (MMServices)
888 @implementation MMAppController (Private)
890 - (MMVimController *)keyVimController
892 NSWindow *keyWindow = [NSApp keyWindow];
894 unsigned i, count = [vimControllers count];
895 for (i = 0; i < count; ++i) {
896 MMVimController *vc = [vimControllers objectAtIndex:i];
897 if ([[[vc windowController] window] isEqual:keyWindow])
905 - (MMVimController *)topmostVimController
907 // Find the topmost visible window which has an associated vim controller.
908 NSEnumerator *e = [[NSApp orderedWindows] objectEnumerator];
910 while ((window = [e nextObject]) && [window isVisible]) {
911 unsigned i, count = [vimControllers count];
912 for (i = 0; i < count; ++i) {
913 MMVimController *vc = [vimControllers objectAtIndex:i];
914 if ([[[vc windowController] window] isEqual:window])
922 - (int)launchVimProcessWithArguments:(NSArray *)args
925 NSString *path = [[NSBundle mainBundle] pathForAuxiliaryExecutable:@"Vim"];
928 NSLog(@"ERROR: Vim executable could not be found inside app bundle!");
932 NSArray *taskArgs = [NSArray arrayWithObjects:@"-g", @"-f", nil];
934 taskArgs = [taskArgs arrayByAddingObjectsFromArray:args];
936 BOOL useLoginShell = [[NSUserDefaults standardUserDefaults]
937 boolForKey:MMLoginShellKey];
939 // Run process with a login shell, roughly:
940 // echo "exec Vim -g -f args" | ARGV0=-`basename $SHELL` $SHELL [-l]
941 pid = executeInLoginShell(path, taskArgs);
943 // Run process directly:
945 NSTask *task = [NSTask launchedTaskWithLaunchPath:path
947 pid = task ? [task processIdentifier] : -1;
951 // NOTE: If the process has no arguments, then add a null argument to
952 // the pidArguments dictionary. This is later used to detect that a
953 // process without arguments is being launched.
955 [pidArguments setObject:[NSNull null]
956 forKey:[NSNumber numberWithInt:pid]];
958 NSLog(@"WARNING: %s%@ failed (useLoginShell=%d)", _cmd, args,
965 - (NSArray *)filterFilesAndNotify:(NSArray *)filenames
967 // Go trough 'filenames' array and make sure each file exists. Present
968 // warning dialog if some file was missing.
970 NSString *firstMissingFile = nil;
971 NSMutableArray *files = [NSMutableArray array];
972 unsigned i, count = [filenames count];
974 for (i = 0; i < count; ++i) {
975 NSString *name = [filenames objectAtIndex:i];
976 if ([[NSFileManager defaultManager] fileExistsAtPath:name]) {
977 [files addObject:name];
978 } else if (!firstMissingFile) {
979 firstMissingFile = name;
983 if (firstMissingFile) {
984 NSAlert *alert = [[NSAlert alloc] init];
985 [alert addButtonWithTitle:NSLocalizedString(@"OK",
989 if ([files count] >= count-1) {
990 [alert setMessageText:NSLocalizedString(@"File not found",
991 @"File not found dialog, title")];
992 text = [NSString stringWithFormat:NSLocalizedString(
993 @"Could not open file with name %@.",
994 @"File not found dialog, text"), firstMissingFile];
996 [alert setMessageText:NSLocalizedString(@"Multiple files not found",
997 @"File not found dialog, title")];
998 text = [NSString stringWithFormat:NSLocalizedString(
999 @"Could not open file with name %@, and %d other files.",
1000 @"File not found dialog, text"),
1001 firstMissingFile, count-[files count]-1];
1004 [alert setInformativeText:text];
1005 [alert setAlertStyle:NSWarningAlertStyle];
1010 [NSApp replyToOpenOrPrint:NSApplicationDelegateReplyFailure];
1016 - (NSArray *)filterOpenFiles:(NSArray *)filenames
1017 arguments:(NSDictionary *)args
1019 // Check if any of the files in the 'filenames' array are open in any Vim
1020 // process. Remove the files that are open from the 'filenames' array and
1021 // return it. If all files were filtered out, then raise the first file in
1022 // the Vim process it is open. Files that are filtered are sent an odb
1023 // open event in case theID is not zero.
1025 NSMutableDictionary *localArgs =
1026 [NSMutableDictionary dictionaryWithDictionary:args];
1027 MMVimController *raiseController = nil;
1028 NSString *raiseFile = nil;
1029 NSMutableArray *files = [filenames mutableCopy];
1030 NSString *expr = [NSString stringWithFormat:
1031 @"map([\"%@\"],\"bufloaded(v:val)\")",
1032 [files componentsJoinedByString:@"\",\""]];
1033 unsigned i, count = [vimControllers count];
1035 // Ensure that the files aren't opened when passing arguments.
1036 [localArgs setObject:[NSNumber numberWithBool:NO] forKey:@"openFiles"];
1038 for (i = 0; i < count && [files count]; ++i) {
1039 MMVimController *controller = [vimControllers objectAtIndex:i];
1041 // Query Vim for which files in the 'files' array are open.
1042 NSString *eval = [controller evaluateVimExpression:expr];
1043 if (!eval) continue;
1045 NSIndexSet *idxSet = [NSIndexSet indexSetWithVimList:eval];
1046 if ([idxSet count]) {
1048 // Remember the file and which Vim that has it open so that
1049 // we can raise it later on.
1050 raiseController = controller;
1051 raiseFile = [files objectAtIndex:[idxSet firstIndex]];
1052 [[raiseFile retain] autorelease];
1055 // Pass (ODB/Xcode/Spotlight) arguments to this process.
1056 [localArgs setObject:[files objectsAtIndexes:idxSet]
1057 forKey:@"filenames"];
1058 [self passArguments:localArgs toVimController:controller];
1060 // Remove all the files that were open in this Vim process and
1061 // create a new expression to evaluate.
1062 [files removeObjectsAtIndexes:idxSet];
1063 expr = [NSString stringWithFormat:
1064 @"map([\"%@\"],\"bufloaded(v:val)\")",
1065 [files componentsJoinedByString:@"\",\""]];
1069 if (![files count] && raiseFile) {
1070 // Raise the window containing the first file that was already open,
1071 // and make sure that the tab containing that file is selected. Only
1072 // do this if there are no more files to open, otherwise sometimes the
1073 // window with 'raiseFile' will be raised, other times it might be the
1074 // window that will open with the files in the 'files' array.
1075 raiseFile = [raiseFile stringByEscapingSpecialFilenameCharacters];
1076 NSString *input = [NSString stringWithFormat:@"<C-\\><C-N>"
1077 ":let oldswb=&swb|let &swb=\"useopen,usetab\"|"
1078 "tab sb %@|let &swb=oldswb|unl oldswb|"
1079 "cal foreground()|redr|f<CR>", raiseFile];
1081 [raiseController addVimInput:input];
1087 #if MM_HANDLE_XCODE_MOD_EVENT
1088 - (void)handleXcodeModEvent:(NSAppleEventDescriptor *)event
1089 replyEvent:(NSAppleEventDescriptor *)reply
1092 // Xcode sends this event to query MacVim which open files have been
1094 NSLog(@"reply:%@", reply);
1095 NSLog(@"event:%@", event);
1097 NSEnumerator *e = [vimControllers objectEnumerator];
1099 while ((vc = [e nextObject])) {
1100 DescType type = [reply descriptorType];
1101 unsigned len = [[type data] length];
1102 NSMutableData *data = [NSMutableData data];
1104 [data appendBytes:&type length:sizeof(DescType)];
1105 [data appendBytes:&len length:sizeof(unsigned)];
1106 [data appendBytes:[reply data] length:len];
1108 [vc sendMessage:XcodeModMsgID data:data];
1114 - (int)findLaunchingProcessWithoutArguments
1116 NSArray *keys = [pidArguments allKeysForObject:[NSNull null]];
1117 if ([keys count] > 0) {
1118 //NSLog(@"found launching process without arguments");
1119 return [[keys objectAtIndex:0] intValue];
1125 - (MMVimController *)findUntitledWindow
1127 NSEnumerator *e = [vimControllers objectEnumerator];
1129 while ((vc = [e nextObject])) {
1130 // TODO: This is a moronic test...should query the Vim process if there
1131 // are any open buffers or something like that instead.
1132 NSString *title = [[[vc windowController] window] title];
1134 // TODO: this will not work in a localized MacVim
1135 if ([title hasPrefix:@"[No Name] - VIM"]) {
1136 //NSLog(@"found untitled window");
1144 - (NSMutableDictionary *)extractArgumentsFromOdocEvent:
1145 (NSAppleEventDescriptor *)desc
1147 NSMutableDictionary *dict = [NSMutableDictionary dictionary];
1149 // 1. Extract ODB parameters (if any)
1150 NSAppleEventDescriptor *odbdesc = desc;
1151 if (![odbdesc paramDescriptorForKeyword:keyFileSender]) {
1152 // The ODB paramaters may hide inside the 'keyAEPropData' descriptor.
1153 odbdesc = [odbdesc paramDescriptorForKeyword:keyAEPropData];
1154 if (![odbdesc paramDescriptorForKeyword:keyFileSender])
1159 NSAppleEventDescriptor *p =
1160 [odbdesc paramDescriptorForKeyword:keyFileSender];
1162 [dict setObject:[NSNumber numberWithUnsignedInt:[p typeCodeValue]]
1163 forKey:@"remoteID"];
1165 p = [odbdesc paramDescriptorForKeyword:keyFileCustomPath];
1167 [dict setObject:[p stringValue] forKey:@"remotePath"];
1169 p = [odbdesc paramDescriptorForKeyword:keyFileSenderToken];
1171 [dict setObject:p forKey:@"remotePath"];
1174 // 2. Extract Xcode parameters (if any)
1175 NSAppleEventDescriptor *xcodedesc =
1176 [desc paramDescriptorForKeyword:keyAEPosition];
1179 MMSelectionRange *sr = (MMSelectionRange*)[[xcodedesc data] bytes];
1181 if (sr->lineNum < 0) {
1182 // Should select a range of lines.
1183 range.location = sr->startRange + 1;
1184 range.length = sr->endRange - sr->startRange + 1;
1186 // Should only move cursor to a line.
1187 range.location = sr->lineNum + 1;
1191 [dict setObject:NSStringFromRange(range) forKey:@"selectionRange"];
1194 // 3. Extract Spotlight search text (if any)
1195 NSAppleEventDescriptor *spotlightdesc =
1196 [desc paramDescriptorForKeyword:keyAESearchText];
1198 [dict setObject:[spotlightdesc stringValue] forKey:@"searchText"];
1203 - (void)passArguments:(NSDictionary *)args toVimController:(MMVimController*)vc
1207 // Pass filenames to open if required (the 'openFiles' argument can be used
1208 // to disallow opening of the files).
1209 NSArray *filenames = [args objectForKey:@"filenames"];
1210 if (filenames && [[args objectForKey:@"openFiles"] boolValue]) {
1211 NSString *tabDrop = buildTabDropCommand(filenames);
1212 [vc addVimInput:tabDrop];
1216 if (filenames && [args objectForKey:@"remoteID"]) {
1217 [vc odbEdit:filenames
1218 server:[[args objectForKey:@"remoteID"] unsignedIntValue]
1219 path:[args objectForKey:@"remotePath"]
1220 token:[args objectForKey:@"remoteToken"]];
1223 // Pass range of lines to select
1224 if ([args objectForKey:@"selectionRange"]) {
1225 NSRange selectionRange = NSRangeFromString(
1226 [args objectForKey:@"selectionRange"]);
1227 [vc addVimInput:buildSelectRangeCommand(selectionRange)];
1231 NSString *searchText = [args objectForKey:@"searchText"];
1233 [vc addVimInput:buildSearchTextCommand(searchText)];
1236 @end // MMAppController (Private)
1241 @implementation NSNumber (MMExtras)
1244 return [self intValue];
1246 @end // NSNumber (MMExtras)
1251 @implementation NSMenu (MMExtras)
1253 - (int)indexOfItemWithAction:(SEL)action
1255 int i, count = [self numberOfItems];
1256 for (i = 0; i < count; ++i) {
1257 NSMenuItem *item = [self itemAtIndex:i];
1258 if ([item action] == action)
1265 - (NSMenuItem *)itemWithAction:(SEL)action
1267 int idx = [self indexOfItemWithAction:action];
1268 return idx >= 0 ? [self itemAtIndex:idx] : nil;
1271 - (NSMenu *)findMenuContainingItemWithAction:(SEL)action
1273 // NOTE: We only look for the action in the submenus of 'self'
1274 int i, count = [self numberOfItems];
1275 for (i = 0; i < count; ++i) {
1276 NSMenu *menu = [[self itemAtIndex:i] submenu];
1277 NSMenuItem *item = [menu itemWithAction:action];
1278 if (item) return menu;
1284 - (NSMenu *)findWindowsMenu
1286 return [self findMenuContainingItemWithAction:
1287 @selector(performMiniaturize:)];
1290 - (NSMenu *)findApplicationMenu
1292 // TODO: Just return [self itemAtIndex:0]?
1293 return [self findMenuContainingItemWithAction:@selector(terminate:)];
1296 - (NSMenu *)findServicesMenu
1298 // NOTE! Our heuristic for finding the "Services" menu is to look for the
1299 // second item before the "Hide MacVim" menu item on the "MacVim" menu.
1300 // (The item before "Hide MacVim" should be a separator, but this is not
1301 // important as long as the item before that is the "Services" menu.)
1303 NSMenu *appMenu = [self findApplicationMenu];
1304 if (!appMenu) return nil;
1306 int idx = [appMenu indexOfItemWithAction: @selector(hide:)];
1307 if (idx-2 < 0) return nil; // idx == -1, if selector not found
1309 return [[appMenu itemAtIndex:idx-2] submenu];
1312 - (NSMenu *)findFileMenu
1314 return [self findMenuContainingItemWithAction:@selector(performClose:)];
1317 @end // NSMenu (MMExtras)
1323 executeInLoginShell(NSString *path, NSArray *args)
1325 // Start a login shell and execute the command 'path' with arguments 'args'
1326 // in the shell. This ensures that user environment variables are set even
1327 // when MacVim was started from the Finder.
1330 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
1332 // Determine which shell to use to execute the command. The user
1333 // may decide which shell to use by setting a user default or the
1334 // $SHELL environment variable.
1335 NSString *shell = [ud stringForKey:MMLoginShellCommandKey];
1336 if (!shell || [shell length] == 0)
1337 shell = [[[NSProcessInfo processInfo] environment]
1338 objectForKey:@"SHELL"];
1340 shell = @"/bin/bash";
1342 //NSLog(@"shell = %@", shell);
1344 // Bash needs the '-l' flag to launch a login shell. The user may add
1345 // flags by setting a user default.
1346 NSString *shellArgument = [ud stringForKey:MMLoginShellArgumentKey];
1347 if (!shellArgument || [shellArgument length] == 0) {
1348 if ([[shell lastPathComponent] isEqual:@"bash"])
1349 shellArgument = @"-l";
1351 shellArgument = nil;
1354 //NSLog(@"shellArgument = %@", shellArgument);
1356 // Build input string to pipe to the login shell.
1357 NSMutableString *input = [NSMutableString stringWithFormat:
1358 @"exec \"%@\"", path];
1360 // Append all arguments, making sure they are properly quoted, even
1361 // when they contain single quotes.
1362 NSEnumerator *e = [args objectEnumerator];
1365 while ((obj = [e nextObject])) {
1366 NSMutableString *arg = [NSMutableString stringWithString:obj];
1367 [arg replaceOccurrencesOfString:@"'" withString:@"'\"'\"'"
1368 options:NSLiteralSearch
1369 range:NSMakeRange(0, [arg length])];
1370 [input appendFormat:@" '%@'", arg];
1374 // Build the argument vector used to start the login shell.
1375 NSString *shellArg0 = [NSString stringWithFormat:@"-%@",
1376 [shell lastPathComponent]];
1377 char *shellArgv[3] = { (char *)[shellArg0 UTF8String], NULL, NULL };
1379 shellArgv[1] = (char *)[shellArgument UTF8String];
1381 // Get the C string representation of the shell path before the fork since
1382 // we must not call Foundation functions after a fork.
1383 const char *shellPath = [shell fileSystemRepresentation];
1385 // Fork and execute the process.
1387 if (pipe(ds)) return -1;
1392 } else if (pid == 0) {
1394 if (close(ds[1]) == -1) exit(255);
1395 if (dup2(ds[0], 0) == -1) exit(255);
1397 execv(shellPath, shellArgv);
1399 // Never reached unless execv fails
1403 if (close(ds[0]) == -1) return -1;
1405 // Send input to execute to the child process
1406 [input appendString:@"\n"];
1407 int bytes = [input lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
1409 if (write(ds[1], [input UTF8String], bytes) != bytes) return -1;
1410 if (close(ds[1]) == -1) return -1;