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;
89 @interface NSMenu (MMExtras)
90 - (void)recurseSetAutoenablesItems:(BOOL)on;
93 @interface NSNumber (MMExtras)
99 @implementation MMAppController
103 NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:
104 [NSNumber numberWithBool:NO], MMNoWindowKey,
105 [NSNumber numberWithInt:64], MMTabMinWidthKey,
106 [NSNumber numberWithInt:6*64], MMTabMaxWidthKey,
107 [NSNumber numberWithInt:132], MMTabOptimumWidthKey,
108 [NSNumber numberWithInt:2], MMTextInsetLeftKey,
109 [NSNumber numberWithInt:1], MMTextInsetRightKey,
110 [NSNumber numberWithInt:1], MMTextInsetTopKey,
111 [NSNumber numberWithInt:1], MMTextInsetBottomKey,
112 [NSNumber numberWithBool:NO], MMTerminateAfterLastWindowClosedKey,
113 @"MMTypesetter", MMTypesetterKey,
114 [NSNumber numberWithFloat:1], MMCellWidthMultiplierKey,
115 [NSNumber numberWithFloat:-1], MMBaselineOffsetKey,
116 [NSNumber numberWithBool:YES], MMTranslateCtrlClickKey,
117 [NSNumber numberWithBool:NO], MMOpenFilesInTabsKey,
118 [NSNumber numberWithBool:NO], MMNoFontSubstitutionKey,
119 [NSNumber numberWithBool:NO], MMLoginShellKey,
120 [NSNumber numberWithBool:NO], MMAtsuiRendererKey,
121 [NSNumber numberWithInt:MMUntitledWindowAlways],
123 [NSNumber numberWithBool:NO], MMTexturedWindowKey,
124 [NSNumber numberWithBool:NO], MMZoomBothKey,
125 @"", MMLoginShellCommandKey,
126 @"", MMLoginShellArgumentKey,
127 [NSNumber numberWithBool:YES], MMDialogsTrackPwdKey,
130 [[NSUserDefaults standardUserDefaults] registerDefaults:dict];
132 NSArray *types = [NSArray arrayWithObject:NSStringPboardType];
133 [NSApp registerServicesMenuSendTypes:types returnTypes:types];
138 if ((self = [super init])) {
139 fontContainerRef = loadFonts();
141 vimControllers = [NSMutableArray new];
142 pidArguments = [NSMutableDictionary new];
144 // NOTE: Do not use the default connection since the Logitech Control
145 // Center (LCC) input manager steals and this would cause MacVim to
146 // never open any windows. (This is a bug in LCC but since they are
147 // unlikely to fix it, we graciously give them the default connection.)
148 connection = [[NSConnection alloc] initWithReceivePort:[NSPort port]
150 [connection setRootObject:self];
151 [connection setRequestTimeout:MMRequestTimeout];
152 [connection setReplyTimeout:MMReplyTimeout];
154 // NOTE: When the user is resizing the window the AppKit puts the run
155 // loop in event tracking mode. Unless the connection listens to
156 // request in this mode, live resizing won't work.
157 [connection addRequestMode:NSEventTrackingRunLoopMode];
159 // NOTE! If the name of the connection changes here it must also be
160 // updated in MMBackend.m.
161 NSString *name = [NSString stringWithFormat:@"%@-connection",
162 [[NSBundle mainBundle] bundleIdentifier]];
163 //NSLog(@"Registering connection with name '%@'", name);
164 if (![connection registerName:name]) {
165 NSLog(@"FATAL ERROR: Failed to register connection with name '%@'",
167 [connection release]; connection = nil;
176 //NSLog(@"MMAppController dealloc");
178 [connection release]; connection = nil;
179 [pidArguments release]; pidArguments = nil;
180 [vimControllers release]; vimControllers = nil;
181 [openSelectionString release]; openSelectionString = nil;
182 [recentFilesMenuItem release]; recentFilesMenuItem = nil;
187 - (void)applicationWillFinishLaunching:(NSNotification *)notification
189 // Create the "Open Recent" menu. See
190 // http://lapcatsoftware.com/blog/2007/07/10/working-without-a-nib-part-5-open-recent-menu/
191 // and http://www.cocoabuilder.com/archive/message/cocoa/2007/8/15/187793
192 // for more information.
194 // The menu needs to be created and be added to a toplevel menu in
195 // applicationWillFinishLaunching at the latest, otherwise it doesn't work.
197 recentFilesMenuItem = [[NSMenuItem alloc] initWithTitle:@"Open Recent"
198 action:nil keyEquivalent:@""];
200 NSMenu *recentFilesMenu = [[NSMenu alloc] initWithTitle:@"Open Recent"];
201 [recentFilesMenu performSelector:@selector(_setMenuName:)
202 withObject:@"NSRecentDocumentsMenu"];
204 [recentFilesMenu addItemWithTitle:@"Clear Menu"
205 action:@selector(clearRecentDocuments:)
207 [recentFilesMenuItem setSubmenu:recentFilesMenu];
208 [recentFilesMenu release]; // the menu is retained by recentFilesMenuItem
209 [recentFilesMenuItem setTag:-1]; // must not be 0
211 [[[[NSApp mainMenu] itemWithTitle:@"File"] submenu] addItem:recentFilesMenuItem];
213 #if MM_HANDLE_XCODE_MOD_EVENT
214 [[NSAppleEventManager sharedAppleEventManager]
216 andSelector:@selector(handleXcodeModEvent:replyEvent:)
222 - (void)applicationDidFinishLaunching:(NSNotification *)notification
224 [NSApp setServicesProvider:self];
227 - (BOOL)applicationShouldOpenUntitledFile:(NSApplication *)sender
229 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
230 NSAppleEventManager *aem = [NSAppleEventManager sharedAppleEventManager];
231 NSAppleEventDescriptor *desc = [aem currentAppleEvent];
233 // The user default MMUntitledWindow can be set to control whether an
234 // untitled window should open on 'Open' and 'Reopen' events.
235 int untitledWindowFlag = [ud integerForKey:MMUntitledWindowKey];
236 if ([desc eventID] == kAEOpenApplication
237 && (untitledWindowFlag & MMUntitledWindowOnOpen) == 0)
239 else if ([desc eventID] == kAEReopenApplication
240 && (untitledWindowFlag & MMUntitledWindowOnReopen) == 0)
243 // When a process is started from the command line, the 'Open' event will
244 // contain a parameter to surpress the opening of an untitled window.
245 desc = [desc paramDescriptorForKeyword:keyAEPropData];
246 desc = [desc paramDescriptorForKeyword:keyMMUntitledWindow];
247 if (desc && ![desc booleanValue])
250 // Never open an untitled window if there is at least one open window or if
251 // there are processes that are currently launching.
252 if ([vimControllers count] > 0 || [pidArguments count] > 0)
255 // NOTE! This way it possible to start the app with the command-line
256 // argument '-nowindow yes' and no window will be opened by default.
257 return ![ud boolForKey:MMNoWindowKey];
260 - (BOOL)applicationOpenUntitledFile:(NSApplication *)sender
262 [self newWindow:self];
266 - (void)application:(NSApplication *)sender openFiles:(NSArray *)filenames
268 // Opening files works like this:
269 // a) extract ODB/Xcode/Spotlight parameters from the current Apple event
270 // b) filter out any already open files (see filterOpenFiles::)
271 // c) open any remaining files
273 // A file is opened in an untitled window if there is one (it may be
274 // currently launching, or it may already be visible), otherwise a new
277 // Each launching Vim process has a dictionary of arguments that are passed
278 // to the process when in checks in (via connectBackend:pid:). The
279 // arguments for each launching process can be looked up by its PID (in the
280 // pidArguments dictionary).
282 NSMutableDictionary *arguments = [self extractArgumentsFromOdocEvent:
283 [[NSAppleEventManager sharedAppleEventManager] currentAppleEvent]];
285 // Filter out files that are already open
286 filenames = [self filterOpenFiles:filenames arguments:arguments];
288 // Open any files that remain
289 if ([filenames count]) {
291 BOOL openInTabs = [[NSUserDefaults standardUserDefaults]
292 boolForKey:MMOpenFilesInTabsKey];
294 [arguments setObject:filenames forKey:@"filenames"];
295 [arguments setObject:[NSNumber numberWithBool:YES] forKey:@"openFiles"];
297 // Add file names to "Recent Files" menu.
298 int i, count = [filenames count];
299 for (i = 0; i < count; ++i) {
300 // Don't add files that are being edited remotely (using ODB).
301 if ([arguments objectForKey:@"remoteID"]) continue;
303 [[NSDocumentController sharedDocumentController]
304 noteNewRecentFilePath:[filenames objectAtIndex:i]];
307 if ((openInTabs && (vc = [self topmostVimController]))
308 || (vc = [self findUntitledWindow])) {
309 // Open files in an already open window.
310 [[[vc windowController] window] makeKeyAndOrderFront:self];
311 [self passArguments:arguments toVimController:vc];
313 // Open files in a launching Vim process or start a new process.
314 int pid = [self findLaunchingProcessWithoutArguments];
316 // Pass the filenames to the process straight away.
318 // TODO: It would be nicer if all arguments were passed to the
319 // Vim process in connectBackend::, but if we don't pass the
320 // filename arguments here, the window 'flashes' once when it
321 // opens. This is due to the 'welcome' screen first being
322 // displayed, then quickly thereafter the files are opened.
323 NSArray *fileArgs = [NSArray arrayWithObject:@"-p"];
324 fileArgs = [fileArgs arrayByAddingObjectsFromArray:filenames];
326 pid = [self launchVimProcessWithArguments:fileArgs];
329 // TODO: Notify user of failure?
330 [NSApp replyToOpenOrPrint:
331 NSApplicationDelegateReplyFailure];
335 // Make sure these files aren't opened again when
336 // connectBackend:pid: is called.
337 [arguments setObject:[NSNumber numberWithBool:NO]
338 forKey:@"openFiles"];
341 // TODO: If the Vim process fails to start, or if it changes PID,
342 // then the memory allocated for these parameters will leak.
343 // Ensure that this cannot happen or somehow detect it.
345 if ([arguments count] > 0)
346 [pidArguments setObject:arguments
347 forKey:[NSNumber numberWithInt:pid]];
351 [NSApp replyToOpenOrPrint:NSApplicationDelegateReplySuccess];
352 // NSApplicationDelegateReplySuccess = 0,
353 // NSApplicationDelegateReplyCancel = 1,
354 // NSApplicationDelegateReplyFailure = 2
357 - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender
359 return [[NSUserDefaults standardUserDefaults]
360 boolForKey:MMTerminateAfterLastWindowClosedKey];
363 - (NSApplicationTerminateReply)applicationShouldTerminate:
364 (NSApplication *)sender
366 // TODO: Follow Apple's guidelines for 'Graceful Application Termination'
367 // (in particular, allow user to review changes and save).
368 int reply = NSTerminateNow;
369 BOOL modifiedBuffers = NO;
371 // Go through windows, checking for modified buffers. (Each Vim process
372 // tells MacVim when any buffer has been modified and MacVim sets the
373 // 'documentEdited' flag of the window correspondingly.)
374 NSEnumerator *e = [[NSApp windows] objectEnumerator];
376 while ((window = [e nextObject])) {
377 if ([window isDocumentEdited]) {
378 modifiedBuffers = YES;
383 if (modifiedBuffers) {
384 NSAlert *alert = [[NSAlert alloc] init];
385 [alert setAlertStyle:NSWarningAlertStyle];
386 [alert addButtonWithTitle:@"Quit"];
387 [alert addButtonWithTitle:@"Cancel"];
388 [alert setMessageText:@"Quit without saving?"];
389 [alert setInformativeText:@"There are modified buffers, "
390 "if you quit now all changes will be lost. Quit anyway?"];
392 if ([alert runModal] != NSAlertFirstButtonReturn)
393 reply = NSTerminateCancel;
397 // No unmodified buffers, but give a warning if there are multiple
398 // windows and/or tabs open.
399 int numWindows = [vimControllers count];
402 // Count the number of open tabs
403 e = [vimControllers objectEnumerator];
405 while ((vc = [e nextObject])) {
406 NSString *eval = [vc evaluateVimExpression:@"tabpagenr('$')"];
408 int count = [eval intValue];
409 if (count > 0 && count < INT_MAX)
414 if (numWindows > 1 || numTabs > 1) {
415 NSAlert *alert = [[NSAlert alloc] init];
416 [alert setAlertStyle:NSWarningAlertStyle];
417 [alert addButtonWithTitle:@"Quit"];
418 [alert addButtonWithTitle:@"Cancel"];
419 [alert setMessageText:@"Are you sure you want to quit MacVim?"];
421 NSString *info = nil;
422 if (numWindows > 1) {
423 if (numTabs > numWindows)
424 info = [NSString stringWithFormat:@"There are %d windows "
425 "open in MacVim, with a total of %d tabs. Do you want "
426 "to quit anyway?", numWindows, numTabs];
428 info = [NSString stringWithFormat:@"There are %d windows "
429 "open in MacVim. Do you want to quit anyway?",
433 info = [NSString stringWithFormat:@"There are %d tabs open "
434 "in MacVim. Do you want to quit anyway?", numTabs];
437 [alert setInformativeText:info];
439 if ([alert runModal] != NSAlertFirstButtonReturn)
440 reply = NSTerminateCancel;
447 // Tell all Vim processes to terminate now (otherwise they'll leave swap
449 if (NSTerminateNow == reply) {
450 e = [vimControllers objectEnumerator];
452 while ((vc = [e nextObject]))
453 [vc sendMessage:TerminateNowMsgID data:nil];
459 - (void)applicationWillTerminate:(NSNotification *)notification
461 #if MM_HANDLE_XCODE_MOD_EVENT
462 [[NSAppleEventManager sharedAppleEventManager]
463 removeEventHandlerForEventClass:'KAHL'
467 // This will invalidate all connections (since they were spawned from this
469 [connection invalidate];
471 // Send a SIGINT to all running Vim processes, so that they are sure to
472 // receive the connectionDidDie: notification (a process has to be checking
473 // the run-loop for this to happen).
474 unsigned i, count = [vimControllers count];
475 for (i = 0; i < count; ++i) {
476 MMVimController *controller = [vimControllers objectAtIndex:i];
477 int pid = [controller pid];
482 if (fontContainerRef) {
483 ATSFontDeactivate(fontContainerRef, NULL, kATSOptionFlagsDefault);
484 fontContainerRef = 0;
487 [NSApp setDelegate:nil];
490 - (void)removeVimController:(id)controller
492 //NSLog(@"%s%@", _cmd, controller);
494 [[controller windowController] close];
496 [vimControllers removeObject:controller];
498 if (![vimControllers count]) {
499 // Turn on autoenabling of menus (because no Vim is open to handle it),
500 // but do not touch the MacVim menu. Note that the menus must be
501 // enabled first otherwise autoenabling does not work.
502 NSMenu *mainMenu = [NSApp mainMenu];
503 int i, count = [mainMenu numberOfItems];
504 for (i = 1; i < count; ++i) {
505 NSMenuItem *item = [mainMenu itemAtIndex:i];
506 [item setEnabled:YES];
507 [[item submenu] recurseSetAutoenablesItems:YES];
512 - (void)windowControllerWillOpen:(MMWindowController *)windowController
514 NSPoint topLeft = NSZeroPoint;
515 NSWindow *topWin = [[[self topmostVimController] windowController] window];
516 NSWindow *win = [windowController window];
520 // If there is a window belonging to a Vim process, cascade from it,
521 // otherwise use the autosaved window position (if any).
523 NSRect frame = [topWin frame];
524 topLeft = NSMakePoint(frame.origin.x, NSMaxY(frame));
526 NSString *topLeftString = [[NSUserDefaults standardUserDefaults]
527 stringForKey:MMTopLeftPointKey];
529 topLeft = NSPointFromString(topLeftString);
532 if (!NSEqualPoints(topLeft, NSZeroPoint)) {
534 topLeft = [win cascadeTopLeftFromPoint:topLeft];
536 [win setFrameTopLeftPoint:topLeft];
539 if (openSelectionString) {
540 // TODO: Pass this as a parameter instead! Get rid of
541 // 'openSelectionString' etc.
543 // There is some text to paste into this window as a result of the
544 // services menu "Open selection ..." being used.
545 [[windowController vimController] dropString:openSelectionString];
546 [openSelectionString release];
547 openSelectionString = nil;
551 - (IBAction)newWindow:(id)sender
553 [self launchVimProcessWithArguments:nil];
556 - (IBAction)fileOpen:(id)sender
559 BOOL trackPwd = [[NSUserDefaults standardUserDefaults]
560 boolForKey:MMDialogsTrackPwdKey];
562 MMVimController *vc = [self keyVimController];
563 if (vc) dir = [[vc vimState] objectForKey:@"pwd"];
566 NSOpenPanel *panel = [NSOpenPanel openPanel];
567 [panel setAllowsMultipleSelection:YES];
568 int result = [panel runModalForDirectory:dir file:nil types:nil];
569 if (NSOKButton == result)
570 [self application:NSApp openFiles:[panel filenames]];
573 - (IBAction)selectNextWindow:(id)sender
575 unsigned i, count = [vimControllers count];
578 NSWindow *keyWindow = [NSApp keyWindow];
579 for (i = 0; i < count; ++i) {
580 MMVimController *vc = [vimControllers objectAtIndex:i];
581 if ([[[vc windowController] window] isEqual:keyWindow])
588 MMVimController *vc = [vimControllers objectAtIndex:i];
589 [[vc windowController] showWindow:self];
593 - (IBAction)selectPreviousWindow:(id)sender
595 unsigned i, count = [vimControllers count];
598 NSWindow *keyWindow = [NSApp keyWindow];
599 for (i = 0; i < count; ++i) {
600 MMVimController *vc = [vimControllers objectAtIndex:i];
601 if ([[[vc windowController] window] isEqual:keyWindow])
611 MMVimController *vc = [vimControllers objectAtIndex:i];
612 [[vc windowController] showWindow:self];
616 - (IBAction)fontSizeUp:(id)sender
618 [[NSFontManager sharedFontManager] modifyFont:
619 [NSNumber numberWithInt:NSSizeUpFontAction]];
622 - (IBAction)fontSizeDown:(id)sender
624 [[NSFontManager sharedFontManager] modifyFont:
625 [NSNumber numberWithInt:NSSizeDownFontAction]];
628 - (IBAction)orderFrontPreferencePanel:(id)sender
630 [[MMPreferenceController sharedPrefsWindowController] showWindow:self];
633 - (IBAction)openWebsite:(id)sender
635 [[NSWorkspace sharedWorkspace] openURL:
636 [NSURL URLWithString:MMWebsiteString]];
639 - (byref id <MMFrontendProtocol>)
640 connectBackend:(byref in id <MMBackendProtocol>)backend
643 //NSLog(@"Connect backend (pid=%d)", pid);
644 NSNumber *pidKey = [NSNumber numberWithInt:pid];
645 MMVimController *vc = nil;
648 [(NSDistantObject*)backend
649 setProtocolForProxy:@protocol(MMBackendProtocol)];
651 vc = [[[MMVimController alloc]
652 initWithBackend:backend pid:pid recentFiles:recentFilesMenuItem]
655 if (![vimControllers count]) {
656 // The first window autosaves its position. (The autosaving
657 // features of Cocoa are not used because we need more control over
658 // what is autosaved and when it is restored.)
659 [[vc windowController] setWindowAutosaveKey:MMTopLeftPointKey];
662 [vimControllers addObject:vc];
664 id args = [pidArguments objectForKey:pidKey];
665 if (args && [NSNull null] != args)
666 [self passArguments:args toVimController:vc];
668 // HACK! MacVim does not get activated if it is launched from the
669 // terminal, so we forcibly activate here unless it is an untitled
670 // window opening. Untitled windows are treated differently, else
671 // MacVim would steal the focus if another app was activated while the
672 // untitled window was loading.
673 if (!args || args != [NSNull null])
674 [NSApp activateIgnoringOtherApps:YES];
677 [pidArguments removeObjectForKey:pidKey];
682 @catch (NSException *e) {
683 NSLog(@"Exception caught in %s: \"%@\"", _cmd, e);
686 [vimControllers removeObject:vc];
688 [pidArguments removeObjectForKey:pidKey];
694 - (NSArray *)serverList
696 NSMutableArray *array = [NSMutableArray array];
698 unsigned i, count = [vimControllers count];
699 for (i = 0; i < count; ++i) {
700 MMVimController *controller = [vimControllers objectAtIndex:i];
701 if ([controller serverName])
702 [array addObject:[controller serverName]];
708 @end // MMAppController
713 @implementation MMAppController (MMServices)
715 - (void)openSelection:(NSPasteboard *)pboard userData:(NSString *)userData
716 error:(NSString **)error
718 if (![[pboard types] containsObject:NSStringPboardType]) {
719 NSLog(@"WARNING: Pasteboard contains no object of type "
720 "NSStringPboardType");
724 MMVimController *vc = [self topmostVimController];
726 // Open a new tab first, since dropString: does not do this.
727 [vc sendMessage:AddNewTabMsgID data:nil];
728 [vc dropString:[pboard stringForType:NSStringPboardType]];
730 // NOTE: There is no window to paste the selection into, so save the
731 // text, open a new window, and paste the text when the next window
732 // opens. (If this is called several times in a row, then all but the
733 // last call might be ignored.)
734 if (openSelectionString) [openSelectionString release];
735 openSelectionString = [[pboard stringForType:NSStringPboardType] copy];
737 [self newWindow:self];
741 - (void)openFile:(NSPasteboard *)pboard userData:(NSString *)userData
742 error:(NSString **)error
744 if (![[pboard types] containsObject:NSStringPboardType]) {
745 NSLog(@"WARNING: Pasteboard contains no object of type "
746 "NSStringPboardType");
750 // TODO: Parse multiple filenames and create array with names.
751 NSString *string = [pboard stringForType:NSStringPboardType];
752 string = [string stringByTrimmingCharactersInSet:
753 [NSCharacterSet whitespaceAndNewlineCharacterSet]];
754 string = [string stringByStandardizingPath];
756 NSArray *filenames = [self filterFilesAndNotify:
757 [NSArray arrayWithObject:string]];
758 if ([filenames count] > 0) {
759 MMVimController *vc = nil;
760 if (userData && [userData isEqual:@"Tab"])
761 vc = [self topmostVimController];
764 [vc dropFiles:filenames forceOpen:YES];
766 [self application:NSApp openFiles:filenames];
771 @end // MMAppController (MMServices)
776 @implementation MMAppController (Private)
778 - (MMVimController *)keyVimController
780 NSWindow *keyWindow = [NSApp keyWindow];
782 unsigned i, count = [vimControllers count];
783 for (i = 0; i < count; ++i) {
784 MMVimController *vc = [vimControllers objectAtIndex:i];
785 if ([[[vc windowController] window] isEqual:keyWindow])
793 - (MMVimController *)topmostVimController
795 // Find the topmost visible window which has an associated vim controller.
796 NSEnumerator *e = [[NSApp orderedWindows] objectEnumerator];
798 while ((window = [e nextObject]) && [window isVisible]) {
799 unsigned i, count = [vimControllers count];
800 for (i = 0; i < count; ++i) {
801 MMVimController *vc = [vimControllers objectAtIndex:i];
802 if ([[[vc windowController] window] isEqual:window])
810 - (int)launchVimProcessWithArguments:(NSArray *)args
813 NSString *path = [[NSBundle mainBundle] pathForAuxiliaryExecutable:@"Vim"];
816 NSLog(@"ERROR: Vim executable could not be found inside app bundle!");
820 NSArray *taskArgs = [NSArray arrayWithObjects:@"-g", @"-f", nil];
822 taskArgs = [taskArgs arrayByAddingObjectsFromArray:args];
824 BOOL useLoginShell = [[NSUserDefaults standardUserDefaults]
825 boolForKey:MMLoginShellKey];
827 // Run process with a login shell, roughly:
828 // echo "exec Vim -g -f args" | ARGV0=-`basename $SHELL` $SHELL [-l]
829 pid = executeInLoginShell(path, taskArgs);
831 // Run process directly:
833 NSTask *task = [NSTask launchedTaskWithLaunchPath:path
835 pid = task ? [task processIdentifier] : -1;
839 // NOTE: If the process has no arguments, then add a null argument to
840 // the pidArguments dictionary. This is later used to detect that a
841 // process without arguments is being launched.
843 [pidArguments setObject:[NSNull null]
844 forKey:[NSNumber numberWithInt:pid]];
846 NSLog(@"WARNING: %s%@ failed (useLoginShell=%d)", _cmd, args,
853 - (NSArray *)filterFilesAndNotify:(NSArray *)filenames
855 // Go trough 'filenames' array and make sure each file exists. Present
856 // warning dialog if some file was missing.
858 NSString *firstMissingFile = nil;
859 NSMutableArray *files = [NSMutableArray array];
860 unsigned i, count = [filenames count];
862 for (i = 0; i < count; ++i) {
863 NSString *name = [filenames objectAtIndex:i];
864 if ([[NSFileManager defaultManager] fileExistsAtPath:name]) {
865 [files addObject:name];
866 } else if (!firstMissingFile) {
867 firstMissingFile = name;
871 if (firstMissingFile) {
872 NSAlert *alert = [[NSAlert alloc] init];
873 [alert addButtonWithTitle:@"OK"];
876 if ([files count] >= count-1) {
877 [alert setMessageText:@"File not found"];
878 text = [NSString stringWithFormat:@"Could not open file with "
879 "name %@.", firstMissingFile];
881 [alert setMessageText:@"Multiple files not found"];
882 text = [NSString stringWithFormat:@"Could not open file with "
883 "name %@, and %d other files.", firstMissingFile,
884 count-[files count]-1];
887 [alert setInformativeText:text];
888 [alert setAlertStyle:NSWarningAlertStyle];
893 [NSApp replyToOpenOrPrint:NSApplicationDelegateReplyFailure];
899 - (NSArray *)filterOpenFiles:(NSArray *)filenames
900 arguments:(NSDictionary *)args
902 // Check if any of the files in the 'filenames' array are open in any Vim
903 // process. Remove the files that are open from the 'filenames' array and
904 // return it. If all files were filtered out, then raise the first file in
905 // the Vim process it is open. Files that are filtered are sent an odb
906 // open event in case theID is not zero.
908 NSMutableDictionary *localArgs =
909 [NSMutableDictionary dictionaryWithDictionary:args];
910 MMVimController *raiseController = nil;
911 NSString *raiseFile = nil;
912 NSMutableArray *files = [filenames mutableCopy];
913 NSString *expr = [NSString stringWithFormat:
914 @"map([\"%@\"],\"bufloaded(v:val)\")",
915 [files componentsJoinedByString:@"\",\""]];
916 unsigned i, count = [vimControllers count];
918 // Ensure that the files aren't opened when passing arguments.
919 [localArgs setObject:[NSNumber numberWithBool:NO] forKey:@"openFiles"];
921 for (i = 0; i < count && [files count]; ++i) {
922 MMVimController *controller = [vimControllers objectAtIndex:i];
924 // Query Vim for which files in the 'files' array are open.
925 NSString *eval = [controller evaluateVimExpression:expr];
928 NSIndexSet *idxSet = [NSIndexSet indexSetWithVimList:eval];
929 if ([idxSet count]) {
931 // Remember the file and which Vim that has it open so that
932 // we can raise it later on.
933 raiseController = controller;
934 raiseFile = [files objectAtIndex:[idxSet firstIndex]];
935 [[raiseFile retain] autorelease];
938 // Pass (ODB/Xcode/Spotlight) arguments to this process.
939 [localArgs setObject:[files objectsAtIndexes:idxSet]
940 forKey:@"filenames"];
941 [self passArguments:localArgs toVimController:controller];
943 // Remove all the files that were open in this Vim process and
944 // create a new expression to evaluate.
945 [files removeObjectsAtIndexes:idxSet];
946 expr = [NSString stringWithFormat:
947 @"map([\"%@\"],\"bufloaded(v:val)\")",
948 [files componentsJoinedByString:@"\",\""]];
952 if (![files count] && raiseFile) {
953 // Raise the window containing the first file that was already open,
954 // and make sure that the tab containing that file is selected. Only
955 // do this if there are no more files to open, otherwise sometimes the
956 // window with 'raiseFile' will be raised, other times it might be the
957 // window that will open with the files in the 'files' array.
958 raiseFile = [raiseFile stringByEscapingSpecialFilenameCharacters];
959 NSString *input = [NSString stringWithFormat:@"<C-\\><C-N>"
960 ":let oldswb=&swb|let &swb=\"useopen,usetab\"|"
961 "tab sb %@|let &swb=oldswb|unl oldswb|"
962 "cal foreground()|redr|f<CR>", raiseFile];
964 [raiseController addVimInput:input];
970 #if MM_HANDLE_XCODE_MOD_EVENT
971 - (void)handleXcodeModEvent:(NSAppleEventDescriptor *)event
972 replyEvent:(NSAppleEventDescriptor *)reply
975 // Xcode sends this event to query MacVim which open files have been
977 NSLog(@"reply:%@", reply);
978 NSLog(@"event:%@", event);
980 NSEnumerator *e = [vimControllers objectEnumerator];
982 while ((vc = [e nextObject])) {
983 DescType type = [reply descriptorType];
984 unsigned len = [[type data] length];
985 NSMutableData *data = [NSMutableData data];
987 [data appendBytes:&type length:sizeof(DescType)];
988 [data appendBytes:&len length:sizeof(unsigned)];
989 [data appendBytes:[reply data] length:len];
991 [vc sendMessage:XcodeModMsgID data:data];
997 - (int)findLaunchingProcessWithoutArguments
999 NSArray *keys = [pidArguments allKeysForObject:[NSNull null]];
1000 if ([keys count] > 0) {
1001 //NSLog(@"found launching process without arguments");
1002 return [[keys objectAtIndex:0] intValue];
1008 - (MMVimController *)findUntitledWindow
1010 NSEnumerator *e = [vimControllers objectEnumerator];
1012 while ((vc = [e nextObject])) {
1013 // TODO: This is a moronic test...should query the Vim process if there
1014 // are any open buffers or something like that instead.
1015 NSString *title = [[[vc windowController] window] title];
1016 if ([title hasPrefix:@"[No Name] - VIM"]) {
1017 //NSLog(@"found untitled window");
1025 - (NSMutableDictionary *)extractArgumentsFromOdocEvent:
1026 (NSAppleEventDescriptor *)desc
1028 NSMutableDictionary *dict = [NSMutableDictionary dictionary];
1030 // 1. Extract ODB parameters (if any)
1031 NSAppleEventDescriptor *odbdesc = desc;
1032 if (![odbdesc paramDescriptorForKeyword:keyFileSender]) {
1033 // The ODB paramaters may hide inside the 'keyAEPropData' descriptor.
1034 odbdesc = [odbdesc paramDescriptorForKeyword:keyAEPropData];
1035 if (![odbdesc paramDescriptorForKeyword:keyFileSender])
1040 NSAppleEventDescriptor *p =
1041 [odbdesc paramDescriptorForKeyword:keyFileSender];
1043 [dict setObject:[NSNumber numberWithUnsignedInt:[p typeCodeValue]]
1044 forKey:@"remoteID"];
1046 p = [odbdesc paramDescriptorForKeyword:keyFileCustomPath];
1048 [dict setObject:[p stringValue] forKey:@"remotePath"];
1050 p = [odbdesc paramDescriptorForKeyword:keyFileSenderToken];
1052 [dict setObject:p forKey:@"remotePath"];
1055 // 2. Extract Xcode parameters (if any)
1056 NSAppleEventDescriptor *xcodedesc =
1057 [desc paramDescriptorForKeyword:keyAEPosition];
1060 MMSelectionRange *sr = (MMSelectionRange*)[[xcodedesc data] bytes];
1062 if (sr->lineNum < 0) {
1063 // Should select a range of lines.
1064 range.location = sr->startRange + 1;
1065 range.length = sr->endRange - sr->startRange + 1;
1067 // Should only move cursor to a line.
1068 range.location = sr->lineNum + 1;
1072 [dict setObject:NSStringFromRange(range) forKey:@"selectionRange"];
1075 // 3. Extract Spotlight search text (if any)
1076 NSAppleEventDescriptor *spotlightdesc =
1077 [desc paramDescriptorForKeyword:keyAESearchText];
1079 [dict setObject:[spotlightdesc stringValue] forKey:@"searchText"];
1084 - (void)passArguments:(NSDictionary *)args toVimController:(MMVimController*)vc
1088 // Pass filenames to open if required (the 'openFiles' argument can be used
1089 // to disallow opening of the files).
1090 NSArray *filenames = [args objectForKey:@"filenames"];
1091 if (filenames && [[args objectForKey:@"openFiles"] boolValue]) {
1092 NSString *tabDrop = buildTabDropCommand(filenames);
1093 [vc addVimInput:tabDrop];
1097 if (filenames && [args objectForKey:@"remoteID"]) {
1098 [vc odbEdit:filenames
1099 server:[[args objectForKey:@"remoteID"] unsignedIntValue]
1100 path:[args objectForKey:@"remotePath"]
1101 token:[args objectForKey:@"remoteToken"]];
1104 // Pass range of lines to select
1105 if ([args objectForKey:@"selectionRange"]) {
1106 NSRange selectionRange = NSRangeFromString(
1107 [args objectForKey:@"selectionRange"]);
1108 [vc addVimInput:buildSelectRangeCommand(selectionRange)];
1112 NSString *searchText = [args objectForKey:@"searchText"];
1114 [vc addVimInput:buildSearchTextCommand(searchText)];
1117 @end // MMAppController (Private)
1122 @implementation NSMenu (MMExtras)
1124 - (void)recurseSetAutoenablesItems:(BOOL)on
1126 [self setAutoenablesItems:on];
1128 int i, count = [self numberOfItems];
1129 for (i = 0; i < count; ++i) {
1130 NSMenuItem *item = [self itemAtIndex:i];
1131 [item setEnabled:YES];
1132 NSMenu *submenu = [item submenu];
1134 [submenu recurseSetAutoenablesItems:on];
1139 @end // NSMenu (MMExtras)
1144 @implementation NSNumber (MMExtras)
1147 return [self intValue];
1149 @end // NSNumber (MMExtras)
1155 executeInLoginShell(NSString *path, NSArray *args)
1157 // Start a login shell and execute the command 'path' with arguments 'args'
1158 // in the shell. This ensures that user environment variables are set even
1159 // when MacVim was started from the Finder.
1162 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
1164 // Determine which shell to use to execute the command. The user
1165 // may decide which shell to use by setting a user default or the
1166 // $SHELL environment variable.
1167 NSString *shell = [ud stringForKey:MMLoginShellCommandKey];
1168 if (!shell || [shell length] == 0)
1169 shell = [[[NSProcessInfo processInfo] environment]
1170 objectForKey:@"SHELL"];
1172 shell = @"/bin/bash";
1174 //NSLog(@"shell = %@", shell);
1176 // Bash needs the '-l' flag to launch a login shell. The user may add
1177 // flags by setting a user default.
1178 NSString *shellArgument = [ud stringForKey:MMLoginShellArgumentKey];
1179 if (!shellArgument || [shellArgument length] == 0) {
1180 if ([[shell lastPathComponent] isEqual:@"bash"])
1181 shellArgument = @"-l";
1183 shellArgument = nil;
1186 //NSLog(@"shellArgument = %@", shellArgument);
1188 // Build input string to pipe to the login shell.
1189 NSMutableString *input = [NSMutableString stringWithFormat:
1190 @"exec \"%@\"", path];
1192 // Append all arguments, making sure they are properly quoted, even
1193 // when they contain single quotes.
1194 NSEnumerator *e = [args objectEnumerator];
1197 while ((obj = [e nextObject])) {
1198 NSMutableString *arg = [NSMutableString stringWithString:obj];
1199 [arg replaceOccurrencesOfString:@"'" withString:@"'\"'\"'"
1200 options:NSLiteralSearch
1201 range:NSMakeRange(0, [arg length])];
1202 [input appendFormat:@" '%@'", arg];
1206 // Build the argument vector used to start the login shell.
1207 NSString *shellArg0 = [NSString stringWithFormat:@"-%@",
1208 [shell lastPathComponent]];
1209 char *shellArgv[3] = { (char *)[shellArg0 UTF8String], NULL, NULL };
1211 shellArgv[1] = (char *)[shellArgument UTF8String];
1213 // Get the C string representation of the shell path before the fork since
1214 // we must not call Foundation functions after a fork.
1215 const char *shellPath = [shell fileSystemRepresentation];
1217 // Fork and execute the process.
1219 if (pipe(ds)) return -1;
1224 } else if (pid == 0) {
1226 if (close(ds[1]) == -1) exit(255);
1227 if (dup2(ds[0], 0) == -1) exit(255);
1229 execv(shellPath, shellArgv);
1231 // Never reached unless execv fails
1235 if (close(ds[0]) == -1) return -1;
1237 // Send input to execute to the child process
1238 [input appendString:@"\n"];
1239 int bytes = [input lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
1241 if (write(ds[1], [input UTF8String], bytes) != bytes) return -1;
1242 if (close(ds[1]) == -1) return -1;