Improved method to start Vim processes in a login shell
[MacVim.git] / src / MacVim / MMAppController.m
blobcb5348d7ac2897cca455939a034e6e9bc2d10b4d
1 /* vi:set ts=8 sts=4 sw=4 ft=objc:
2  *
3  * VIM - Vi IMproved            by Bram Moolenaar
4  *                              MacVim GUI port by Bjorn Winckler
5  *
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.
9  */
11  * MMAppController
12  *
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.
17  *
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
22  * MMVimController.
23  *
24  * A Vim process started from the command line connects directly by sending the
25  * connectBackend:pid: message (launchVimProcessWithArguments: is never called
26  * in this case).
27  */
29 #import "MMAppController.h"
30 #import "MMVimController.h"
31 #import "MMWindowController.h"
32 #import "MMPreferenceController.h"
33 #import <unistd.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;
45 #pragma options align=mac68k
46 typedef struct
48     short unused1;      // 0 (not used)
49     short lineNum;      // line to select (< 0 to specify range)
50     long  startRange;   // start of selection range (if line < 0)
51     long  endRange;     // end of selection range (if line < 0)
52     long  unused2;      // 0 (not used)
53     long  theDate;      // modification date/time
54 } MMSelectionRange;
55 #pragma options align=reset
58 static int executeInLoginShell(NSString *path, NSArray *args);
61 @interface MMAppController (MMServices)
62 - (void)openSelection:(NSPasteboard *)pboard userData:(NSString *)userData
63                 error:(NSString **)error;
64 - (void)openFile:(NSPasteboard *)pboard userData:(NSString *)userData
65            error:(NSString **)error;
66 @end
69 @interface MMAppController (Private)
70 - (MMVimController *)keyVimController;
71 - (MMVimController *)topmostVimController;
72 - (int)launchVimProcessWithArguments:(NSArray *)args;
73 - (NSArray *)filterFilesAndNotify:(NSArray *)files;
74 - (NSArray *)filterOpenFiles:(NSArray *)filenames
75                    arguments:(NSDictionary *)args;
76 #if MM_HANDLE_XCODE_MOD_EVENT
77 - (void)handleXcodeModEvent:(NSAppleEventDescriptor *)event
78                  replyEvent:(NSAppleEventDescriptor *)reply;
79 #endif
80 - (int)findLaunchingProcessWithoutArguments;
81 - (MMVimController *)findUntitledWindow;
82 - (NSMutableDictionary *)extractArgumentsFromOdocEvent:
83     (NSAppleEventDescriptor *)desc;
84 - (void)passArguments:(NSDictionary *)args toVimController:(MMVimController*)vc;
85 @end
87 @interface NSMenu (MMExtras)
88 - (void)recurseSetAutoenablesItems:(BOOL)on;
89 @end
91 @interface NSNumber (MMExtras)
92 - (int)tag;
93 @end
97 @implementation MMAppController
99 + (void)initialize
101     NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:
102         [NSNumber numberWithBool:NO],   MMNoWindowKey,
103         [NSNumber numberWithInt:64],    MMTabMinWidthKey,
104         [NSNumber numberWithInt:6*64],  MMTabMaxWidthKey,
105         [NSNumber numberWithInt:132],   MMTabOptimumWidthKey,
106         [NSNumber numberWithInt:2],     MMTextInsetLeftKey,
107         [NSNumber numberWithInt:1],     MMTextInsetRightKey,
108         [NSNumber numberWithInt:1],     MMTextInsetTopKey,
109         [NSNumber numberWithInt:1],     MMTextInsetBottomKey,
110         [NSNumber numberWithBool:NO],   MMTerminateAfterLastWindowClosedKey,
111         @"MMTypesetter",                MMTypesetterKey,
112         [NSNumber numberWithFloat:1],   MMCellWidthMultiplierKey,
113         [NSNumber numberWithFloat:-1],  MMBaselineOffsetKey,
114         [NSNumber numberWithBool:YES],  MMTranslateCtrlClickKey,
115         [NSNumber numberWithBool:NO],   MMOpenFilesInTabsKey,
116         [NSNumber numberWithBool:NO],   MMNoFontSubstitutionKey,
117         [NSNumber numberWithBool:NO],   MMLoginShellKey,
118         [NSNumber numberWithBool:NO],   MMAtsuiRendererKey,
119         [NSNumber numberWithInt:MMUntitledWindowAlways],
120                                         MMUntitledWindowKey,
121         [NSNumber numberWithBool:NO],   MMTexturedWindowKey,
122         [NSNumber numberWithBool:NO],   MMZoomBothKey,
123         @"",                            MMLoginShellCommandKey,
124         @"",                            MMLoginShellArgumentKey,
125         nil];
127     [[NSUserDefaults standardUserDefaults] registerDefaults:dict];
129     NSArray *types = [NSArray arrayWithObject:NSStringPboardType];
130     [NSApp registerServicesMenuSendTypes:types returnTypes:types];
133 - (id)init
135     if ((self = [super init])) {
136         fontContainerRef = loadFonts();
138         vimControllers = [NSMutableArray new];
139         pidArguments = [NSMutableDictionary new];
141         // NOTE!  If the name of the connection changes here it must also be
142         // updated in MMBackend.m.
143         NSConnection *connection = [NSConnection defaultConnection];
144         NSString *name = [NSString stringWithFormat:@"%@-connection",
145                  [[NSBundle mainBundle] bundleIdentifier]];
146         //NSLog(@"Registering connection with name '%@'", name);
147         if ([connection registerName:name]) {
148             [connection setRequestTimeout:MMRequestTimeout];
149             [connection setReplyTimeout:MMReplyTimeout];
150             [connection setRootObject:self];
152             // NOTE: When the user is resizing the window the AppKit puts the
153             // run loop in event tracking mode.  Unless the connection listens
154             // to request in this mode, live resizing won't work.
155             [connection addRequestMode:NSEventTrackingRunLoopMode];
156         } else {
157             NSLog(@"WARNING: Failed to register connection with name '%@'",
158                     name);
159         }
160     }
162     return self;
165 - (void)dealloc
167     //NSLog(@"MMAppController dealloc");
169     [pidArguments release];  pidArguments = nil;
170     [vimControllers release];  vimControllers = nil;
171     [openSelectionString release];  openSelectionString = nil;
173     [super dealloc];
176 #if MM_HANDLE_XCODE_MOD_EVENT
177 - (void)applicationWillFinishLaunching:(NSNotification *)notification
179     [[NSAppleEventManager sharedAppleEventManager]
180             setEventHandler:self
181                 andSelector:@selector(handleXcodeModEvent:replyEvent:)
182               forEventClass:'KAHL'
183                  andEventID:'MOD '];
185 #endif
187 - (void)applicationDidFinishLaunching:(NSNotification *)notification
189     [NSApp setServicesProvider:self];
192 - (BOOL)applicationShouldOpenUntitledFile:(NSApplication *)sender
194     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
195     NSAppleEventManager *aem = [NSAppleEventManager sharedAppleEventManager];
196     NSAppleEventDescriptor *desc = [aem currentAppleEvent];
198     // The user default MMUntitledWindow can be set to control whether an
199     // untitled window should open on 'Open' and 'Reopen' events.
200     int untitledWindowFlag = [ud integerForKey:MMUntitledWindowKey];
201     if ([desc eventID] == kAEOpenApplication
202             && (untitledWindowFlag & MMUntitledWindowOnOpen) == 0)
203         return NO;
204     else if ([desc eventID] == kAEReopenApplication
205             && (untitledWindowFlag & MMUntitledWindowOnReopen) == 0)
206         return NO;
208     // When a process is started from the command line, the 'Open' event will
209     // contain a parameter to surpress the opening of an untitled window.
210     desc = [desc paramDescriptorForKeyword:keyAEPropData];
211     desc = [desc paramDescriptorForKeyword:keyMMUntitledWindow];
212     if (desc && ![desc booleanValue])
213         return NO;
215     // Never open an untitled window if there is at least one open window or if
216     // there are processes that are currently launching.
217     if ([vimControllers count] > 0 || [pidArguments count] > 0)
218         return NO;
220     // NOTE!  This way it possible to start the app with the command-line
221     // argument '-nowindow yes' and no window will be opened by default.
222     return ![ud boolForKey:MMNoWindowKey];
225 - (BOOL)applicationOpenUntitledFile:(NSApplication *)sender
227     [self newWindow:self];
228     return YES;
231 - (void)application:(NSApplication *)sender openFiles:(NSArray *)filenames
233     // Opening files works like this:
234     //  a) extract ODB/Xcode/Spotlight parameters from the current Apple event
235     //  b) filter out any already open files (see filterOpenFiles::)
236     //  c) open any remaining files
237     //
238     // A file is opened in an untitled window if there is one (it may be
239     // currently launching, or it may already be visible), otherwise a new
240     // window is opened.
241     //
242     // Each launching Vim process has a dictionary of arguments that are passed
243     // to the process when in checks in (via connectBackend:pid:).  The
244     // arguments for each launching process can be looked up by its PID (in the
245     // pidArguments dictionary).
247     NSMutableDictionary *arguments = [self extractArgumentsFromOdocEvent:
248             [[NSAppleEventManager sharedAppleEventManager] currentAppleEvent]];
250     // Filter out files that are already open
251     filenames = [self filterOpenFiles:filenames arguments:arguments];
253     // Open any files that remain
254     if ([filenames count]) {
255         MMVimController *vc;
256         BOOL openInTabs = [[NSUserDefaults standardUserDefaults]
257             boolForKey:MMOpenFilesInTabsKey];
259         [arguments setObject:filenames forKey:@"filenames"];
260         [arguments setObject:[NSNumber numberWithBool:YES] forKey:@"openFiles"];
262         if ((openInTabs && (vc = [self topmostVimController]))
263                || (vc = [self findUntitledWindow])) {
264             // Open files in an already open window.
265             [[[vc windowController] window] makeKeyAndOrderFront:self];
266             [self passArguments:arguments toVimController:vc];
267         } else {
268             // Open files in a launching Vim process or start a new process.
269             int pid = [self findLaunchingProcessWithoutArguments];
270             if (!pid) {
271                 // Pass the filenames to the process straight away.
272                 //
273                 // TODO: It would be nicer if all arguments were passed to the
274                 // Vim process in connectBackend::, but if we don't pass the
275                 // filename arguments here, the window 'flashes' once when it
276                 // opens.  This is due to the 'welcome' screen first being
277                 // displayed, then quickly thereafter the files are opened.
278                 NSArray *fileArgs = [NSArray arrayWithObject:@"-p"];
279                 fileArgs = [fileArgs arrayByAddingObjectsFromArray:filenames];
281                 pid = [self launchVimProcessWithArguments:fileArgs];
283                 if (-1 == pid) {
284                     // TODO: Notify user of failure?
285                     [NSApp replyToOpenOrPrint:
286                         NSApplicationDelegateReplyFailure];
287                     return;
288                 }
290                 // Make sure these files aren't opened again when
291                 // connectBackend:pid: is called.
292                 [arguments setObject:[NSNumber numberWithBool:NO]
293                               forKey:@"openFiles"];
294             }
296             // TODO: If the Vim process fails to start, or if it changes PID,
297             // then the memory allocated for these parameters will leak.
298             // Ensure that this cannot happen or somehow detect it.
300             if ([arguments count] > 0)
301                 [pidArguments setObject:arguments
302                                  forKey:[NSNumber numberWithInt:pid]];
303         }
304     }
306     [NSApp replyToOpenOrPrint:NSApplicationDelegateReplySuccess];
307     // NSApplicationDelegateReplySuccess = 0,
308     // NSApplicationDelegateReplyCancel = 1,
309     // NSApplicationDelegateReplyFailure = 2
312 - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender
314     return [[NSUserDefaults standardUserDefaults]
315             boolForKey:MMTerminateAfterLastWindowClosedKey];
318 - (NSApplicationTerminateReply)applicationShouldTerminate:
319     (NSApplication *)sender
321     // TODO: Follow Apple's guidelines for 'Graceful Application Termination'
322     // (in particular, allow user to review changes and save).
323     int reply = NSTerminateNow;
324     BOOL modifiedBuffers = NO;
326     // Go through windows, checking for modified buffers.  (Each Vim process
327     // tells MacVim when any buffer has been modified and MacVim sets the
328     // 'documentEdited' flag of the window correspondingly.)
329     NSEnumerator *e = [[NSApp windows] objectEnumerator];
330     id window;
331     while ((window = [e nextObject])) {
332         if ([window isDocumentEdited]) {
333             modifiedBuffers = YES;
334             break;
335         }
336     }
338     if (modifiedBuffers) {
339         NSAlert *alert = [[NSAlert alloc] init];
340         [alert setAlertStyle:NSWarningAlertStyle];
341         [alert addButtonWithTitle:@"Quit"];
342         [alert addButtonWithTitle:@"Cancel"];
343         [alert setMessageText:@"Quit without saving?"];
344         [alert setInformativeText:@"There are modified buffers, "
345             "if you quit now all changes will be lost.  Quit anyway?"];
347         if ([alert runModal] != NSAlertFirstButtonReturn)
348             reply = NSTerminateCancel;
350         [alert release];
351     } else {
352         // No unmodified buffers, but give a warning if there are multiple
353         // windows and/or tabs open.
354         int numWindows = [vimControllers count];
355         int numTabs = 0;
357         // Count the number of open tabs
358         e = [vimControllers objectEnumerator];
359         id vc;
360         while ((vc = [e nextObject])) {
361             NSString *eval = [vc evaluateVimExpression:@"tabpagenr('$')"];
362             if (eval) {
363                 int count = [eval intValue];
364                 if (count > 0 && count < INT_MAX)
365                     numTabs += count;
366             }
367         }
369         if (numWindows > 1 || numTabs > 1) {
370             NSAlert *alert = [[NSAlert alloc] init];
371             [alert setAlertStyle:NSWarningAlertStyle];
372             [alert addButtonWithTitle:@"Quit"];
373             [alert addButtonWithTitle:@"Cancel"];
374             [alert setMessageText:@"Are you sure you want to quit MacVim?"];
376             NSString *info = nil;
377             if (numWindows > 1) {
378                 if (numTabs > numWindows)
379                     info = [NSString stringWithFormat:@"There are %d windows "
380                         "open in MacVim, with a total of %d tabs. Do you want "
381                         "to quit anyway?", numWindows, numTabs];
382                 else
383                     info = [NSString stringWithFormat:@"There are %d windows "
384                         "open in MacVim. Do you want to quit anyway?",
385                         numWindows];
387             } else {
388                 info = [NSString stringWithFormat:@"There are %d tabs open "
389                     "in MacVim. Do you want to quit anyway?", numTabs];
390             }
392             [alert setInformativeText:info];
394             if ([alert runModal] != NSAlertFirstButtonReturn)
395                 reply = NSTerminateCancel;
397             [alert release];
398         }
399     }
402     // Tell all Vim processes to terminate now (otherwise they'll leave swap
403     // files behind).
404     if (NSTerminateNow == reply) {
405         e = [vimControllers objectEnumerator];
406         id vc;
407         while ((vc = [e nextObject]))
408             [vc sendMessage:TerminateNowMsgID data:nil];
409     }
411     return reply;
414 - (void)applicationWillTerminate:(NSNotification *)notification
416 #if MM_HANDLE_XCODE_MOD_EVENT
417     [[NSAppleEventManager sharedAppleEventManager]
418             removeEventHandlerForEventClass:'KAHL'
419                                  andEventID:'MOD '];
420 #endif
422     // This will invalidate all connections (since they were spawned from the
423     // default connection).
424     [[NSConnection defaultConnection] invalidate];
426     // Send a SIGINT to all running Vim processes, so that they are sure to
427     // receive the connectionDidDie: notification (a process has to be checking
428     // the run-loop for this to happen).
429     unsigned i, count = [vimControllers count];
430     for (i = 0; i < count; ++i) {
431         MMVimController *controller = [vimControllers objectAtIndex:i];
432         int pid = [controller pid];
433         if (pid > 0)
434             kill(pid, SIGINT);
435     }
437     if (fontContainerRef) {
438         ATSFontDeactivate(fontContainerRef, NULL, kATSOptionFlagsDefault);
439         fontContainerRef = 0;
440     }
442     [NSApp setDelegate:nil];
445 - (void)removeVimController:(id)controller
447     //NSLog(@"%s%@", _cmd, controller);
449     [[controller windowController] close];
451     [vimControllers removeObject:controller];
453     if (![vimControllers count]) {
454         // Turn on autoenabling of menus (because no Vim is open to handle it),
455         // but do not touch the MacVim menu.  Note that the menus must be
456         // enabled first otherwise autoenabling does not work.
457         NSMenu *mainMenu = [NSApp mainMenu];
458         int i, count = [mainMenu numberOfItems];
459         for (i = 1; i < count; ++i) {
460             NSMenuItem *item = [mainMenu itemAtIndex:i];
461             [item setEnabled:YES];
462             [[item submenu] recurseSetAutoenablesItems:YES];
463         }
464     }
467 - (void)windowControllerWillOpen:(MMWindowController *)windowController
469     NSPoint topLeft = NSZeroPoint;
470     NSWindow *keyWin = [NSApp keyWindow];
471     NSWindow *win = [windowController window];
473     if (!win) return;
475     // If there is a key window, cascade from it, otherwise use the autosaved
476     // window position (if any).
477     if (keyWin) {
478         NSRect frame = [keyWin frame];
479         topLeft = NSMakePoint(frame.origin.x, NSMaxY(frame));
480     } else {
481         NSString *topLeftString = [[NSUserDefaults standardUserDefaults]
482             stringForKey:MMTopLeftPointKey];
483         if (topLeftString)
484             topLeft = NSPointFromString(topLeftString);
485     }
487     if (!NSEqualPoints(topLeft, NSZeroPoint)) {
488         if (keyWin)
489             topLeft = [win cascadeTopLeftFromPoint:topLeft];
491         [win setFrameTopLeftPoint:topLeft];
492     }
494     if (openSelectionString) {
495         // TODO: Pass this as a parameter instead!  Get rid of
496         // 'openSelectionString' etc.
497         //
498         // There is some text to paste into this window as a result of the
499         // services menu "Open selection ..." being used.
500         [[windowController vimController] dropString:openSelectionString];
501         [openSelectionString release];
502         openSelectionString = nil;
503     }
506 - (IBAction)newWindow:(id)sender
508     [self launchVimProcessWithArguments:nil];
511 - (IBAction)fileOpen:(id)sender
513     NSOpenPanel *panel = [NSOpenPanel openPanel];
514     [panel setAllowsMultipleSelection:YES];
516     int result = [panel runModalForTypes:nil];
517     if (NSOKButton == result)
518         [self application:NSApp openFiles:[panel filenames]];
521 - (IBAction)selectNextWindow:(id)sender
523     unsigned i, count = [vimControllers count];
524     if (!count) return;
526     NSWindow *keyWindow = [NSApp keyWindow];
527     for (i = 0; i < count; ++i) {
528         MMVimController *vc = [vimControllers objectAtIndex:i];
529         if ([[[vc windowController] window] isEqual:keyWindow])
530             break;
531     }
533     if (i < count) {
534         if (++i >= count)
535             i = 0;
536         MMVimController *vc = [vimControllers objectAtIndex:i];
537         [[vc windowController] showWindow:self];
538     }
541 - (IBAction)selectPreviousWindow:(id)sender
543     unsigned i, count = [vimControllers count];
544     if (!count) return;
546     NSWindow *keyWindow = [NSApp keyWindow];
547     for (i = 0; i < count; ++i) {
548         MMVimController *vc = [vimControllers objectAtIndex:i];
549         if ([[[vc windowController] window] isEqual:keyWindow])
550             break;
551     }
553     if (i < count) {
554         if (i > 0) {
555             --i;
556         } else {
557             i = count - 1;
558         }
559         MMVimController *vc = [vimControllers objectAtIndex:i];
560         [[vc windowController] showWindow:self];
561     }
564 - (IBAction)fontSizeUp:(id)sender
566     [[NSFontManager sharedFontManager] modifyFont:
567             [NSNumber numberWithInt:NSSizeUpFontAction]];
570 - (IBAction)fontSizeDown:(id)sender
572     [[NSFontManager sharedFontManager] modifyFont:
573             [NSNumber numberWithInt:NSSizeDownFontAction]];
576 - (IBAction)orderFrontPreferencePanel:(id)sender
578     [[MMPreferenceController sharedPrefsWindowController] showWindow:self];
581 - (byref id <MMFrontendProtocol>)
582     connectBackend:(byref in id <MMBackendProtocol>)backend
583                pid:(int)pid
585     //NSLog(@"Connect backend (pid=%d)", pid);
586     NSNumber *pidKey = [NSNumber numberWithInt:pid];
587     MMVimController *vc = nil;
589     @try {
590         [(NSDistantObject*)backend
591                 setProtocolForProxy:@protocol(MMBackendProtocol)];
593         vc = [[[MMVimController alloc]
594                 initWithBackend:backend pid:pid] autorelease];
596         if (![vimControllers count]) {
597             // The first window autosaves its position.  (The autosaving
598             // features of Cocoa are not used because we need more control over
599             // what is autosaved and when it is restored.)
600             [[vc windowController] setWindowAutosaveKey:MMTopLeftPointKey];
601         }
603         [vimControllers addObject:vc];
605         id args = [pidArguments objectForKey:pidKey];
606         if (args && [NSNull null] != args)
607             [self passArguments:args toVimController:vc];
609         // HACK!  MacVim does not get activated if it is launched from the
610         // terminal, so we forcibly activate here unless it is an untitled
611         // window opening.  Untitled windows are treated differently, else
612         // MacVim would steal the focus if another app was activated while the
613         // untitled window was loading.
614         if (!args || args != [NSNull null])
615             [NSApp activateIgnoringOtherApps:YES];
617         if (args)
618             [pidArguments removeObjectForKey:pidKey];
620         return vc;
621     }
623     @catch (NSException *e) {
624         NSLog(@"Exception caught in %s: \"%@\"", _cmd, e);
626         if (vc)
627             [vimControllers removeObject:vc];
629         [pidArguments removeObjectForKey:pidKey];
630     }
632     return nil;
635 - (NSArray *)serverList
637     NSMutableArray *array = [NSMutableArray array];
639     unsigned i, count = [vimControllers count];
640     for (i = 0; i < count; ++i) {
641         MMVimController *controller = [vimControllers objectAtIndex:i];
642         if ([controller serverName])
643             [array addObject:[controller serverName]];
644     }
646     return array;
649 @end // MMAppController
654 @implementation MMAppController (MMServices)
656 - (void)openSelection:(NSPasteboard *)pboard userData:(NSString *)userData
657                 error:(NSString **)error
659     if (![[pboard types] containsObject:NSStringPboardType]) {
660         NSLog(@"WARNING: Pasteboard contains no object of type "
661                 "NSStringPboardType");
662         return;
663     }
665     MMVimController *vc = [self topmostVimController];
666     if (vc) {
667         // Open a new tab first, since dropString: does not do this.
668         [vc sendMessage:AddNewTabMsgID data:nil];
669         [vc dropString:[pboard stringForType:NSStringPboardType]];
670     } else {
671         // NOTE: There is no window to paste the selection into, so save the
672         // text, open a new window, and paste the text when the next window
673         // opens.  (If this is called several times in a row, then all but the
674         // last call might be ignored.)
675         if (openSelectionString) [openSelectionString release];
676         openSelectionString = [[pboard stringForType:NSStringPboardType] copy];
678         [self newWindow:self];
679     }
682 - (void)openFile:(NSPasteboard *)pboard userData:(NSString *)userData
683            error:(NSString **)error
685     if (![[pboard types] containsObject:NSStringPboardType]) {
686         NSLog(@"WARNING: Pasteboard contains no object of type "
687                 "NSStringPboardType");
688         return;
689     }
691     // TODO: Parse multiple filenames and create array with names.
692     NSString *string = [pboard stringForType:NSStringPboardType];
693     string = [string stringByTrimmingCharactersInSet:
694             [NSCharacterSet whitespaceAndNewlineCharacterSet]];
695     string = [string stringByStandardizingPath];
697     NSArray *filenames = [self filterFilesAndNotify:
698             [NSArray arrayWithObject:string]];
699     if ([filenames count] > 0) {
700         MMVimController *vc = nil;
701         if (userData && [userData isEqual:@"Tab"])
702             vc = [self topmostVimController];
704         if (vc) {
705             [vc dropFiles:filenames forceOpen:YES];
706         } else {
707             [self application:NSApp openFiles:filenames];
708         }
709     }
712 @end // MMAppController (MMServices)
717 @implementation MMAppController (Private)
719 - (MMVimController *)keyVimController
721     NSWindow *keyWindow = [NSApp keyWindow];
722     if (keyWindow) {
723         unsigned i, count = [vimControllers count];
724         for (i = 0; i < count; ++i) {
725             MMVimController *vc = [vimControllers objectAtIndex:i];
726             if ([[[vc windowController] window] isEqual:keyWindow])
727                 return vc;
728         }
729     }
731     return nil;
734 - (MMVimController *)topmostVimController
736     NSArray *windows = [NSApp orderedWindows];
737     if ([windows count] > 0) {
738         NSWindow *window = [windows objectAtIndex:0];
739         unsigned i, count = [vimControllers count];
740         for (i = 0; i < count; ++i) {
741             MMVimController *vc = [vimControllers objectAtIndex:i];
742             if ([[[vc windowController] window] isEqual:window])
743                 return vc;
744         }
745     }
747     return nil;
750 - (int)launchVimProcessWithArguments:(NSArray *)args
752     int pid = -1;
753     NSString *path = [[NSBundle mainBundle] pathForAuxiliaryExecutable:@"Vim"];
755     if (!path) {
756         NSLog(@"ERROR: Vim executable could not be found inside app bundle!");
757         return -1;
758     }
760     NSArray *taskArgs = [NSArray arrayWithObjects:@"-g", @"-f", nil];
761     if (args)
762         taskArgs = [taskArgs arrayByAddingObjectsFromArray:args];
764     BOOL useLoginShell = [[NSUserDefaults standardUserDefaults]
765             boolForKey:MMLoginShellKey];
766     if (useLoginShell) {
767         // Run process with a login shell, roughly:
768         //   echo "exec Vim -g -f args" | ARGV0=-`basename $SHELL` $SHELL [-l]
769         pid = executeInLoginShell(path, taskArgs);
770     } else {
771         // Run process directly:
772         //   Vim -g -f args
773         NSTask *task = [NSTask launchedTaskWithLaunchPath:path
774                                                 arguments:taskArgs];
775         pid = task ? [task processIdentifier] : -1;
776     }
778     if (-1 != pid) {
779         // NOTE: If the process has no arguments, then add a null argument to
780         // the pidArguments dictionary.  This is later used to detect that a
781         // process without arguments is being launched.
782         if (!args)
783             [pidArguments setObject:[NSNull null]
784                              forKey:[NSNumber numberWithInt:pid]];
785     } else {
786         NSLog(@"WARNING: %s%@ failed (useLoginShell=%d)", _cmd, args,
787                 useLoginShell);
788     }
790     return pid;
793 - (NSArray *)filterFilesAndNotify:(NSArray *)filenames
795     // Go trough 'filenames' array and make sure each file exists.  Present
796     // warning dialog if some file was missing.
798     NSString *firstMissingFile = nil;
799     NSMutableArray *files = [NSMutableArray array];
800     unsigned i, count = [filenames count];
802     for (i = 0; i < count; ++i) {
803         NSString *name = [filenames objectAtIndex:i];
804         if ([[NSFileManager defaultManager] fileExistsAtPath:name]) {
805             [files addObject:name];
806         } else if (!firstMissingFile) {
807             firstMissingFile = name;
808         }
809     }
811     if (firstMissingFile) {
812         NSAlert *alert = [[NSAlert alloc] init];
813         [alert addButtonWithTitle:@"OK"];
815         NSString *text;
816         if ([files count] >= count-1) {
817             [alert setMessageText:@"File not found"];
818             text = [NSString stringWithFormat:@"Could not open file with "
819                 "name %@.", firstMissingFile];
820         } else {
821             [alert setMessageText:@"Multiple files not found"];
822             text = [NSString stringWithFormat:@"Could not open file with "
823                 "name %@, and %d other files.", firstMissingFile,
824                 count-[files count]-1];
825         }
827         [alert setInformativeText:text];
828         [alert setAlertStyle:NSWarningAlertStyle];
830         [alert runModal];
831         [alert release];
833         [NSApp replyToOpenOrPrint:NSApplicationDelegateReplyFailure];
834     }
836     return files;
839 - (NSArray *)filterOpenFiles:(NSArray *)filenames
840                    arguments:(NSDictionary *)args
842     // Check if any of the files in the 'filenames' array are open in any Vim
843     // process.  Remove the files that are open from the 'filenames' array and
844     // return it.  If all files were filtered out, then raise the first file in
845     // the Vim process it is open.  Files that are filtered are sent an odb
846     // open event in case theID is not zero.
848     NSMutableDictionary *localArgs =
849             [NSMutableDictionary dictionaryWithDictionary:args];
850     MMVimController *raiseController = nil;
851     NSString *raiseFile = nil;
852     NSMutableArray *files = [filenames mutableCopy];
853     NSString *expr = [NSString stringWithFormat:
854             @"map([\"%@\"],\"bufloaded(v:val)\")",
855             [files componentsJoinedByString:@"\",\""]];
856     unsigned i, count = [vimControllers count];
858     // Ensure that the files aren't opened when passing arguments.
859     [localArgs setObject:[NSNumber numberWithBool:NO] forKey:@"openFiles"];
861     for (i = 0; i < count && [files count]; ++i) {
862         MMVimController *controller = [vimControllers objectAtIndex:i];
864         // Query Vim for which files in the 'files' array are open.
865         NSString *eval = [controller evaluateVimExpression:expr];
866         if (!eval) continue;
868         NSIndexSet *idxSet = [NSIndexSet indexSetWithVimList:eval];
869         if ([idxSet count]) {
870             if (!raiseFile) {
871                 // Remember the file and which Vim that has it open so that
872                 // we can raise it later on.
873                 raiseController = controller;
874                 raiseFile = [files objectAtIndex:[idxSet firstIndex]];
875                 [[raiseFile retain] autorelease];
876             }
878             // Pass (ODB/Xcode/Spotlight) arguments to this process.
879             [localArgs setObject:[files objectsAtIndexes:idxSet]
880                           forKey:@"filenames"];
881             [self passArguments:localArgs toVimController:controller];
883             // Remove all the files that were open in this Vim process and
884             // create a new expression to evaluate.
885             [files removeObjectsAtIndexes:idxSet];
886             expr = [NSString stringWithFormat:
887                     @"map([\"%@\"],\"bufloaded(v:val)\")",
888                     [files componentsJoinedByString:@"\",\""]];
889         }
890     }
892     if (![files count] && raiseFile) {
893         // Raise the window containing the first file that was already open,
894         // and make sure that the tab containing that file is selected.  Only
895         // do this if there are no more files to open, otherwise sometimes the
896         // window with 'raiseFile' will be raised, other times it might be the
897         // window that will open with the files in the 'files' array.
898         raiseFile = [raiseFile stringByEscapingSpecialFilenameCharacters];
899         NSString *input = [NSString stringWithFormat:@"<C-\\><C-N>"
900             ":let oldswb=&swb|let &swb=\"useopen,usetab\"|"
901             "tab sb %@|let &swb=oldswb|unl oldswb|"
902             "cal foreground()|redr|f<CR>", raiseFile];
904         [raiseController addVimInput:input];
905     }
907     return files;
910 #if MM_HANDLE_XCODE_MOD_EVENT
911 - (void)handleXcodeModEvent:(NSAppleEventDescriptor *)event
912                  replyEvent:(NSAppleEventDescriptor *)reply
914 #if 0
915     // Xcode sends this event to query MacVim which open files have been
916     // modified.
917     NSLog(@"reply:%@", reply);
918     NSLog(@"event:%@", event);
920     NSEnumerator *e = [vimControllers objectEnumerator];
921     id vc;
922     while ((vc = [e nextObject])) {
923         DescType type = [reply descriptorType];
924         unsigned len = [[type data] length];
925         NSMutableData *data = [NSMutableData data];
927         [data appendBytes:&type length:sizeof(DescType)];
928         [data appendBytes:&len length:sizeof(unsigned)];
929         [data appendBytes:[reply data] length:len];
931         [vc sendMessage:XcodeModMsgID data:data];
932     }
933 #endif
935 #endif
937 - (int)findLaunchingProcessWithoutArguments
939     NSArray *keys = [pidArguments allKeysForObject:[NSNull null]];
940     if ([keys count] > 0) {
941         //NSLog(@"found launching process without arguments");
942         return [[keys objectAtIndex:0] intValue];
943     }
945     return 0;
948 - (MMVimController *)findUntitledWindow
950     NSEnumerator *e = [vimControllers objectEnumerator];
951     id vc;
952     while ((vc = [e nextObject])) {
953         // TODO: This is a moronic test...should query the Vim process if there
954         // are any open buffers or something like that instead.
955         NSString *title = [[[vc windowController] window] title];
956         if ([title hasPrefix:@"[No Name] - VIM"]) {
957             //NSLog(@"found untitled window");
958             return vc;
959         }
960     }
962     return nil;
965 - (NSMutableDictionary *)extractArgumentsFromOdocEvent:
966     (NSAppleEventDescriptor *)desc
968     NSMutableDictionary *dict = [NSMutableDictionary dictionary];
970     // 1. Extract ODB parameters (if any)
971     NSAppleEventDescriptor *odbdesc = desc;
972     if (![odbdesc paramDescriptorForKeyword:keyFileSender]) {
973         // The ODB paramaters may hide inside the 'keyAEPropData' descriptor.
974         odbdesc = [odbdesc paramDescriptorForKeyword:keyAEPropData];
975         if (![odbdesc paramDescriptorForKeyword:keyFileSender])
976             odbdesc = nil;
977     }
979     if (odbdesc) {
980         NSAppleEventDescriptor *p =
981                 [odbdesc paramDescriptorForKeyword:keyFileSender];
982         if (p)
983             [dict setObject:[NSNumber numberWithUnsignedInt:[p typeCodeValue]]
984                      forKey:@"remoteID"];
986         p = [odbdesc paramDescriptorForKeyword:keyFileCustomPath];
987         if (p)
988             [dict setObject:[p stringValue] forKey:@"remotePath"];
990         p = [odbdesc paramDescriptorForKeyword:keyFileSenderToken];
991         if (p)
992             [dict setObject:p forKey:@"remotePath"];
993     }
995     // 2. Extract Xcode parameters (if any)
996     NSAppleEventDescriptor *xcodedesc =
997             [desc paramDescriptorForKeyword:keyAEPosition];
998     if (xcodedesc) {
999         NSRange range;
1000         MMSelectionRange *sr = (MMSelectionRange*)[[xcodedesc data] bytes];
1002         if (sr->lineNum < 0) {
1003             // Should select a range of lines.
1004             range.location = sr->startRange + 1;
1005             range.length = sr->endRange - sr->startRange + 1;
1006         } else {
1007             // Should only move cursor to a line.
1008             range.location = sr->lineNum + 1;
1009             range.length = 0;
1010         }
1012         [dict setObject:NSStringFromRange(range) forKey:@"selectionRange"];
1013     }
1015     // 3. Extract Spotlight search text (if any)
1016     NSAppleEventDescriptor *spotlightdesc = 
1017             [desc paramDescriptorForKeyword:keyAESearchText];
1018     if (spotlightdesc)
1019         [dict setObject:[spotlightdesc stringValue] forKey:@"searchText"];
1021     return dict;
1024 - (void)passArguments:(NSDictionary *)args toVimController:(MMVimController*)vc
1026     if (!args) return;
1028     // Pass filenames to open if required (the 'openFiles' argument can be used
1029     // to disallow opening of the files).
1030     NSArray *filenames = [args objectForKey:@"filenames"];
1031     if (filenames && [[args objectForKey:@"openFiles"] boolValue]) {
1032         NSString *tabDrop = buildTabDropCommand(filenames);
1033         [vc addVimInput:tabDrop];
1034     }
1036     // Pass ODB data
1037     if (filenames && [args objectForKey:@"remoteID"]) {
1038         [vc odbEdit:filenames
1039              server:[[args objectForKey:@"remoteID"] unsignedIntValue]
1040                path:[args objectForKey:@"remotePath"]
1041               token:[args objectForKey:@"remoteToken"]];
1042     }
1044     // Pass range of lines to select
1045     if ([args objectForKey:@"selectionRange"]) {
1046         NSRange selectionRange = NSRangeFromString(
1047                 [args objectForKey:@"selectionRange"]);
1048         [vc addVimInput:buildSelectRangeCommand(selectionRange)];
1049     }
1051     // Pass search text
1052     NSString *searchText = [args objectForKey:@"searchText"];
1053     if (searchText)
1054         [vc addVimInput:buildSearchTextCommand(searchText)];
1057 @end // MMAppController (Private)
1062 @implementation NSMenu (MMExtras)
1064 - (void)recurseSetAutoenablesItems:(BOOL)on
1066     [self setAutoenablesItems:on];
1068     int i, count = [self numberOfItems];
1069     for (i = 0; i < count; ++i) {
1070         NSMenuItem *item = [self itemAtIndex:i];
1071         [item setEnabled:YES];
1072         NSMenu *submenu = [item submenu];
1073         if (submenu) {
1074             [submenu recurseSetAutoenablesItems:on];
1075         }
1076     }
1079 @end  // NSMenu (MMExtras)
1084 @implementation NSNumber (MMExtras)
1085 - (int)tag
1087     return [self intValue];
1089 @end // NSNumber (MMExtras)
1094     static int
1095 executeInLoginShell(NSString *path, NSArray *args)
1097     // Start a login shell and execute the command 'path' with arguments 'args'
1098     // in the shell.  This ensures that user environment variables are set even
1099     // when MacVim was started from the Finder.
1101     int pid = -1;
1102     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
1104     // Determine which shell to use to execute the command.  The user
1105     // may decide which shell to use by setting a user default or the
1106     // $SHELL environment variable.
1107     NSString *shell = [ud stringForKey:MMLoginShellCommandKey];
1108     if (!shell || [shell length] == 0)
1109         shell = [[[NSProcessInfo processInfo] environment]
1110             objectForKey:@"SHELL"];
1111     if (!shell)
1112         shell = @"/bin/bash";
1114     //NSLog(@"shell = %@", shell);
1116     // Bash needs the '-l' flag to launch a login shell.  The user may add
1117     // flags by setting a user default.
1118     NSString *shellArgument = [ud stringForKey:MMLoginShellArgumentKey];
1119     if (!shellArgument || [shellArgument length] == 0) {
1120         if ([[shell lastPathComponent] isEqual:@"bash"])
1121             shellArgument = @"-l";
1122         else
1123             shellArgument = nil;
1124     }
1126     //NSLog(@"shellArgument = %@", shellArgument);
1128     // Build input string to pipe to the login shell.
1129     NSMutableString *input = [NSMutableString stringWithFormat:
1130             @"exec \"%@\"", path];
1131     if (args) {
1132         // Append all arguments, making sure they are properly quoted, even
1133         // when they contain single quotes.
1134         NSEnumerator *e = [args objectEnumerator];
1135         id obj;
1137         while ((obj = [e nextObject])) {
1138             NSMutableString *arg = [NSMutableString stringWithString:obj];
1139             [arg replaceOccurrencesOfString:@"'" withString:@"'\"'\"'"
1140                                     options:NSLiteralSearch
1141                                       range:NSMakeRange(0, [arg length])];
1142             [input appendFormat:@" '%@'", arg];
1143         }
1144     }
1146     // Build the argument vector used to start the login shell.
1147     NSString *shellArg0 = [NSString stringWithFormat:@"-%@",
1148              [shell lastPathComponent]];
1149     char *shellArgv[3] = { (char *)[shellArg0 UTF8String], NULL, NULL };
1150     if (shellArgument)
1151         shellArgv[1] = (char *)[shellArgument UTF8String];
1153     // Get the C string representation of the shell path before the fork since
1154     // we must not call Foundation functions after a fork.
1155     char *shellPath = [shell fileSystemRepresentation];
1157     // Fork and execute the process.
1158     int ds[2];
1159     if (pipe(ds)) return -1;
1161     pid = fork();
1162     if (pid == -1) {
1163         return -1;
1164     } else if (pid == 0) {
1165         // Child process
1166         if (close(ds[1]) == -1) exit(255);
1167         if (dup2(ds[0], 0) == -1) exit(255);
1169         execv(shellPath, shellArgv);
1171         // Never reached unless execv fails
1172         exit(255);
1173     } else {
1174         // Parent process
1175         if (close(ds[0]) == -1) return -1;
1177         // Send input to execute to the child process
1178         [input appendString:@"\n"];
1179         int bytes = [input lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
1181         if (write(ds[1], [input UTF8String], bytes) != bytes) return -1;
1182         if (close(ds[1]) == -1) return -1;
1183     }
1185     return pid;