Add "Recent Files" menu
[MacVim.git] / src / MacVim / MMAppController.m
blobd3755fc444ee38e301e7c5d072fa78c44cc37aa3
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;
174     [recentFilesMenuItem release];  recentFilesMenuItem = nil;
176     [super dealloc];
179 - (void)applicationWillFinishLaunching:(NSNotification *)notification
181     // Create the "Open Recent" menu. See
182     // http://lapcatsoftware.com/blog/2007/07/10/working-without-a-nib-part-5-open-recent-menu/
183     // and http://www.cocoabuilder.com/archive/message/cocoa/2007/8/15/187793
184     // for more information.
185     // 
186     // The menu needs to be created and be added to a toplevel menu in
187     // applicationWillFinishLaunching at the latest, otherwise it doesn't work.
189     recentFilesMenuItem = [[NSMenuItem alloc] initWithTitle:@"Open Recent"
190                                               action:nil keyEquivalent:@""];
192     NSMenu *recentFilesMenu = [[NSMenu alloc] initWithTitle:@"Open Recent"];
193     [recentFilesMenu performSelector:@selector(_setMenuName:)
194                           withObject:@"NSRecentDocumentsMenu"];
196     [recentFilesMenu addItemWithTitle:@"Clear Menu"
197                                action:@selector(clearRecentDocuments:)
198                         keyEquivalent:@""];
199     [recentFilesMenuItem setSubmenu:recentFilesMenu];
200     [recentFilesMenu release];  // the menu is retained by recentFilesMenuItem
201     [recentFilesMenuItem setTag:-1];  // must not be 0
203     [[[[NSApp mainMenu] itemWithTitle:@"File"] submenu] addItem:recentFilesMenuItem];
205 #if MM_HANDLE_XCODE_MOD_EVENT
206     [[NSAppleEventManager sharedAppleEventManager]
207             setEventHandler:self
208                 andSelector:@selector(handleXcodeModEvent:replyEvent:)
209               forEventClass:'KAHL'
210                  andEventID:'MOD '];
211 #endif
214 - (void)applicationDidFinishLaunching:(NSNotification *)notification
216     [NSApp setServicesProvider:self];
219 - (BOOL)applicationShouldOpenUntitledFile:(NSApplication *)sender
221     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
222     NSAppleEventManager *aem = [NSAppleEventManager sharedAppleEventManager];
223     NSAppleEventDescriptor *desc = [aem currentAppleEvent];
225     // The user default MMUntitledWindow can be set to control whether an
226     // untitled window should open on 'Open' and 'Reopen' events.
227     int untitledWindowFlag = [ud integerForKey:MMUntitledWindowKey];
228     if ([desc eventID] == kAEOpenApplication
229             && (untitledWindowFlag & MMUntitledWindowOnOpen) == 0)
230         return NO;
231     else if ([desc eventID] == kAEReopenApplication
232             && (untitledWindowFlag & MMUntitledWindowOnReopen) == 0)
233         return NO;
235     // When a process is started from the command line, the 'Open' event will
236     // contain a parameter to surpress the opening of an untitled window.
237     desc = [desc paramDescriptorForKeyword:keyAEPropData];
238     desc = [desc paramDescriptorForKeyword:keyMMUntitledWindow];
239     if (desc && ![desc booleanValue])
240         return NO;
242     // Never open an untitled window if there is at least one open window or if
243     // there are processes that are currently launching.
244     if ([vimControllers count] > 0 || [pidArguments count] > 0)
245         return NO;
247     // NOTE!  This way it possible to start the app with the command-line
248     // argument '-nowindow yes' and no window will be opened by default.
249     return ![ud boolForKey:MMNoWindowKey];
252 - (BOOL)applicationOpenUntitledFile:(NSApplication *)sender
254     [self newWindow:self];
255     return YES;
258 - (void)application:(NSApplication *)sender openFiles:(NSArray *)filenames
260     // Opening files works like this:
261     //  a) extract ODB/Xcode/Spotlight parameters from the current Apple event
262     //  b) filter out any already open files (see filterOpenFiles::)
263     //  c) open any remaining files
264     //
265     // A file is opened in an untitled window if there is one (it may be
266     // currently launching, or it may already be visible), otherwise a new
267     // window is opened.
268     //
269     // Each launching Vim process has a dictionary of arguments that are passed
270     // to the process when in checks in (via connectBackend:pid:).  The
271     // arguments for each launching process can be looked up by its PID (in the
272     // pidArguments dictionary).
274     NSMutableDictionary *arguments = [self extractArgumentsFromOdocEvent:
275             [[NSAppleEventManager sharedAppleEventManager] currentAppleEvent]];
277     // Filter out files that are already open
278     filenames = [self filterOpenFiles:filenames arguments:arguments];
280     // Open any files that remain
281     if ([filenames count]) {
282         MMVimController *vc;
283         BOOL openInTabs = [[NSUserDefaults standardUserDefaults]
284             boolForKey:MMOpenFilesInTabsKey];
286         [arguments setObject:filenames forKey:@"filenames"];
287         [arguments setObject:[NSNumber numberWithBool:YES] forKey:@"openFiles"];
289         // Add file names to "Recent Files" menu.
290         int i, count = [filenames count];
291         for (i = 0; i < count; ++i) {
292             // Don't add files that are being edited remotely (using ODB).
293             if ([arguments objectForKey:@"remoteID"]) continue;
295             NSURL *url = [NSURL fileURLWithPath:[filenames objectAtIndex:i]];
296             if (!url) continue;
297             [[NSDocumentController sharedDocumentController]
298                     noteNewRecentDocumentURL:url];
299         }
301         if ((openInTabs && (vc = [self topmostVimController]))
302                || (vc = [self findUntitledWindow])) {
303             // Open files in an already open window.
304             [[[vc windowController] window] makeKeyAndOrderFront:self];
305             [self passArguments:arguments toVimController:vc];
306         } else {
307             // Open files in a launching Vim process or start a new process.
308             int pid = [self findLaunchingProcessWithoutArguments];
309             if (!pid) {
310                 // Pass the filenames to the process straight away.
311                 //
312                 // TODO: It would be nicer if all arguments were passed to the
313                 // Vim process in connectBackend::, but if we don't pass the
314                 // filename arguments here, the window 'flashes' once when it
315                 // opens.  This is due to the 'welcome' screen first being
316                 // displayed, then quickly thereafter the files are opened.
317                 NSArray *fileArgs = [NSArray arrayWithObject:@"-p"];
318                 fileArgs = [fileArgs arrayByAddingObjectsFromArray:filenames];
320                 pid = [self launchVimProcessWithArguments:fileArgs];
322                 if (-1 == pid) {
323                     // TODO: Notify user of failure?
324                     [NSApp replyToOpenOrPrint:
325                         NSApplicationDelegateReplyFailure];
326                     return;
327                 }
329                 // Make sure these files aren't opened again when
330                 // connectBackend:pid: is called.
331                 [arguments setObject:[NSNumber numberWithBool:NO]
332                               forKey:@"openFiles"];
333             }
335             // TODO: If the Vim process fails to start, or if it changes PID,
336             // then the memory allocated for these parameters will leak.
337             // Ensure that this cannot happen or somehow detect it.
339             if ([arguments count] > 0)
340                 [pidArguments setObject:arguments
341                                  forKey:[NSNumber numberWithInt:pid]];
342         }
343     }
345     [NSApp replyToOpenOrPrint:NSApplicationDelegateReplySuccess];
346     // NSApplicationDelegateReplySuccess = 0,
347     // NSApplicationDelegateReplyCancel = 1,
348     // NSApplicationDelegateReplyFailure = 2
351 - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender
353     return [[NSUserDefaults standardUserDefaults]
354             boolForKey:MMTerminateAfterLastWindowClosedKey];
357 - (NSApplicationTerminateReply)applicationShouldTerminate:
358     (NSApplication *)sender
360     // TODO: Follow Apple's guidelines for 'Graceful Application Termination'
361     // (in particular, allow user to review changes and save).
362     int reply = NSTerminateNow;
363     BOOL modifiedBuffers = NO;
365     // Go through windows, checking for modified buffers.  (Each Vim process
366     // tells MacVim when any buffer has been modified and MacVim sets the
367     // 'documentEdited' flag of the window correspondingly.)
368     NSEnumerator *e = [[NSApp windows] objectEnumerator];
369     id window;
370     while ((window = [e nextObject])) {
371         if ([window isDocumentEdited]) {
372             modifiedBuffers = YES;
373             break;
374         }
375     }
377     if (modifiedBuffers) {
378         NSAlert *alert = [[NSAlert alloc] init];
379         [alert setAlertStyle:NSWarningAlertStyle];
380         [alert addButtonWithTitle:@"Quit"];
381         [alert addButtonWithTitle:@"Cancel"];
382         [alert setMessageText:@"Quit without saving?"];
383         [alert setInformativeText:@"There are modified buffers, "
384             "if you quit now all changes will be lost.  Quit anyway?"];
386         if ([alert runModal] != NSAlertFirstButtonReturn)
387             reply = NSTerminateCancel;
389         [alert release];
390     } else {
391         // No unmodified buffers, but give a warning if there are multiple
392         // windows and/or tabs open.
393         int numWindows = [vimControllers count];
394         int numTabs = 0;
396         // Count the number of open tabs
397         e = [vimControllers objectEnumerator];
398         id vc;
399         while ((vc = [e nextObject])) {
400             NSString *eval = [vc evaluateVimExpression:@"tabpagenr('$')"];
401             if (eval) {
402                 int count = [eval intValue];
403                 if (count > 0 && count < INT_MAX)
404                     numTabs += count;
405             }
406         }
408         if (numWindows > 1 || numTabs > 1) {
409             NSAlert *alert = [[NSAlert alloc] init];
410             [alert setAlertStyle:NSWarningAlertStyle];
411             [alert addButtonWithTitle:@"Quit"];
412             [alert addButtonWithTitle:@"Cancel"];
413             [alert setMessageText:@"Are you sure you want to quit MacVim?"];
415             NSString *info = nil;
416             if (numWindows > 1) {
417                 if (numTabs > numWindows)
418                     info = [NSString stringWithFormat:@"There are %d windows "
419                         "open in MacVim, with a total of %d tabs. Do you want "
420                         "to quit anyway?", numWindows, numTabs];
421                 else
422                     info = [NSString stringWithFormat:@"There are %d windows "
423                         "open in MacVim. Do you want to quit anyway?",
424                         numWindows];
426             } else {
427                 info = [NSString stringWithFormat:@"There are %d tabs open "
428                     "in MacVim. Do you want to quit anyway?", numTabs];
429             }
431             [alert setInformativeText:info];
433             if ([alert runModal] != NSAlertFirstButtonReturn)
434                 reply = NSTerminateCancel;
436             [alert release];
437         }
438     }
441     // Tell all Vim processes to terminate now (otherwise they'll leave swap
442     // files behind).
443     if (NSTerminateNow == reply) {
444         e = [vimControllers objectEnumerator];
445         id vc;
446         while ((vc = [e nextObject]))
447             [vc sendMessage:TerminateNowMsgID data:nil];
448     }
450     return reply;
453 - (void)applicationWillTerminate:(NSNotification *)notification
455 #if MM_HANDLE_XCODE_MOD_EVENT
456     [[NSAppleEventManager sharedAppleEventManager]
457             removeEventHandlerForEventClass:'KAHL'
458                                  andEventID:'MOD '];
459 #endif
461     // This will invalidate all connections (since they were spawned from the
462     // default connection).
463     [[NSConnection defaultConnection] invalidate];
465     // Send a SIGINT to all running Vim processes, so that they are sure to
466     // receive the connectionDidDie: notification (a process has to be checking
467     // the run-loop for this to happen).
468     unsigned i, count = [vimControllers count];
469     for (i = 0; i < count; ++i) {
470         MMVimController *controller = [vimControllers objectAtIndex:i];
471         int pid = [controller pid];
472         if (pid > 0)
473             kill(pid, SIGINT);
474     }
476     if (fontContainerRef) {
477         ATSFontDeactivate(fontContainerRef, NULL, kATSOptionFlagsDefault);
478         fontContainerRef = 0;
479     }
481     [NSApp setDelegate:nil];
484 - (void)removeVimController:(id)controller
486     //NSLog(@"%s%@", _cmd, controller);
488     [[controller windowController] close];
490     [vimControllers removeObject:controller];
492     if (![vimControllers count]) {
493         // Turn on autoenabling of menus (because no Vim is open to handle it),
494         // but do not touch the MacVim menu.  Note that the menus must be
495         // enabled first otherwise autoenabling does not work.
496         NSMenu *mainMenu = [NSApp mainMenu];
497         int i, count = [mainMenu numberOfItems];
498         for (i = 1; i < count; ++i) {
499             NSMenuItem *item = [mainMenu itemAtIndex:i];
500             [item setEnabled:YES];
501             [[item submenu] recurseSetAutoenablesItems:YES];
502         }
503     }
506 - (void)windowControllerWillOpen:(MMWindowController *)windowController
508     NSPoint topLeft = NSZeroPoint;
509     NSWindow *topWin = [[[self topmostVimController] windowController] window];
510     NSWindow *win = [windowController window];
512     if (!win) return;
514     // If there is a window belonging to a Vim process, cascade from it,
515     // otherwise use the autosaved window position (if any).
516     if (topWin) {
517         NSRect frame = [topWin frame];
518         topLeft = NSMakePoint(frame.origin.x, NSMaxY(frame));
519     } else {
520         NSString *topLeftString = [[NSUserDefaults standardUserDefaults]
521             stringForKey:MMTopLeftPointKey];
522         if (topLeftString)
523             topLeft = NSPointFromString(topLeftString);
524     }
526     if (!NSEqualPoints(topLeft, NSZeroPoint)) {
527         if (topWin)
528             topLeft = [win cascadeTopLeftFromPoint:topLeft];
530         [win setFrameTopLeftPoint:topLeft];
531     }
533     if (openSelectionString) {
534         // TODO: Pass this as a parameter instead!  Get rid of
535         // 'openSelectionString' etc.
536         //
537         // There is some text to paste into this window as a result of the
538         // services menu "Open selection ..." being used.
539         [[windowController vimController] dropString:openSelectionString];
540         [openSelectionString release];
541         openSelectionString = nil;
542     }
545 - (IBAction)newWindow:(id)sender
547     [self launchVimProcessWithArguments:nil];
550 - (IBAction)fileOpen:(id)sender
552     NSOpenPanel *panel = [NSOpenPanel openPanel];
553     [panel setAllowsMultipleSelection:YES];
555     int result = [panel runModalForTypes:nil];
556     if (NSOKButton == result)
557         [self application:NSApp openFiles:[panel filenames]];
560 - (IBAction)selectNextWindow:(id)sender
562     unsigned i, count = [vimControllers count];
563     if (!count) return;
565     NSWindow *keyWindow = [NSApp keyWindow];
566     for (i = 0; i < count; ++i) {
567         MMVimController *vc = [vimControllers objectAtIndex:i];
568         if ([[[vc windowController] window] isEqual:keyWindow])
569             break;
570     }
572     if (i < count) {
573         if (++i >= count)
574             i = 0;
575         MMVimController *vc = [vimControllers objectAtIndex:i];
576         [[vc windowController] showWindow:self];
577     }
580 - (IBAction)selectPreviousWindow:(id)sender
582     unsigned i, count = [vimControllers count];
583     if (!count) return;
585     NSWindow *keyWindow = [NSApp keyWindow];
586     for (i = 0; i < count; ++i) {
587         MMVimController *vc = [vimControllers objectAtIndex:i];
588         if ([[[vc windowController] window] isEqual:keyWindow])
589             break;
590     }
592     if (i < count) {
593         if (i > 0) {
594             --i;
595         } else {
596             i = count - 1;
597         }
598         MMVimController *vc = [vimControllers objectAtIndex:i];
599         [[vc windowController] showWindow:self];
600     }
603 - (IBAction)fontSizeUp:(id)sender
605     [[NSFontManager sharedFontManager] modifyFont:
606             [NSNumber numberWithInt:NSSizeUpFontAction]];
609 - (IBAction)fontSizeDown:(id)sender
611     [[NSFontManager sharedFontManager] modifyFont:
612             [NSNumber numberWithInt:NSSizeDownFontAction]];
615 - (IBAction)orderFrontPreferencePanel:(id)sender
617     [[MMPreferenceController sharedPrefsWindowController] showWindow:self];
620 - (IBAction)openWebsite:(id)sender
622     [[NSWorkspace sharedWorkspace] openURL:
623             [NSURL URLWithString:MMWebsiteString]];
626 - (byref id <MMFrontendProtocol>)
627     connectBackend:(byref in id <MMBackendProtocol>)backend
628                pid:(int)pid
630     //NSLog(@"Connect backend (pid=%d)", pid);
631     NSNumber *pidKey = [NSNumber numberWithInt:pid];
632     MMVimController *vc = nil;
634     @try {
635         [(NSDistantObject*)backend
636                 setProtocolForProxy:@protocol(MMBackendProtocol)];
638         vc = [[[MMVimController alloc]
639             initWithBackend:backend pid:pid recentFiles:recentFilesMenuItem]
640             autorelease];
642         if (![vimControllers count]) {
643             // The first window autosaves its position.  (The autosaving
644             // features of Cocoa are not used because we need more control over
645             // what is autosaved and when it is restored.)
646             [[vc windowController] setWindowAutosaveKey:MMTopLeftPointKey];
647         }
649         [vimControllers addObject:vc];
651         id args = [pidArguments objectForKey:pidKey];
652         if (args && [NSNull null] != args)
653             [self passArguments:args toVimController:vc];
655         // HACK!  MacVim does not get activated if it is launched from the
656         // terminal, so we forcibly activate here unless it is an untitled
657         // window opening.  Untitled windows are treated differently, else
658         // MacVim would steal the focus if another app was activated while the
659         // untitled window was loading.
660         if (!args || args != [NSNull null])
661             [NSApp activateIgnoringOtherApps:YES];
663         if (args)
664             [pidArguments removeObjectForKey:pidKey];
666         return vc;
667     }
669     @catch (NSException *e) {
670         NSLog(@"Exception caught in %s: \"%@\"", _cmd, e);
672         if (vc)
673             [vimControllers removeObject:vc];
675         [pidArguments removeObjectForKey:pidKey];
676     }
678     return nil;
681 - (NSArray *)serverList
683     NSMutableArray *array = [NSMutableArray array];
685     unsigned i, count = [vimControllers count];
686     for (i = 0; i < count; ++i) {
687         MMVimController *controller = [vimControllers objectAtIndex:i];
688         if ([controller serverName])
689             [array addObject:[controller serverName]];
690     }
692     return array;
695 @end // MMAppController
700 @implementation MMAppController (MMServices)
702 - (void)openSelection:(NSPasteboard *)pboard userData:(NSString *)userData
703                 error:(NSString **)error
705     if (![[pboard types] containsObject:NSStringPboardType]) {
706         NSLog(@"WARNING: Pasteboard contains no object of type "
707                 "NSStringPboardType");
708         return;
709     }
711     MMVimController *vc = [self topmostVimController];
712     if (vc) {
713         // Open a new tab first, since dropString: does not do this.
714         [vc sendMessage:AddNewTabMsgID data:nil];
715         [vc dropString:[pboard stringForType:NSStringPboardType]];
716     } else {
717         // NOTE: There is no window to paste the selection into, so save the
718         // text, open a new window, and paste the text when the next window
719         // opens.  (If this is called several times in a row, then all but the
720         // last call might be ignored.)
721         if (openSelectionString) [openSelectionString release];
722         openSelectionString = [[pboard stringForType:NSStringPboardType] copy];
724         [self newWindow:self];
725     }
728 - (void)openFile:(NSPasteboard *)pboard userData:(NSString *)userData
729            error:(NSString **)error
731     if (![[pboard types] containsObject:NSStringPboardType]) {
732         NSLog(@"WARNING: Pasteboard contains no object of type "
733                 "NSStringPboardType");
734         return;
735     }
737     // TODO: Parse multiple filenames and create array with names.
738     NSString *string = [pboard stringForType:NSStringPboardType];
739     string = [string stringByTrimmingCharactersInSet:
740             [NSCharacterSet whitespaceAndNewlineCharacterSet]];
741     string = [string stringByStandardizingPath];
743     NSArray *filenames = [self filterFilesAndNotify:
744             [NSArray arrayWithObject:string]];
745     if ([filenames count] > 0) {
746         MMVimController *vc = nil;
747         if (userData && [userData isEqual:@"Tab"])
748             vc = [self topmostVimController];
750         if (vc) {
751             [vc dropFiles:filenames forceOpen:YES];
752         } else {
753             [self application:NSApp openFiles:filenames];
754         }
755     }
758 @end // MMAppController (MMServices)
763 @implementation MMAppController (Private)
765 - (MMVimController *)keyVimController
767     NSWindow *keyWindow = [NSApp keyWindow];
768     if (keyWindow) {
769         unsigned i, count = [vimControllers count];
770         for (i = 0; i < count; ++i) {
771             MMVimController *vc = [vimControllers objectAtIndex:i];
772             if ([[[vc windowController] window] isEqual:keyWindow])
773                 return vc;
774         }
775     }
777     return nil;
780 - (MMVimController *)topmostVimController
782     // Find the topmost visible window which has an associated vim controller.
783     NSEnumerator *e = [[NSApp orderedWindows] objectEnumerator];
784     id window;
785     while ((window = [e nextObject]) && [window isVisible]) {
786         unsigned i, count = [vimControllers count];
787         for (i = 0; i < count; ++i) {
788             MMVimController *vc = [vimControllers objectAtIndex:i];
789             if ([[[vc windowController] window] isEqual:window])
790                 return vc;
791         }
792     }
794     return nil;
797 - (int)launchVimProcessWithArguments:(NSArray *)args
799     int pid = -1;
800     NSString *path = [[NSBundle mainBundle] pathForAuxiliaryExecutable:@"Vim"];
802     if (!path) {
803         NSLog(@"ERROR: Vim executable could not be found inside app bundle!");
804         return -1;
805     }
807     NSArray *taskArgs = [NSArray arrayWithObjects:@"-g", @"-f", nil];
808     if (args)
809         taskArgs = [taskArgs arrayByAddingObjectsFromArray:args];
811     BOOL useLoginShell = [[NSUserDefaults standardUserDefaults]
812             boolForKey:MMLoginShellKey];
813     if (useLoginShell) {
814         // Run process with a login shell, roughly:
815         //   echo "exec Vim -g -f args" | ARGV0=-`basename $SHELL` $SHELL [-l]
816         pid = executeInLoginShell(path, taskArgs);
817     } else {
818         // Run process directly:
819         //   Vim -g -f args
820         NSTask *task = [NSTask launchedTaskWithLaunchPath:path
821                                                 arguments:taskArgs];
822         pid = task ? [task processIdentifier] : -1;
823     }
825     if (-1 != pid) {
826         // NOTE: If the process has no arguments, then add a null argument to
827         // the pidArguments dictionary.  This is later used to detect that a
828         // process without arguments is being launched.
829         if (!args)
830             [pidArguments setObject:[NSNull null]
831                              forKey:[NSNumber numberWithInt:pid]];
832     } else {
833         NSLog(@"WARNING: %s%@ failed (useLoginShell=%d)", _cmd, args,
834                 useLoginShell);
835     }
837     return pid;
840 - (NSArray *)filterFilesAndNotify:(NSArray *)filenames
842     // Go trough 'filenames' array and make sure each file exists.  Present
843     // warning dialog if some file was missing.
845     NSString *firstMissingFile = nil;
846     NSMutableArray *files = [NSMutableArray array];
847     unsigned i, count = [filenames count];
849     for (i = 0; i < count; ++i) {
850         NSString *name = [filenames objectAtIndex:i];
851         if ([[NSFileManager defaultManager] fileExistsAtPath:name]) {
852             [files addObject:name];
853         } else if (!firstMissingFile) {
854             firstMissingFile = name;
855         }
856     }
858     if (firstMissingFile) {
859         NSAlert *alert = [[NSAlert alloc] init];
860         [alert addButtonWithTitle:@"OK"];
862         NSString *text;
863         if ([files count] >= count-1) {
864             [alert setMessageText:@"File not found"];
865             text = [NSString stringWithFormat:@"Could not open file with "
866                 "name %@.", firstMissingFile];
867         } else {
868             [alert setMessageText:@"Multiple files not found"];
869             text = [NSString stringWithFormat:@"Could not open file with "
870                 "name %@, and %d other files.", firstMissingFile,
871                 count-[files count]-1];
872         }
874         [alert setInformativeText:text];
875         [alert setAlertStyle:NSWarningAlertStyle];
877         [alert runModal];
878         [alert release];
880         [NSApp replyToOpenOrPrint:NSApplicationDelegateReplyFailure];
881     }
883     return files;
886 - (NSArray *)filterOpenFiles:(NSArray *)filenames
887                    arguments:(NSDictionary *)args
889     // Check if any of the files in the 'filenames' array are open in any Vim
890     // process.  Remove the files that are open from the 'filenames' array and
891     // return it.  If all files were filtered out, then raise the first file in
892     // the Vim process it is open.  Files that are filtered are sent an odb
893     // open event in case theID is not zero.
895     NSMutableDictionary *localArgs =
896             [NSMutableDictionary dictionaryWithDictionary:args];
897     MMVimController *raiseController = nil;
898     NSString *raiseFile = nil;
899     NSMutableArray *files = [filenames mutableCopy];
900     NSString *expr = [NSString stringWithFormat:
901             @"map([\"%@\"],\"bufloaded(v:val)\")",
902             [files componentsJoinedByString:@"\",\""]];
903     unsigned i, count = [vimControllers count];
905     // Ensure that the files aren't opened when passing arguments.
906     [localArgs setObject:[NSNumber numberWithBool:NO] forKey:@"openFiles"];
908     for (i = 0; i < count && [files count]; ++i) {
909         MMVimController *controller = [vimControllers objectAtIndex:i];
911         // Query Vim for which files in the 'files' array are open.
912         NSString *eval = [controller evaluateVimExpression:expr];
913         if (!eval) continue;
915         NSIndexSet *idxSet = [NSIndexSet indexSetWithVimList:eval];
916         if ([idxSet count]) {
917             if (!raiseFile) {
918                 // Remember the file and which Vim that has it open so that
919                 // we can raise it later on.
920                 raiseController = controller;
921                 raiseFile = [files objectAtIndex:[idxSet firstIndex]];
922                 [[raiseFile retain] autorelease];
923             }
925             // Pass (ODB/Xcode/Spotlight) arguments to this process.
926             [localArgs setObject:[files objectsAtIndexes:idxSet]
927                           forKey:@"filenames"];
928             [self passArguments:localArgs toVimController:controller];
930             // Remove all the files that were open in this Vim process and
931             // create a new expression to evaluate.
932             [files removeObjectsAtIndexes:idxSet];
933             expr = [NSString stringWithFormat:
934                     @"map([\"%@\"],\"bufloaded(v:val)\")",
935                     [files componentsJoinedByString:@"\",\""]];
936         }
937     }
939     if (![files count] && raiseFile) {
940         // Raise the window containing the first file that was already open,
941         // and make sure that the tab containing that file is selected.  Only
942         // do this if there are no more files to open, otherwise sometimes the
943         // window with 'raiseFile' will be raised, other times it might be the
944         // window that will open with the files in the 'files' array.
945         raiseFile = [raiseFile stringByEscapingSpecialFilenameCharacters];
946         NSString *input = [NSString stringWithFormat:@"<C-\\><C-N>"
947             ":let oldswb=&swb|let &swb=\"useopen,usetab\"|"
948             "tab sb %@|let &swb=oldswb|unl oldswb|"
949             "cal foreground()|redr|f<CR>", raiseFile];
951         [raiseController addVimInput:input];
952     }
954     return files;
957 #if MM_HANDLE_XCODE_MOD_EVENT
958 - (void)handleXcodeModEvent:(NSAppleEventDescriptor *)event
959                  replyEvent:(NSAppleEventDescriptor *)reply
961 #if 0
962     // Xcode sends this event to query MacVim which open files have been
963     // modified.
964     NSLog(@"reply:%@", reply);
965     NSLog(@"event:%@", event);
967     NSEnumerator *e = [vimControllers objectEnumerator];
968     id vc;
969     while ((vc = [e nextObject])) {
970         DescType type = [reply descriptorType];
971         unsigned len = [[type data] length];
972         NSMutableData *data = [NSMutableData data];
974         [data appendBytes:&type length:sizeof(DescType)];
975         [data appendBytes:&len length:sizeof(unsigned)];
976         [data appendBytes:[reply data] length:len];
978         [vc sendMessage:XcodeModMsgID data:data];
979     }
980 #endif
982 #endif
984 - (int)findLaunchingProcessWithoutArguments
986     NSArray *keys = [pidArguments allKeysForObject:[NSNull null]];
987     if ([keys count] > 0) {
988         //NSLog(@"found launching process without arguments");
989         return [[keys objectAtIndex:0] intValue];
990     }
992     return 0;
995 - (MMVimController *)findUntitledWindow
997     NSEnumerator *e = [vimControllers objectEnumerator];
998     id vc;
999     while ((vc = [e nextObject])) {
1000         // TODO: This is a moronic test...should query the Vim process if there
1001         // are any open buffers or something like that instead.
1002         NSString *title = [[[vc windowController] window] title];
1003         if ([title hasPrefix:@"[No Name] - VIM"]) {
1004             //NSLog(@"found untitled window");
1005             return vc;
1006         }
1007     }
1009     return nil;
1012 - (NSMutableDictionary *)extractArgumentsFromOdocEvent:
1013     (NSAppleEventDescriptor *)desc
1015     NSMutableDictionary *dict = [NSMutableDictionary dictionary];
1017     // 1. Extract ODB parameters (if any)
1018     NSAppleEventDescriptor *odbdesc = desc;
1019     if (![odbdesc paramDescriptorForKeyword:keyFileSender]) {
1020         // The ODB paramaters may hide inside the 'keyAEPropData' descriptor.
1021         odbdesc = [odbdesc paramDescriptorForKeyword:keyAEPropData];
1022         if (![odbdesc paramDescriptorForKeyword:keyFileSender])
1023             odbdesc = nil;
1024     }
1026     if (odbdesc) {
1027         NSAppleEventDescriptor *p =
1028                 [odbdesc paramDescriptorForKeyword:keyFileSender];
1029         if (p)
1030             [dict setObject:[NSNumber numberWithUnsignedInt:[p typeCodeValue]]
1031                      forKey:@"remoteID"];
1033         p = [odbdesc paramDescriptorForKeyword:keyFileCustomPath];
1034         if (p)
1035             [dict setObject:[p stringValue] forKey:@"remotePath"];
1037         p = [odbdesc paramDescriptorForKeyword:keyFileSenderToken];
1038         if (p)
1039             [dict setObject:p forKey:@"remotePath"];
1040     }
1042     // 2. Extract Xcode parameters (if any)
1043     NSAppleEventDescriptor *xcodedesc =
1044             [desc paramDescriptorForKeyword:keyAEPosition];
1045     if (xcodedesc) {
1046         NSRange range;
1047         MMSelectionRange *sr = (MMSelectionRange*)[[xcodedesc data] bytes];
1049         if (sr->lineNum < 0) {
1050             // Should select a range of lines.
1051             range.location = sr->startRange + 1;
1052             range.length = sr->endRange - sr->startRange + 1;
1053         } else {
1054             // Should only move cursor to a line.
1055             range.location = sr->lineNum + 1;
1056             range.length = 0;
1057         }
1059         [dict setObject:NSStringFromRange(range) forKey:@"selectionRange"];
1060     }
1062     // 3. Extract Spotlight search text (if any)
1063     NSAppleEventDescriptor *spotlightdesc = 
1064             [desc paramDescriptorForKeyword:keyAESearchText];
1065     if (spotlightdesc)
1066         [dict setObject:[spotlightdesc stringValue] forKey:@"searchText"];
1068     return dict;
1071 - (void)passArguments:(NSDictionary *)args toVimController:(MMVimController*)vc
1073     if (!args) return;
1075     // Pass filenames to open if required (the 'openFiles' argument can be used
1076     // to disallow opening of the files).
1077     NSArray *filenames = [args objectForKey:@"filenames"];
1078     if (filenames && [[args objectForKey:@"openFiles"] boolValue]) {
1079         NSString *tabDrop = buildTabDropCommand(filenames);
1080         [vc addVimInput:tabDrop];
1081     }
1083     // Pass ODB data
1084     if (filenames && [args objectForKey:@"remoteID"]) {
1085         [vc odbEdit:filenames
1086              server:[[args objectForKey:@"remoteID"] unsignedIntValue]
1087                path:[args objectForKey:@"remotePath"]
1088               token:[args objectForKey:@"remoteToken"]];
1089     }
1091     // Pass range of lines to select
1092     if ([args objectForKey:@"selectionRange"]) {
1093         NSRange selectionRange = NSRangeFromString(
1094                 [args objectForKey:@"selectionRange"]);
1095         [vc addVimInput:buildSelectRangeCommand(selectionRange)];
1096     }
1098     // Pass search text
1099     NSString *searchText = [args objectForKey:@"searchText"];
1100     if (searchText)
1101         [vc addVimInput:buildSearchTextCommand(searchText)];
1104 @end // MMAppController (Private)
1109 @implementation NSMenu (MMExtras)
1111 - (void)recurseSetAutoenablesItems:(BOOL)on
1113     [self setAutoenablesItems:on];
1115     int i, count = [self numberOfItems];
1116     for (i = 0; i < count; ++i) {
1117         NSMenuItem *item = [self itemAtIndex:i];
1118         [item setEnabled:YES];
1119         NSMenu *submenu = [item submenu];
1120         if (submenu) {
1121             [submenu recurseSetAutoenablesItems:on];
1122         }
1123     }
1126 @end  // NSMenu (MMExtras)
1131 @implementation NSNumber (MMExtras)
1132 - (int)tag
1134     return [self intValue];
1136 @end // NSNumber (MMExtras)
1141     static int
1142 executeInLoginShell(NSString *path, NSArray *args)
1144     // Start a login shell and execute the command 'path' with arguments 'args'
1145     // in the shell.  This ensures that user environment variables are set even
1146     // when MacVim was started from the Finder.
1148     int pid = -1;
1149     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
1151     // Determine which shell to use to execute the command.  The user
1152     // may decide which shell to use by setting a user default or the
1153     // $SHELL environment variable.
1154     NSString *shell = [ud stringForKey:MMLoginShellCommandKey];
1155     if (!shell || [shell length] == 0)
1156         shell = [[[NSProcessInfo processInfo] environment]
1157             objectForKey:@"SHELL"];
1158     if (!shell)
1159         shell = @"/bin/bash";
1161     //NSLog(@"shell = %@", shell);
1163     // Bash needs the '-l' flag to launch a login shell.  The user may add
1164     // flags by setting a user default.
1165     NSString *shellArgument = [ud stringForKey:MMLoginShellArgumentKey];
1166     if (!shellArgument || [shellArgument length] == 0) {
1167         if ([[shell lastPathComponent] isEqual:@"bash"])
1168             shellArgument = @"-l";
1169         else
1170             shellArgument = nil;
1171     }
1173     //NSLog(@"shellArgument = %@", shellArgument);
1175     // Build input string to pipe to the login shell.
1176     NSMutableString *input = [NSMutableString stringWithFormat:
1177             @"exec \"%@\"", path];
1178     if (args) {
1179         // Append all arguments, making sure they are properly quoted, even
1180         // when they contain single quotes.
1181         NSEnumerator *e = [args objectEnumerator];
1182         id obj;
1184         while ((obj = [e nextObject])) {
1185             NSMutableString *arg = [NSMutableString stringWithString:obj];
1186             [arg replaceOccurrencesOfString:@"'" withString:@"'\"'\"'"
1187                                     options:NSLiteralSearch
1188                                       range:NSMakeRange(0, [arg length])];
1189             [input appendFormat:@" '%@'", arg];
1190         }
1191     }
1193     // Build the argument vector used to start the login shell.
1194     NSString *shellArg0 = [NSString stringWithFormat:@"-%@",
1195              [shell lastPathComponent]];
1196     char *shellArgv[3] = { (char *)[shellArg0 UTF8String], NULL, NULL };
1197     if (shellArgument)
1198         shellArgv[1] = (char *)[shellArgument UTF8String];
1200     // Get the C string representation of the shell path before the fork since
1201     // we must not call Foundation functions after a fork.
1202     const char *shellPath = [shell fileSystemRepresentation];
1204     // Fork and execute the process.
1205     int ds[2];
1206     if (pipe(ds)) return -1;
1208     pid = fork();
1209     if (pid == -1) {
1210         return -1;
1211     } else if (pid == 0) {
1212         // Child process
1213         if (close(ds[1]) == -1) exit(255);
1214         if (dup2(ds[0], 0) == -1) exit(255);
1216         execv(shellPath, shellArgv);
1218         // Never reached unless execv fails
1219         exit(255);
1220     } else {
1221         // Parent process
1222         if (close(ds[0]) == -1) return -1;
1224         // Send input to execute to the child process
1225         [input appendString:@"\n"];
1226         int bytes = [input lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
1228         if (write(ds[1], [input UTF8String], bytes) != bytes) return -1;
1229         if (close(ds[1]) == -1) return -1;
1230     }
1232     return pid;