Open and save dialogs track the Vim pwd
[MacVim.git] / src / MacVim / MMAppController.m
blob5a301cadeff8da626243cd3ed0a7175586b16d96
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         [NSNumber numberWithBool:YES],  MMDialogsTrackPwdKey,
128         nil];
130     [[NSUserDefaults standardUserDefaults] registerDefaults:dict];
132     NSArray *types = [NSArray arrayWithObject:NSStringPboardType];
133     [NSApp registerServicesMenuSendTypes:types returnTypes:types];
136 - (id)init
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]
149                                                       sendPort:nil];
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 '%@'",
166                     name);
167             [connection release];  connection = nil;
168         }
169     }
171     return self;
174 - (void)dealloc
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;
184     [super dealloc];
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.
193     // 
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:)
206                         keyEquivalent:@""];
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]
215             setEventHandler:self
216                 andSelector:@selector(handleXcodeModEvent:replyEvent:)
217               forEventClass:'KAHL'
218                  andEventID:'MOD '];
219 #endif
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)
238         return NO;
239     else if ([desc eventID] == kAEReopenApplication
240             && (untitledWindowFlag & MMUntitledWindowOnReopen) == 0)
241         return NO;
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])
248         return NO;
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)
253         return NO;
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];
263     return YES;
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
272     //
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
275     // window is opened.
276     //
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]) {
290         MMVimController *vc;
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]];
305         }
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];
312         } else {
313             // Open files in a launching Vim process or start a new process.
314             int pid = [self findLaunchingProcessWithoutArguments];
315             if (!pid) {
316                 // Pass the filenames to the process straight away.
317                 //
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];
328                 if (-1 == pid) {
329                     // TODO: Notify user of failure?
330                     [NSApp replyToOpenOrPrint:
331                         NSApplicationDelegateReplyFailure];
332                     return;
333                 }
335                 // Make sure these files aren't opened again when
336                 // connectBackend:pid: is called.
337                 [arguments setObject:[NSNumber numberWithBool:NO]
338                               forKey:@"openFiles"];
339             }
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]];
348         }
349     }
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];
375     id window;
376     while ((window = [e nextObject])) {
377         if ([window isDocumentEdited]) {
378             modifiedBuffers = YES;
379             break;
380         }
381     }
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;
395         [alert release];
396     } else {
397         // No unmodified buffers, but give a warning if there are multiple
398         // windows and/or tabs open.
399         int numWindows = [vimControllers count];
400         int numTabs = 0;
402         // Count the number of open tabs
403         e = [vimControllers objectEnumerator];
404         id vc;
405         while ((vc = [e nextObject])) {
406             NSString *eval = [vc evaluateVimExpression:@"tabpagenr('$')"];
407             if (eval) {
408                 int count = [eval intValue];
409                 if (count > 0 && count < INT_MAX)
410                     numTabs += count;
411             }
412         }
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];
427                 else
428                     info = [NSString stringWithFormat:@"There are %d windows "
429                         "open in MacVim. Do you want to quit anyway?",
430                         numWindows];
432             } else {
433                 info = [NSString stringWithFormat:@"There are %d tabs open "
434                     "in MacVim. Do you want to quit anyway?", numTabs];
435             }
437             [alert setInformativeText:info];
439             if ([alert runModal] != NSAlertFirstButtonReturn)
440                 reply = NSTerminateCancel;
442             [alert release];
443         }
444     }
447     // Tell all Vim processes to terminate now (otherwise they'll leave swap
448     // files behind).
449     if (NSTerminateNow == reply) {
450         e = [vimControllers objectEnumerator];
451         id vc;
452         while ((vc = [e nextObject]))
453             [vc sendMessage:TerminateNowMsgID data:nil];
454     }
456     return reply;
459 - (void)applicationWillTerminate:(NSNotification *)notification
461 #if MM_HANDLE_XCODE_MOD_EVENT
462     [[NSAppleEventManager sharedAppleEventManager]
463             removeEventHandlerForEventClass:'KAHL'
464                                  andEventID:'MOD '];
465 #endif
467     // This will invalidate all connections (since they were spawned from this
468     // connection).
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];
478         if (pid > 0)
479             kill(pid, SIGINT);
480     }
482     if (fontContainerRef) {
483         ATSFontDeactivate(fontContainerRef, NULL, kATSOptionFlagsDefault);
484         fontContainerRef = 0;
485     }
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];
508         }
509     }
512 - (void)windowControllerWillOpen:(MMWindowController *)windowController
514     NSPoint topLeft = NSZeroPoint;
515     NSWindow *topWin = [[[self topmostVimController] windowController] window];
516     NSWindow *win = [windowController window];
518     if (!win) return;
520     // If there is a window belonging to a Vim process, cascade from it,
521     // otherwise use the autosaved window position (if any).
522     if (topWin) {
523         NSRect frame = [topWin frame];
524         topLeft = NSMakePoint(frame.origin.x, NSMaxY(frame));
525     } else {
526         NSString *topLeftString = [[NSUserDefaults standardUserDefaults]
527             stringForKey:MMTopLeftPointKey];
528         if (topLeftString)
529             topLeft = NSPointFromString(topLeftString);
530     }
532     if (!NSEqualPoints(topLeft, NSZeroPoint)) {
533         if (topWin)
534             topLeft = [win cascadeTopLeftFromPoint:topLeft];
536         [win setFrameTopLeftPoint:topLeft];
537     }
539     if (openSelectionString) {
540         // TODO: Pass this as a parameter instead!  Get rid of
541         // 'openSelectionString' etc.
542         //
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;
548     }
551 - (IBAction)newWindow:(id)sender
553     [self launchVimProcessWithArguments:nil];
556 - (IBAction)fileOpen:(id)sender
558     NSString *dir = nil;
559     BOOL trackPwd = [[NSUserDefaults standardUserDefaults]
560             boolForKey:MMDialogsTrackPwdKey];
561     if (trackPwd) {
562         MMVimController *vc = [self keyVimController];
563         if (vc) dir = [[vc vimState] objectForKey:@"pwd"];
564     }
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];
576     if (!count) return;
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])
582             break;
583     }
585     if (i < count) {
586         if (++i >= count)
587             i = 0;
588         MMVimController *vc = [vimControllers objectAtIndex:i];
589         [[vc windowController] showWindow:self];
590     }
593 - (IBAction)selectPreviousWindow:(id)sender
595     unsigned i, count = [vimControllers count];
596     if (!count) return;
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])
602             break;
603     }
605     if (i < count) {
606         if (i > 0) {
607             --i;
608         } else {
609             i = count - 1;
610         }
611         MMVimController *vc = [vimControllers objectAtIndex:i];
612         [[vc windowController] showWindow:self];
613     }
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
641                pid:(int)pid
643     //NSLog(@"Connect backend (pid=%d)", pid);
644     NSNumber *pidKey = [NSNumber numberWithInt:pid];
645     MMVimController *vc = nil;
647     @try {
648         [(NSDistantObject*)backend
649                 setProtocolForProxy:@protocol(MMBackendProtocol)];
651         vc = [[[MMVimController alloc]
652             initWithBackend:backend pid:pid recentFiles:recentFilesMenuItem]
653             autorelease];
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];
660         }
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];
676         if (args)
677             [pidArguments removeObjectForKey:pidKey];
679         return vc;
680     }
682     @catch (NSException *e) {
683         NSLog(@"Exception caught in %s: \"%@\"", _cmd, e);
685         if (vc)
686             [vimControllers removeObject:vc];
688         [pidArguments removeObjectForKey:pidKey];
689     }
691     return nil;
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]];
703     }
705     return array;
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");
721         return;
722     }
724     MMVimController *vc = [self topmostVimController];
725     if (vc) {
726         // Open a new tab first, since dropString: does not do this.
727         [vc sendMessage:AddNewTabMsgID data:nil];
728         [vc dropString:[pboard stringForType:NSStringPboardType]];
729     } else {
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];
738     }
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");
747         return;
748     }
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];
763         if (vc) {
764             [vc dropFiles:filenames forceOpen:YES];
765         } else {
766             [self application:NSApp openFiles:filenames];
767         }
768     }
771 @end // MMAppController (MMServices)
776 @implementation MMAppController (Private)
778 - (MMVimController *)keyVimController
780     NSWindow *keyWindow = [NSApp keyWindow];
781     if (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])
786                 return vc;
787         }
788     }
790     return nil;
793 - (MMVimController *)topmostVimController
795     // Find the topmost visible window which has an associated vim controller.
796     NSEnumerator *e = [[NSApp orderedWindows] objectEnumerator];
797     id window;
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])
803                 return vc;
804         }
805     }
807     return nil;
810 - (int)launchVimProcessWithArguments:(NSArray *)args
812     int pid = -1;
813     NSString *path = [[NSBundle mainBundle] pathForAuxiliaryExecutable:@"Vim"];
815     if (!path) {
816         NSLog(@"ERROR: Vim executable could not be found inside app bundle!");
817         return -1;
818     }
820     NSArray *taskArgs = [NSArray arrayWithObjects:@"-g", @"-f", nil];
821     if (args)
822         taskArgs = [taskArgs arrayByAddingObjectsFromArray:args];
824     BOOL useLoginShell = [[NSUserDefaults standardUserDefaults]
825             boolForKey:MMLoginShellKey];
826     if (useLoginShell) {
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);
830     } else {
831         // Run process directly:
832         //   Vim -g -f args
833         NSTask *task = [NSTask launchedTaskWithLaunchPath:path
834                                                 arguments:taskArgs];
835         pid = task ? [task processIdentifier] : -1;
836     }
838     if (-1 != pid) {
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.
842         if (!args)
843             [pidArguments setObject:[NSNull null]
844                              forKey:[NSNumber numberWithInt:pid]];
845     } else {
846         NSLog(@"WARNING: %s%@ failed (useLoginShell=%d)", _cmd, args,
847                 useLoginShell);
848     }
850     return pid;
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;
868         }
869     }
871     if (firstMissingFile) {
872         NSAlert *alert = [[NSAlert alloc] init];
873         [alert addButtonWithTitle:@"OK"];
875         NSString *text;
876         if ([files count] >= count-1) {
877             [alert setMessageText:@"File not found"];
878             text = [NSString stringWithFormat:@"Could not open file with "
879                 "name %@.", firstMissingFile];
880         } else {
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];
885         }
887         [alert setInformativeText:text];
888         [alert setAlertStyle:NSWarningAlertStyle];
890         [alert runModal];
891         [alert release];
893         [NSApp replyToOpenOrPrint:NSApplicationDelegateReplyFailure];
894     }
896     return files;
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];
926         if (!eval) continue;
928         NSIndexSet *idxSet = [NSIndexSet indexSetWithVimList:eval];
929         if ([idxSet count]) {
930             if (!raiseFile) {
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];
936             }
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:@"\",\""]];
949         }
950     }
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];
965     }
967     return files;
970 #if MM_HANDLE_XCODE_MOD_EVENT
971 - (void)handleXcodeModEvent:(NSAppleEventDescriptor *)event
972                  replyEvent:(NSAppleEventDescriptor *)reply
974 #if 0
975     // Xcode sends this event to query MacVim which open files have been
976     // modified.
977     NSLog(@"reply:%@", reply);
978     NSLog(@"event:%@", event);
980     NSEnumerator *e = [vimControllers objectEnumerator];
981     id vc;
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];
992     }
993 #endif
995 #endif
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];
1003     }
1005     return 0;
1008 - (MMVimController *)findUntitledWindow
1010     NSEnumerator *e = [vimControllers objectEnumerator];
1011     id vc;
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");
1018             return vc;
1019         }
1020     }
1022     return nil;
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])
1036             odbdesc = nil;
1037     }
1039     if (odbdesc) {
1040         NSAppleEventDescriptor *p =
1041                 [odbdesc paramDescriptorForKeyword:keyFileSender];
1042         if (p)
1043             [dict setObject:[NSNumber numberWithUnsignedInt:[p typeCodeValue]]
1044                      forKey:@"remoteID"];
1046         p = [odbdesc paramDescriptorForKeyword:keyFileCustomPath];
1047         if (p)
1048             [dict setObject:[p stringValue] forKey:@"remotePath"];
1050         p = [odbdesc paramDescriptorForKeyword:keyFileSenderToken];
1051         if (p)
1052             [dict setObject:p forKey:@"remotePath"];
1053     }
1055     // 2. Extract Xcode parameters (if any)
1056     NSAppleEventDescriptor *xcodedesc =
1057             [desc paramDescriptorForKeyword:keyAEPosition];
1058     if (xcodedesc) {
1059         NSRange range;
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;
1066         } else {
1067             // Should only move cursor to a line.
1068             range.location = sr->lineNum + 1;
1069             range.length = 0;
1070         }
1072         [dict setObject:NSStringFromRange(range) forKey:@"selectionRange"];
1073     }
1075     // 3. Extract Spotlight search text (if any)
1076     NSAppleEventDescriptor *spotlightdesc = 
1077             [desc paramDescriptorForKeyword:keyAESearchText];
1078     if (spotlightdesc)
1079         [dict setObject:[spotlightdesc stringValue] forKey:@"searchText"];
1081     return dict;
1084 - (void)passArguments:(NSDictionary *)args toVimController:(MMVimController*)vc
1086     if (!args) return;
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];
1094     }
1096     // Pass ODB data
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"]];
1102     }
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)];
1109     }
1111     // Pass search text
1112     NSString *searchText = [args objectForKey:@"searchText"];
1113     if (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];
1133         if (submenu) {
1134             [submenu recurseSetAutoenablesItems:on];
1135         }
1136     }
1139 @end  // NSMenu (MMExtras)
1144 @implementation NSNumber (MMExtras)
1145 - (int)tag
1147     return [self intValue];
1149 @end // NSNumber (MMExtras)
1154     static int
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.
1161     int pid = -1;
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"];
1171     if (!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";
1182         else
1183             shellArgument = nil;
1184     }
1186     //NSLog(@"shellArgument = %@", shellArgument);
1188     // Build input string to pipe to the login shell.
1189     NSMutableString *input = [NSMutableString stringWithFormat:
1190             @"exec \"%@\"", path];
1191     if (args) {
1192         // Append all arguments, making sure they are properly quoted, even
1193         // when they contain single quotes.
1194         NSEnumerator *e = [args objectEnumerator];
1195         id obj;
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];
1203         }
1204     }
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 };
1210     if (shellArgument)
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.
1218     int ds[2];
1219     if (pipe(ds)) return -1;
1221     pid = fork();
1222     if (pid == -1) {
1223         return -1;
1224     } else if (pid == 0) {
1225         // Child process
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
1232         exit(255);
1233     } else {
1234         // Parent process
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;
1243     }
1245     return pid;