Only cascade from windows belonging to Vim process
[MacVim.git] / src / MacVim / MMAppController.m
blob2e60e6455fb1f44fba300f0196579fcc96c3da54
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;
44 static NSString *MMWebsiteString = @"http://code.google.com/p/macvim/";
47 #pragma options align=mac68k
48 typedef struct
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
56 } MMSelectionRange;
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;
68 @end
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;
81 #endif
82 - (int)findLaunchingProcessWithoutArguments;
83 - (MMVimController *)findUntitledWindow;
84 - (NSMutableDictionary *)extractArgumentsFromOdocEvent:
85     (NSAppleEventDescriptor *)desc;
86 - (void)passArguments:(NSDictionary *)args toVimController:(MMVimController*)vc;
87 @end
89 @interface NSMenu (MMExtras)
90 - (void)recurseSetAutoenablesItems:(BOOL)on;
91 @end
93 @interface NSNumber (MMExtras)
94 - (int)tag;
95 @end
99 @implementation MMAppController
101 + (void)initialize
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],
122                                         MMUntitledWindowKey,
123         [NSNumber numberWithBool:NO],   MMTexturedWindowKey,
124         [NSNumber numberWithBool:NO],   MMZoomBothKey,
125         @"",                            MMLoginShellCommandKey,
126         @"",                            MMLoginShellArgumentKey,
127         nil];
129     [[NSUserDefaults standardUserDefaults] registerDefaults:dict];
131     NSArray *types = [NSArray arrayWithObject:NSStringPboardType];
132     [NSApp registerServicesMenuSendTypes:types returnTypes:types];
135 - (id)init
137     if ((self = [super init])) {
138         fontContainerRef = loadFonts();
140         vimControllers = [NSMutableArray new];
141         pidArguments = [NSMutableDictionary new];
143         // NOTE!  If the name of the connection changes here it must also be
144         // updated in MMBackend.m.
145         NSConnection *connection = [NSConnection defaultConnection];
146         NSString *name = [NSString stringWithFormat:@"%@-connection",
147                  [[NSBundle mainBundle] bundleIdentifier]];
148         //NSLog(@"Registering connection with name '%@'", name);
149         if ([connection registerName:name]) {
150             [connection setRequestTimeout:MMRequestTimeout];
151             [connection setReplyTimeout:MMReplyTimeout];
152             [connection setRootObject:self];
154             // NOTE: When the user is resizing the window the AppKit puts the
155             // run loop in event tracking mode.  Unless the connection listens
156             // to request in this mode, live resizing won't work.
157             [connection addRequestMode:NSEventTrackingRunLoopMode];
158         } else {
159             NSLog(@"WARNING: Failed to register connection with name '%@'",
160                     name);
161         }
162     }
164     return self;
167 - (void)dealloc
169     //NSLog(@"MMAppController dealloc");
171     [pidArguments release];  pidArguments = nil;
172     [vimControllers release];  vimControllers = nil;
173     [openSelectionString release];  openSelectionString = nil;
175     [super dealloc];
178 #if MM_HANDLE_XCODE_MOD_EVENT
179 - (void)applicationWillFinishLaunching:(NSNotification *)notification
181     [[NSAppleEventManager sharedAppleEventManager]
182             setEventHandler:self
183                 andSelector:@selector(handleXcodeModEvent:replyEvent:)
184               forEventClass:'KAHL'
185                  andEventID:'MOD '];
187 #endif
189 - (void)applicationDidFinishLaunching:(NSNotification *)notification
191     [NSApp setServicesProvider:self];
194 - (BOOL)applicationShouldOpenUntitledFile:(NSApplication *)sender
196     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
197     NSAppleEventManager *aem = [NSAppleEventManager sharedAppleEventManager];
198     NSAppleEventDescriptor *desc = [aem currentAppleEvent];
200     // The user default MMUntitledWindow can be set to control whether an
201     // untitled window should open on 'Open' and 'Reopen' events.
202     int untitledWindowFlag = [ud integerForKey:MMUntitledWindowKey];
203     if ([desc eventID] == kAEOpenApplication
204             && (untitledWindowFlag & MMUntitledWindowOnOpen) == 0)
205         return NO;
206     else if ([desc eventID] == kAEReopenApplication
207             && (untitledWindowFlag & MMUntitledWindowOnReopen) == 0)
208         return NO;
210     // When a process is started from the command line, the 'Open' event will
211     // contain a parameter to surpress the opening of an untitled window.
212     desc = [desc paramDescriptorForKeyword:keyAEPropData];
213     desc = [desc paramDescriptorForKeyword:keyMMUntitledWindow];
214     if (desc && ![desc booleanValue])
215         return NO;
217     // Never open an untitled window if there is at least one open window or if
218     // there are processes that are currently launching.
219     if ([vimControllers count] > 0 || [pidArguments count] > 0)
220         return NO;
222     // NOTE!  This way it possible to start the app with the command-line
223     // argument '-nowindow yes' and no window will be opened by default.
224     return ![ud boolForKey:MMNoWindowKey];
227 - (BOOL)applicationOpenUntitledFile:(NSApplication *)sender
229     [self newWindow:self];
230     return YES;
233 - (void)application:(NSApplication *)sender openFiles:(NSArray *)filenames
235     // Opening files works like this:
236     //  a) extract ODB/Xcode/Spotlight parameters from the current Apple event
237     //  b) filter out any already open files (see filterOpenFiles::)
238     //  c) open any remaining files
239     //
240     // A file is opened in an untitled window if there is one (it may be
241     // currently launching, or it may already be visible), otherwise a new
242     // window is opened.
243     //
244     // Each launching Vim process has a dictionary of arguments that are passed
245     // to the process when in checks in (via connectBackend:pid:).  The
246     // arguments for each launching process can be looked up by its PID (in the
247     // pidArguments dictionary).
249     NSMutableDictionary *arguments = [self extractArgumentsFromOdocEvent:
250             [[NSAppleEventManager sharedAppleEventManager] currentAppleEvent]];
252     // Filter out files that are already open
253     filenames = [self filterOpenFiles:filenames arguments:arguments];
255     // Open any files that remain
256     if ([filenames count]) {
257         MMVimController *vc;
258         BOOL openInTabs = [[NSUserDefaults standardUserDefaults]
259             boolForKey:MMOpenFilesInTabsKey];
261         [arguments setObject:filenames forKey:@"filenames"];
262         [arguments setObject:[NSNumber numberWithBool:YES] forKey:@"openFiles"];
264         if ((openInTabs && (vc = [self topmostVimController]))
265                || (vc = [self findUntitledWindow])) {
266             // Open files in an already open window.
267             [[[vc windowController] window] makeKeyAndOrderFront:self];
268             [self passArguments:arguments toVimController:vc];
269         } else {
270             // Open files in a launching Vim process or start a new process.
271             int pid = [self findLaunchingProcessWithoutArguments];
272             if (!pid) {
273                 // Pass the filenames to the process straight away.
274                 //
275                 // TODO: It would be nicer if all arguments were passed to the
276                 // Vim process in connectBackend::, but if we don't pass the
277                 // filename arguments here, the window 'flashes' once when it
278                 // opens.  This is due to the 'welcome' screen first being
279                 // displayed, then quickly thereafter the files are opened.
280                 NSArray *fileArgs = [NSArray arrayWithObject:@"-p"];
281                 fileArgs = [fileArgs arrayByAddingObjectsFromArray:filenames];
283                 pid = [self launchVimProcessWithArguments:fileArgs];
285                 if (-1 == pid) {
286                     // TODO: Notify user of failure?
287                     [NSApp replyToOpenOrPrint:
288                         NSApplicationDelegateReplyFailure];
289                     return;
290                 }
292                 // Make sure these files aren't opened again when
293                 // connectBackend:pid: is called.
294                 [arguments setObject:[NSNumber numberWithBool:NO]
295                               forKey:@"openFiles"];
296             }
298             // TODO: If the Vim process fails to start, or if it changes PID,
299             // then the memory allocated for these parameters will leak.
300             // Ensure that this cannot happen or somehow detect it.
302             if ([arguments count] > 0)
303                 [pidArguments setObject:arguments
304                                  forKey:[NSNumber numberWithInt:pid]];
305         }
306     }
308     [NSApp replyToOpenOrPrint:NSApplicationDelegateReplySuccess];
309     // NSApplicationDelegateReplySuccess = 0,
310     // NSApplicationDelegateReplyCancel = 1,
311     // NSApplicationDelegateReplyFailure = 2
314 - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender
316     return [[NSUserDefaults standardUserDefaults]
317             boolForKey:MMTerminateAfterLastWindowClosedKey];
320 - (NSApplicationTerminateReply)applicationShouldTerminate:
321     (NSApplication *)sender
323     // TODO: Follow Apple's guidelines for 'Graceful Application Termination'
324     // (in particular, allow user to review changes and save).
325     int reply = NSTerminateNow;
326     BOOL modifiedBuffers = NO;
328     // Go through windows, checking for modified buffers.  (Each Vim process
329     // tells MacVim when any buffer has been modified and MacVim sets the
330     // 'documentEdited' flag of the window correspondingly.)
331     NSEnumerator *e = [[NSApp windows] objectEnumerator];
332     id window;
333     while ((window = [e nextObject])) {
334         if ([window isDocumentEdited]) {
335             modifiedBuffers = YES;
336             break;
337         }
338     }
340     if (modifiedBuffers) {
341         NSAlert *alert = [[NSAlert alloc] init];
342         [alert setAlertStyle:NSWarningAlertStyle];
343         [alert addButtonWithTitle:@"Quit"];
344         [alert addButtonWithTitle:@"Cancel"];
345         [alert setMessageText:@"Quit without saving?"];
346         [alert setInformativeText:@"There are modified buffers, "
347             "if you quit now all changes will be lost.  Quit anyway?"];
349         if ([alert runModal] != NSAlertFirstButtonReturn)
350             reply = NSTerminateCancel;
352         [alert release];
353     } else {
354         // No unmodified buffers, but give a warning if there are multiple
355         // windows and/or tabs open.
356         int numWindows = [vimControllers count];
357         int numTabs = 0;
359         // Count the number of open tabs
360         e = [vimControllers objectEnumerator];
361         id vc;
362         while ((vc = [e nextObject])) {
363             NSString *eval = [vc evaluateVimExpression:@"tabpagenr('$')"];
364             if (eval) {
365                 int count = [eval intValue];
366                 if (count > 0 && count < INT_MAX)
367                     numTabs += count;
368             }
369         }
371         if (numWindows > 1 || numTabs > 1) {
372             NSAlert *alert = [[NSAlert alloc] init];
373             [alert setAlertStyle:NSWarningAlertStyle];
374             [alert addButtonWithTitle:@"Quit"];
375             [alert addButtonWithTitle:@"Cancel"];
376             [alert setMessageText:@"Are you sure you want to quit MacVim?"];
378             NSString *info = nil;
379             if (numWindows > 1) {
380                 if (numTabs > numWindows)
381                     info = [NSString stringWithFormat:@"There are %d windows "
382                         "open in MacVim, with a total of %d tabs. Do you want "
383                         "to quit anyway?", numWindows, numTabs];
384                 else
385                     info = [NSString stringWithFormat:@"There are %d windows "
386                         "open in MacVim. Do you want to quit anyway?",
387                         numWindows];
389             } else {
390                 info = [NSString stringWithFormat:@"There are %d tabs open "
391                     "in MacVim. Do you want to quit anyway?", numTabs];
392             }
394             [alert setInformativeText:info];
396             if ([alert runModal] != NSAlertFirstButtonReturn)
397                 reply = NSTerminateCancel;
399             [alert release];
400         }
401     }
404     // Tell all Vim processes to terminate now (otherwise they'll leave swap
405     // files behind).
406     if (NSTerminateNow == reply) {
407         e = [vimControllers objectEnumerator];
408         id vc;
409         while ((vc = [e nextObject]))
410             [vc sendMessage:TerminateNowMsgID data:nil];
411     }
413     return reply;
416 - (void)applicationWillTerminate:(NSNotification *)notification
418 #if MM_HANDLE_XCODE_MOD_EVENT
419     [[NSAppleEventManager sharedAppleEventManager]
420             removeEventHandlerForEventClass:'KAHL'
421                                  andEventID:'MOD '];
422 #endif
424     // This will invalidate all connections (since they were spawned from the
425     // default connection).
426     [[NSConnection defaultConnection] invalidate];
428     // Send a SIGINT to all running Vim processes, so that they are sure to
429     // receive the connectionDidDie: notification (a process has to be checking
430     // the run-loop for this to happen).
431     unsigned i, count = [vimControllers count];
432     for (i = 0; i < count; ++i) {
433         MMVimController *controller = [vimControllers objectAtIndex:i];
434         int pid = [controller pid];
435         if (pid > 0)
436             kill(pid, SIGINT);
437     }
439     if (fontContainerRef) {
440         ATSFontDeactivate(fontContainerRef, NULL, kATSOptionFlagsDefault);
441         fontContainerRef = 0;
442     }
444     [NSApp setDelegate:nil];
447 - (void)removeVimController:(id)controller
449     //NSLog(@"%s%@", _cmd, controller);
451     [[controller windowController] close];
453     [vimControllers removeObject:controller];
455     if (![vimControllers count]) {
456         // Turn on autoenabling of menus (because no Vim is open to handle it),
457         // but do not touch the MacVim menu.  Note that the menus must be
458         // enabled first otherwise autoenabling does not work.
459         NSMenu *mainMenu = [NSApp mainMenu];
460         int i, count = [mainMenu numberOfItems];
461         for (i = 1; i < count; ++i) {
462             NSMenuItem *item = [mainMenu itemAtIndex:i];
463             [item setEnabled:YES];
464             [[item submenu] recurseSetAutoenablesItems:YES];
465         }
466     }
469 - (void)windowControllerWillOpen:(MMWindowController *)windowController
471     NSPoint topLeft = NSZeroPoint;
472     NSWindow *topWin = [[[self topmostVimController] windowController] window];
473     NSWindow *win = [windowController window];
475     if (!win) return;
477     // If there is a window belonging to a Vim process, cascade from it,
478     // otherwise use the autosaved window position (if any).
479     if (topWin) {
480         NSRect frame = [topWin frame];
481         topLeft = NSMakePoint(frame.origin.x, NSMaxY(frame));
482     } else {
483         NSString *topLeftString = [[NSUserDefaults standardUserDefaults]
484             stringForKey:MMTopLeftPointKey];
485         if (topLeftString)
486             topLeft = NSPointFromString(topLeftString);
487     }
489     if (!NSEqualPoints(topLeft, NSZeroPoint)) {
490         if (topWin)
491             topLeft = [win cascadeTopLeftFromPoint:topLeft];
493         [win setFrameTopLeftPoint:topLeft];
494     }
496     if (openSelectionString) {
497         // TODO: Pass this as a parameter instead!  Get rid of
498         // 'openSelectionString' etc.
499         //
500         // There is some text to paste into this window as a result of the
501         // services menu "Open selection ..." being used.
502         [[windowController vimController] dropString:openSelectionString];
503         [openSelectionString release];
504         openSelectionString = nil;
505     }
508 - (IBAction)newWindow:(id)sender
510     [self launchVimProcessWithArguments:nil];
513 - (IBAction)fileOpen:(id)sender
515     NSOpenPanel *panel = [NSOpenPanel openPanel];
516     [panel setAllowsMultipleSelection:YES];
518     int result = [panel runModalForTypes:nil];
519     if (NSOKButton == result)
520         [self application:NSApp openFiles:[panel filenames]];
523 - (IBAction)selectNextWindow:(id)sender
525     unsigned i, count = [vimControllers count];
526     if (!count) return;
528     NSWindow *keyWindow = [NSApp keyWindow];
529     for (i = 0; i < count; ++i) {
530         MMVimController *vc = [vimControllers objectAtIndex:i];
531         if ([[[vc windowController] window] isEqual:keyWindow])
532             break;
533     }
535     if (i < count) {
536         if (++i >= count)
537             i = 0;
538         MMVimController *vc = [vimControllers objectAtIndex:i];
539         [[vc windowController] showWindow:self];
540     }
543 - (IBAction)selectPreviousWindow:(id)sender
545     unsigned i, count = [vimControllers count];
546     if (!count) return;
548     NSWindow *keyWindow = [NSApp keyWindow];
549     for (i = 0; i < count; ++i) {
550         MMVimController *vc = [vimControllers objectAtIndex:i];
551         if ([[[vc windowController] window] isEqual:keyWindow])
552             break;
553     }
555     if (i < count) {
556         if (i > 0) {
557             --i;
558         } else {
559             i = count - 1;
560         }
561         MMVimController *vc = [vimControllers objectAtIndex:i];
562         [[vc windowController] showWindow:self];
563     }
566 - (IBAction)fontSizeUp:(id)sender
568     [[NSFontManager sharedFontManager] modifyFont:
569             [NSNumber numberWithInt:NSSizeUpFontAction]];
572 - (IBAction)fontSizeDown:(id)sender
574     [[NSFontManager sharedFontManager] modifyFont:
575             [NSNumber numberWithInt:NSSizeDownFontAction]];
578 - (IBAction)orderFrontPreferencePanel:(id)sender
580     [[MMPreferenceController sharedPrefsWindowController] showWindow:self];
583 - (IBAction)openWebsite:(id)sender
585     [[NSWorkspace sharedWorkspace] openURL:
586             [NSURL URLWithString:MMWebsiteString]];
589 - (byref id <MMFrontendProtocol>)
590     connectBackend:(byref in id <MMBackendProtocol>)backend
591                pid:(int)pid
593     //NSLog(@"Connect backend (pid=%d)", pid);
594     NSNumber *pidKey = [NSNumber numberWithInt:pid];
595     MMVimController *vc = nil;
597     @try {
598         [(NSDistantObject*)backend
599                 setProtocolForProxy:@protocol(MMBackendProtocol)];
601         vc = [[[MMVimController alloc]
602                 initWithBackend:backend pid:pid] autorelease];
604         if (![vimControllers count]) {
605             // The first window autosaves its position.  (The autosaving
606             // features of Cocoa are not used because we need more control over
607             // what is autosaved and when it is restored.)
608             [[vc windowController] setWindowAutosaveKey:MMTopLeftPointKey];
609         }
611         [vimControllers addObject:vc];
613         id args = [pidArguments objectForKey:pidKey];
614         if (args && [NSNull null] != args)
615             [self passArguments:args toVimController:vc];
617         // HACK!  MacVim does not get activated if it is launched from the
618         // terminal, so we forcibly activate here unless it is an untitled
619         // window opening.  Untitled windows are treated differently, else
620         // MacVim would steal the focus if another app was activated while the
621         // untitled window was loading.
622         if (!args || args != [NSNull null])
623             [NSApp activateIgnoringOtherApps:YES];
625         if (args)
626             [pidArguments removeObjectForKey:pidKey];
628         return vc;
629     }
631     @catch (NSException *e) {
632         NSLog(@"Exception caught in %s: \"%@\"", _cmd, e);
634         if (vc)
635             [vimControllers removeObject:vc];
637         [pidArguments removeObjectForKey:pidKey];
638     }
640     return nil;
643 - (NSArray *)serverList
645     NSMutableArray *array = [NSMutableArray array];
647     unsigned i, count = [vimControllers count];
648     for (i = 0; i < count; ++i) {
649         MMVimController *controller = [vimControllers objectAtIndex:i];
650         if ([controller serverName])
651             [array addObject:[controller serverName]];
652     }
654     return array;
657 @end // MMAppController
662 @implementation MMAppController (MMServices)
664 - (void)openSelection:(NSPasteboard *)pboard userData:(NSString *)userData
665                 error:(NSString **)error
667     if (![[pboard types] containsObject:NSStringPboardType]) {
668         NSLog(@"WARNING: Pasteboard contains no object of type "
669                 "NSStringPboardType");
670         return;
671     }
673     MMVimController *vc = [self topmostVimController];
674     if (vc) {
675         // Open a new tab first, since dropString: does not do this.
676         [vc sendMessage:AddNewTabMsgID data:nil];
677         [vc dropString:[pboard stringForType:NSStringPboardType]];
678     } else {
679         // NOTE: There is no window to paste the selection into, so save the
680         // text, open a new window, and paste the text when the next window
681         // opens.  (If this is called several times in a row, then all but the
682         // last call might be ignored.)
683         if (openSelectionString) [openSelectionString release];
684         openSelectionString = [[pboard stringForType:NSStringPboardType] copy];
686         [self newWindow:self];
687     }
690 - (void)openFile:(NSPasteboard *)pboard userData:(NSString *)userData
691            error:(NSString **)error
693     if (![[pboard types] containsObject:NSStringPboardType]) {
694         NSLog(@"WARNING: Pasteboard contains no object of type "
695                 "NSStringPboardType");
696         return;
697     }
699     // TODO: Parse multiple filenames and create array with names.
700     NSString *string = [pboard stringForType:NSStringPboardType];
701     string = [string stringByTrimmingCharactersInSet:
702             [NSCharacterSet whitespaceAndNewlineCharacterSet]];
703     string = [string stringByStandardizingPath];
705     NSArray *filenames = [self filterFilesAndNotify:
706             [NSArray arrayWithObject:string]];
707     if ([filenames count] > 0) {
708         MMVimController *vc = nil;
709         if (userData && [userData isEqual:@"Tab"])
710             vc = [self topmostVimController];
712         if (vc) {
713             [vc dropFiles:filenames forceOpen:YES];
714         } else {
715             [self application:NSApp openFiles:filenames];
716         }
717     }
720 @end // MMAppController (MMServices)
725 @implementation MMAppController (Private)
727 - (MMVimController *)keyVimController
729     NSWindow *keyWindow = [NSApp keyWindow];
730     if (keyWindow) {
731         unsigned i, count = [vimControllers count];
732         for (i = 0; i < count; ++i) {
733             MMVimController *vc = [vimControllers objectAtIndex:i];
734             if ([[[vc windowController] window] isEqual:keyWindow])
735                 return vc;
736         }
737     }
739     return nil;
742 - (MMVimController *)topmostVimController
744     NSArray *windows = [NSApp orderedWindows];
745     if ([windows count] > 0) {
746         NSWindow *window = [windows objectAtIndex:0];
747         unsigned i, count = [vimControllers count];
748         for (i = 0; i < count; ++i) {
749             MMVimController *vc = [vimControllers objectAtIndex:i];
750             if ([[[vc windowController] window] isEqual:window])
751                 return vc;
752         }
753     }
755     return nil;
758 - (int)launchVimProcessWithArguments:(NSArray *)args
760     int pid = -1;
761     NSString *path = [[NSBundle mainBundle] pathForAuxiliaryExecutable:@"Vim"];
763     if (!path) {
764         NSLog(@"ERROR: Vim executable could not be found inside app bundle!");
765         return -1;
766     }
768     NSArray *taskArgs = [NSArray arrayWithObjects:@"-g", @"-f", nil];
769     if (args)
770         taskArgs = [taskArgs arrayByAddingObjectsFromArray:args];
772     BOOL useLoginShell = [[NSUserDefaults standardUserDefaults]
773             boolForKey:MMLoginShellKey];
774     if (useLoginShell) {
775         // Run process with a login shell, roughly:
776         //   echo "exec Vim -g -f args" | ARGV0=-`basename $SHELL` $SHELL [-l]
777         pid = executeInLoginShell(path, taskArgs);
778     } else {
779         // Run process directly:
780         //   Vim -g -f args
781         NSTask *task = [NSTask launchedTaskWithLaunchPath:path
782                                                 arguments:taskArgs];
783         pid = task ? [task processIdentifier] : -1;
784     }
786     if (-1 != pid) {
787         // NOTE: If the process has no arguments, then add a null argument to
788         // the pidArguments dictionary.  This is later used to detect that a
789         // process without arguments is being launched.
790         if (!args)
791             [pidArguments setObject:[NSNull null]
792                              forKey:[NSNumber numberWithInt:pid]];
793     } else {
794         NSLog(@"WARNING: %s%@ failed (useLoginShell=%d)", _cmd, args,
795                 useLoginShell);
796     }
798     return pid;
801 - (NSArray *)filterFilesAndNotify:(NSArray *)filenames
803     // Go trough 'filenames' array and make sure each file exists.  Present
804     // warning dialog if some file was missing.
806     NSString *firstMissingFile = nil;
807     NSMutableArray *files = [NSMutableArray array];
808     unsigned i, count = [filenames count];
810     for (i = 0; i < count; ++i) {
811         NSString *name = [filenames objectAtIndex:i];
812         if ([[NSFileManager defaultManager] fileExistsAtPath:name]) {
813             [files addObject:name];
814         } else if (!firstMissingFile) {
815             firstMissingFile = name;
816         }
817     }
819     if (firstMissingFile) {
820         NSAlert *alert = [[NSAlert alloc] init];
821         [alert addButtonWithTitle:@"OK"];
823         NSString *text;
824         if ([files count] >= count-1) {
825             [alert setMessageText:@"File not found"];
826             text = [NSString stringWithFormat:@"Could not open file with "
827                 "name %@.", firstMissingFile];
828         } else {
829             [alert setMessageText:@"Multiple files not found"];
830             text = [NSString stringWithFormat:@"Could not open file with "
831                 "name %@, and %d other files.", firstMissingFile,
832                 count-[files count]-1];
833         }
835         [alert setInformativeText:text];
836         [alert setAlertStyle:NSWarningAlertStyle];
838         [alert runModal];
839         [alert release];
841         [NSApp replyToOpenOrPrint:NSApplicationDelegateReplyFailure];
842     }
844     return files;
847 - (NSArray *)filterOpenFiles:(NSArray *)filenames
848                    arguments:(NSDictionary *)args
850     // Check if any of the files in the 'filenames' array are open in any Vim
851     // process.  Remove the files that are open from the 'filenames' array and
852     // return it.  If all files were filtered out, then raise the first file in
853     // the Vim process it is open.  Files that are filtered are sent an odb
854     // open event in case theID is not zero.
856     NSMutableDictionary *localArgs =
857             [NSMutableDictionary dictionaryWithDictionary:args];
858     MMVimController *raiseController = nil;
859     NSString *raiseFile = nil;
860     NSMutableArray *files = [filenames mutableCopy];
861     NSString *expr = [NSString stringWithFormat:
862             @"map([\"%@\"],\"bufloaded(v:val)\")",
863             [files componentsJoinedByString:@"\",\""]];
864     unsigned i, count = [vimControllers count];
866     // Ensure that the files aren't opened when passing arguments.
867     [localArgs setObject:[NSNumber numberWithBool:NO] forKey:@"openFiles"];
869     for (i = 0; i < count && [files count]; ++i) {
870         MMVimController *controller = [vimControllers objectAtIndex:i];
872         // Query Vim for which files in the 'files' array are open.
873         NSString *eval = [controller evaluateVimExpression:expr];
874         if (!eval) continue;
876         NSIndexSet *idxSet = [NSIndexSet indexSetWithVimList:eval];
877         if ([idxSet count]) {
878             if (!raiseFile) {
879                 // Remember the file and which Vim that has it open so that
880                 // we can raise it later on.
881                 raiseController = controller;
882                 raiseFile = [files objectAtIndex:[idxSet firstIndex]];
883                 [[raiseFile retain] autorelease];
884             }
886             // Pass (ODB/Xcode/Spotlight) arguments to this process.
887             [localArgs setObject:[files objectsAtIndexes:idxSet]
888                           forKey:@"filenames"];
889             [self passArguments:localArgs toVimController:controller];
891             // Remove all the files that were open in this Vim process and
892             // create a new expression to evaluate.
893             [files removeObjectsAtIndexes:idxSet];
894             expr = [NSString stringWithFormat:
895                     @"map([\"%@\"],\"bufloaded(v:val)\")",
896                     [files componentsJoinedByString:@"\",\""]];
897         }
898     }
900     if (![files count] && raiseFile) {
901         // Raise the window containing the first file that was already open,
902         // and make sure that the tab containing that file is selected.  Only
903         // do this if there are no more files to open, otherwise sometimes the
904         // window with 'raiseFile' will be raised, other times it might be the
905         // window that will open with the files in the 'files' array.
906         raiseFile = [raiseFile stringByEscapingSpecialFilenameCharacters];
907         NSString *input = [NSString stringWithFormat:@"<C-\\><C-N>"
908             ":let oldswb=&swb|let &swb=\"useopen,usetab\"|"
909             "tab sb %@|let &swb=oldswb|unl oldswb|"
910             "cal foreground()|redr|f<CR>", raiseFile];
912         [raiseController addVimInput:input];
913     }
915     return files;
918 #if MM_HANDLE_XCODE_MOD_EVENT
919 - (void)handleXcodeModEvent:(NSAppleEventDescriptor *)event
920                  replyEvent:(NSAppleEventDescriptor *)reply
922 #if 0
923     // Xcode sends this event to query MacVim which open files have been
924     // modified.
925     NSLog(@"reply:%@", reply);
926     NSLog(@"event:%@", event);
928     NSEnumerator *e = [vimControllers objectEnumerator];
929     id vc;
930     while ((vc = [e nextObject])) {
931         DescType type = [reply descriptorType];
932         unsigned len = [[type data] length];
933         NSMutableData *data = [NSMutableData data];
935         [data appendBytes:&type length:sizeof(DescType)];
936         [data appendBytes:&len length:sizeof(unsigned)];
937         [data appendBytes:[reply data] length:len];
939         [vc sendMessage:XcodeModMsgID data:data];
940     }
941 #endif
943 #endif
945 - (int)findLaunchingProcessWithoutArguments
947     NSArray *keys = [pidArguments allKeysForObject:[NSNull null]];
948     if ([keys count] > 0) {
949         //NSLog(@"found launching process without arguments");
950         return [[keys objectAtIndex:0] intValue];
951     }
953     return 0;
956 - (MMVimController *)findUntitledWindow
958     NSEnumerator *e = [vimControllers objectEnumerator];
959     id vc;
960     while ((vc = [e nextObject])) {
961         // TODO: This is a moronic test...should query the Vim process if there
962         // are any open buffers or something like that instead.
963         NSString *title = [[[vc windowController] window] title];
964         if ([title hasPrefix:@"[No Name] - VIM"]) {
965             //NSLog(@"found untitled window");
966             return vc;
967         }
968     }
970     return nil;
973 - (NSMutableDictionary *)extractArgumentsFromOdocEvent:
974     (NSAppleEventDescriptor *)desc
976     NSMutableDictionary *dict = [NSMutableDictionary dictionary];
978     // 1. Extract ODB parameters (if any)
979     NSAppleEventDescriptor *odbdesc = desc;
980     if (![odbdesc paramDescriptorForKeyword:keyFileSender]) {
981         // The ODB paramaters may hide inside the 'keyAEPropData' descriptor.
982         odbdesc = [odbdesc paramDescriptorForKeyword:keyAEPropData];
983         if (![odbdesc paramDescriptorForKeyword:keyFileSender])
984             odbdesc = nil;
985     }
987     if (odbdesc) {
988         NSAppleEventDescriptor *p =
989                 [odbdesc paramDescriptorForKeyword:keyFileSender];
990         if (p)
991             [dict setObject:[NSNumber numberWithUnsignedInt:[p typeCodeValue]]
992                      forKey:@"remoteID"];
994         p = [odbdesc paramDescriptorForKeyword:keyFileCustomPath];
995         if (p)
996             [dict setObject:[p stringValue] forKey:@"remotePath"];
998         p = [odbdesc paramDescriptorForKeyword:keyFileSenderToken];
999         if (p)
1000             [dict setObject:p forKey:@"remotePath"];
1001     }
1003     // 2. Extract Xcode parameters (if any)
1004     NSAppleEventDescriptor *xcodedesc =
1005             [desc paramDescriptorForKeyword:keyAEPosition];
1006     if (xcodedesc) {
1007         NSRange range;
1008         MMSelectionRange *sr = (MMSelectionRange*)[[xcodedesc data] bytes];
1010         if (sr->lineNum < 0) {
1011             // Should select a range of lines.
1012             range.location = sr->startRange + 1;
1013             range.length = sr->endRange - sr->startRange + 1;
1014         } else {
1015             // Should only move cursor to a line.
1016             range.location = sr->lineNum + 1;
1017             range.length = 0;
1018         }
1020         [dict setObject:NSStringFromRange(range) forKey:@"selectionRange"];
1021     }
1023     // 3. Extract Spotlight search text (if any)
1024     NSAppleEventDescriptor *spotlightdesc = 
1025             [desc paramDescriptorForKeyword:keyAESearchText];
1026     if (spotlightdesc)
1027         [dict setObject:[spotlightdesc stringValue] forKey:@"searchText"];
1029     return dict;
1032 - (void)passArguments:(NSDictionary *)args toVimController:(MMVimController*)vc
1034     if (!args) return;
1036     // Pass filenames to open if required (the 'openFiles' argument can be used
1037     // to disallow opening of the files).
1038     NSArray *filenames = [args objectForKey:@"filenames"];
1039     if (filenames && [[args objectForKey:@"openFiles"] boolValue]) {
1040         NSString *tabDrop = buildTabDropCommand(filenames);
1041         [vc addVimInput:tabDrop];
1042     }
1044     // Pass ODB data
1045     if (filenames && [args objectForKey:@"remoteID"]) {
1046         [vc odbEdit:filenames
1047              server:[[args objectForKey:@"remoteID"] unsignedIntValue]
1048                path:[args objectForKey:@"remotePath"]
1049               token:[args objectForKey:@"remoteToken"]];
1050     }
1052     // Pass range of lines to select
1053     if ([args objectForKey:@"selectionRange"]) {
1054         NSRange selectionRange = NSRangeFromString(
1055                 [args objectForKey:@"selectionRange"]);
1056         [vc addVimInput:buildSelectRangeCommand(selectionRange)];
1057     }
1059     // Pass search text
1060     NSString *searchText = [args objectForKey:@"searchText"];
1061     if (searchText)
1062         [vc addVimInput:buildSearchTextCommand(searchText)];
1065 @end // MMAppController (Private)
1070 @implementation NSMenu (MMExtras)
1072 - (void)recurseSetAutoenablesItems:(BOOL)on
1074     [self setAutoenablesItems:on];
1076     int i, count = [self numberOfItems];
1077     for (i = 0; i < count; ++i) {
1078         NSMenuItem *item = [self itemAtIndex:i];
1079         [item setEnabled:YES];
1080         NSMenu *submenu = [item submenu];
1081         if (submenu) {
1082             [submenu recurseSetAutoenablesItems:on];
1083         }
1084     }
1087 @end  // NSMenu (MMExtras)
1092 @implementation NSNumber (MMExtras)
1093 - (int)tag
1095     return [self intValue];
1097 @end // NSNumber (MMExtras)
1102     static int
1103 executeInLoginShell(NSString *path, NSArray *args)
1105     // Start a login shell and execute the command 'path' with arguments 'args'
1106     // in the shell.  This ensures that user environment variables are set even
1107     // when MacVim was started from the Finder.
1109     int pid = -1;
1110     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
1112     // Determine which shell to use to execute the command.  The user
1113     // may decide which shell to use by setting a user default or the
1114     // $SHELL environment variable.
1115     NSString *shell = [ud stringForKey:MMLoginShellCommandKey];
1116     if (!shell || [shell length] == 0)
1117         shell = [[[NSProcessInfo processInfo] environment]
1118             objectForKey:@"SHELL"];
1119     if (!shell)
1120         shell = @"/bin/bash";
1122     //NSLog(@"shell = %@", shell);
1124     // Bash needs the '-l' flag to launch a login shell.  The user may add
1125     // flags by setting a user default.
1126     NSString *shellArgument = [ud stringForKey:MMLoginShellArgumentKey];
1127     if (!shellArgument || [shellArgument length] == 0) {
1128         if ([[shell lastPathComponent] isEqual:@"bash"])
1129             shellArgument = @"-l";
1130         else
1131             shellArgument = nil;
1132     }
1134     //NSLog(@"shellArgument = %@", shellArgument);
1136     // Build input string to pipe to the login shell.
1137     NSMutableString *input = [NSMutableString stringWithFormat:
1138             @"exec \"%@\"", path];
1139     if (args) {
1140         // Append all arguments, making sure they are properly quoted, even
1141         // when they contain single quotes.
1142         NSEnumerator *e = [args objectEnumerator];
1143         id obj;
1145         while ((obj = [e nextObject])) {
1146             NSMutableString *arg = [NSMutableString stringWithString:obj];
1147             [arg replaceOccurrencesOfString:@"'" withString:@"'\"'\"'"
1148                                     options:NSLiteralSearch
1149                                       range:NSMakeRange(0, [arg length])];
1150             [input appendFormat:@" '%@'", arg];
1151         }
1152     }
1154     // Build the argument vector used to start the login shell.
1155     NSString *shellArg0 = [NSString stringWithFormat:@"-%@",
1156              [shell lastPathComponent]];
1157     char *shellArgv[3] = { (char *)[shellArg0 UTF8String], NULL, NULL };
1158     if (shellArgument)
1159         shellArgv[1] = (char *)[shellArgument UTF8String];
1161     // Get the C string representation of the shell path before the fork since
1162     // we must not call Foundation functions after a fork.
1163     const char *shellPath = [shell fileSystemRepresentation];
1165     // Fork and execute the process.
1166     int ds[2];
1167     if (pipe(ds)) return -1;
1169     pid = fork();
1170     if (pid == -1) {
1171         return -1;
1172     } else if (pid == 0) {
1173         // Child process
1174         if (close(ds[1]) == -1) exit(255);
1175         if (dup2(ds[0], 0) == -1) exit(255);
1177         execv(shellPath, shellArgv);
1179         // Never reached unless execv fails
1180         exit(255);
1181     } else {
1182         // Parent process
1183         if (close(ds[0]) == -1) return -1;
1185         // Send input to execute to the child process
1186         [input appendString:@"\n"];
1187         int bytes = [input lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
1189         if (write(ds[1], [input UTF8String], bytes) != bytes) return -1;
1190         if (close(ds[1]) == -1) return -1;
1191     }
1193     return pid;