Fix problems with 'fullscreen' and :mksession
[MacVim.git] / src / MacVim / MMAppController.m
blob4b94982503800d4f154151f95c8178e0f1a3fd90
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 and takes care of the main menu.
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  *
28  * The main menu is handled as follows.  Each Vim controller keeps its own main
29  * menu.  All menus except the "MacVim" menu are controlled by the Vim process.
30  * The app controller also keeps a reference to the "default main menu" which
31  * is set up in MainMenu.nib.  When no editor window is open the default main
32  * menu is used.  When a new editor window becomes main its main menu becomes
33  * the new main menu, this is done in -[MMAppController setMainMenu:].
34  *   NOTE: Certain heuristics are used to find the "MacVim", "Windows", "File",
35  * and "Services" menu.  If MainMenu.nib changes these heuristics may have to
36  * change as well.  For specifics see the find... methods defined in the NSMenu
37  * category "MMExtras".
38  */
40 #import "MMAppController.h"
41 #import "MMPreferenceController.h"
42 #import "MMVimController.h"
43 #import "MMWindowController.h"
44 #import "Miscellaneous.h"
46 #ifdef MM_ENABLE_PLUGINS
47 #import "MMPlugInManager.h"
48 #endif
50 #import <unistd.h>
51 #import <CoreServices/CoreServices.h>
54 #define MM_HANDLE_XCODE_MOD_EVENT 0
58 // Default timeout intervals on all connections.
59 static NSTimeInterval MMRequestTimeout = 5;
60 static NSTimeInterval MMReplyTimeout = 5;
62 static NSString *MMWebsiteString = @"http://code.google.com/p/macvim/";
64 // When terminating, notify Vim processes then sleep for these many
65 // microseconds.
66 static useconds_t MMTerminationSleepPeriod = 10000;
68 #if (MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_4)
69 // Latency (in s) between FS event occuring and being reported to MacVim.
70 // Should be small so that MacVim is notified of changes to the ~/.vim
71 // directory more or less immediately.
72 static CFTimeInterval MMEventStreamLatency = 0.1;
73 #endif
76 #pragma options align=mac68k
77 typedef struct
79     short unused1;      // 0 (not used)
80     short lineNum;      // line to select (< 0 to specify range)
81     long  startRange;   // start of selection range (if line < 0)
82     long  endRange;     // end of selection range (if line < 0)
83     long  unused2;      // 0 (not used)
84     long  theDate;      // modification date/time
85 } MMSelectionRange;
86 #pragma options align=reset
89 static int executeInLoginShell(NSString *path, NSArray *args);
92 @interface MMAppController (MMServices)
93 - (void)openSelection:(NSPasteboard *)pboard userData:(NSString *)userData
94                 error:(NSString **)error;
95 - (void)openFile:(NSPasteboard *)pboard userData:(NSString *)userData
96            error:(NSString **)error;
97 @end
100 @interface MMAppController (Private)
101 - (MMVimController *)topmostVimController;
102 - (int)launchVimProcessWithArguments:(NSArray *)args;
103 - (NSArray *)filterFilesAndNotify:(NSArray *)files;
104 - (NSArray *)filterOpenFiles:(NSArray *)filenames
105                openFilesDict:(NSDictionary **)openFiles;
106 #if MM_HANDLE_XCODE_MOD_EVENT
107 - (void)handleXcodeModEvent:(NSAppleEventDescriptor *)event
108                  replyEvent:(NSAppleEventDescriptor *)reply;
109 #endif
110 - (void)handleGetURLEvent:(NSAppleEventDescriptor *)event
111                replyEvent:(NSAppleEventDescriptor *)reply;
112 - (int)findLaunchingProcessWithoutArguments;
113 - (MMVimController *)findUnusedEditor;
114 - (NSMutableDictionary *)extractArgumentsFromOdocEvent:
115     (NSAppleEventDescriptor *)desc;
116 - (void)scheduleVimControllerPreloadAfterDelay:(NSTimeInterval)delay;
117 - (void)cancelVimControllerPreloadRequests;
118 - (void)preloadVimController:(id)sender;
119 - (int)maxPreloadCacheSize;
120 - (MMVimController *)takeVimControllerFromCache;
121 - (void)clearPreloadCacheWithCount:(int)count;
122 - (void)rebuildPreloadCache;
123 - (NSDate *)rcFilesModificationDate;
124 - (BOOL)openVimControllerWithArguments:(NSDictionary *)arguments;
125 - (void)activateWhenNextWindowOpens;
126 - (void)startWatchingVimDir;
127 - (void)stopWatchingVimDir;
128 - (void)handleFSEvent;
130 #ifdef MM_ENABLE_PLUGINS
131 - (void)removePlugInMenu;
132 - (void)addPlugInMenuToMenu:(NSMenu *)mainMenu;
133 #endif
134 @end
138 #if (MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_4)
139     static void
140 fsEventCallback(ConstFSEventStreamRef streamRef,
141                 void *clientCallBackInfo,
142                 size_t numEvents,
143                 void *eventPaths,
144                 const FSEventStreamEventFlags eventFlags[],
145                 const FSEventStreamEventId eventIds[])
147     [[MMAppController sharedInstance] handleFSEvent];
149 #endif
151 @implementation MMAppController
153 + (void)initialize
155     NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:
156         [NSNumber numberWithBool:NO],   MMNoWindowKey,
157         [NSNumber numberWithInt:64],    MMTabMinWidthKey,
158         [NSNumber numberWithInt:6*64],  MMTabMaxWidthKey,
159         [NSNumber numberWithInt:132],   MMTabOptimumWidthKey,
160         [NSNumber numberWithInt:2],     MMTextInsetLeftKey,
161         [NSNumber numberWithInt:1],     MMTextInsetRightKey,
162         [NSNumber numberWithInt:1],     MMTextInsetTopKey,
163         [NSNumber numberWithInt:1],     MMTextInsetBottomKey,
164         @"MMTypesetter",                MMTypesetterKey,
165         [NSNumber numberWithFloat:1],   MMCellWidthMultiplierKey,
166         [NSNumber numberWithFloat:-1],  MMBaselineOffsetKey,
167         [NSNumber numberWithBool:YES],  MMTranslateCtrlClickKey,
168         [NSNumber numberWithInt:0],     MMOpenInCurrentWindowKey,
169         [NSNumber numberWithBool:NO],   MMNoFontSubstitutionKey,
170         [NSNumber numberWithBool:YES],  MMLoginShellKey,
171         [NSNumber numberWithBool:NO],   MMAtsuiRendererKey,
172         [NSNumber numberWithInt:MMUntitledWindowAlways],
173                                         MMUntitledWindowKey,
174         [NSNumber numberWithBool:NO],   MMTexturedWindowKey,
175         [NSNumber numberWithBool:NO],   MMZoomBothKey,
176         @"",                            MMLoginShellCommandKey,
177         @"",                            MMLoginShellArgumentKey,
178         [NSNumber numberWithBool:YES],  MMDialogsTrackPwdKey,
179 #ifdef MM_ENABLE_PLUGINS
180         [NSNumber numberWithBool:YES],  MMShowLeftPlugInContainerKey,
181 #endif
182         [NSNumber numberWithInt:3],     MMOpenLayoutKey,
183         [NSNumber numberWithBool:NO],   MMVerticalSplitKey,
184         [NSNumber numberWithInt:0],     MMPreloadCacheSizeKey,
185         [NSNumber numberWithInt:0],     MMLastWindowClosedBehaviorKey,
186         nil];
188     [[NSUserDefaults standardUserDefaults] registerDefaults:dict];
190     NSArray *types = [NSArray arrayWithObject:NSStringPboardType];
191     [NSApp registerServicesMenuSendTypes:types returnTypes:types];
193     // NOTE: Set the current directory to user's home directory, otherwise it
194     // will default to the root directory.  (This matters since new Vim
195     // processes inherit MacVim's environment variables.)
196     [[NSFileManager defaultManager] changeCurrentDirectoryPath:
197             NSHomeDirectory()];
200 - (id)init
202     if (!(self = [super init])) return nil;
204     fontContainerRef = loadFonts();
206     vimControllers = [NSMutableArray new];
207     cachedVimControllers = [NSMutableArray new];
208     preloadPid = -1;
209     pidArguments = [NSMutableDictionary new];
211 #ifdef MM_ENABLE_PLUGINS
212     NSString *plugInTitle = NSLocalizedString(@"Plug-In",
213                                               @"Plug-In menu title");
214     plugInMenuItem = [[NSMenuItem alloc] initWithTitle:plugInTitle
215                                                 action:NULL
216                                          keyEquivalent:@""];
217     NSMenu *submenu = [[NSMenu alloc] initWithTitle:plugInTitle];
218     [plugInMenuItem setSubmenu:submenu];
219     [submenu release];
220 #endif
222     // NOTE: Do not use the default connection since the Logitech Control
223     // Center (LCC) input manager steals and this would cause MacVim to
224     // never open any windows.  (This is a bug in LCC but since they are
225     // unlikely to fix it, we graciously give them the default connection.)
226     connection = [[NSConnection alloc] initWithReceivePort:[NSPort port]
227                                                   sendPort:nil];
228     [connection setRootObject:self];
229     [connection setRequestTimeout:MMRequestTimeout];
230     [connection setReplyTimeout:MMReplyTimeout];
232     // NOTE: When the user is resizing the window the AppKit puts the run
233     // loop in event tracking mode.  Unless the connection listens to
234     // request in this mode, live resizing won't work.
235     [connection addRequestMode:NSEventTrackingRunLoopMode];
237     // NOTE!  If the name of the connection changes here it must also be
238     // updated in MMBackend.m.
239     NSString *name = [NSString stringWithFormat:@"%@-connection",
240              [[NSBundle mainBundle] bundlePath]];
241     //NSLog(@"Registering connection with name '%@'", name);
242     if (![connection registerName:name]) {
243         NSLog(@"FATAL ERROR: Failed to register connection with name '%@'",
244                 name);
245         [connection release];  connection = nil;
246     }
248     return self;
251 - (void)dealloc
253     //NSLog(@"MMAppController dealloc");
255     [connection release];  connection = nil;
256     [pidArguments release];  pidArguments = nil;
257     [vimControllers release];  vimControllers = nil;
258     [cachedVimControllers release];  cachedVimControllers = nil;
259     [openSelectionString release];  openSelectionString = nil;
260     [recentFilesMenuItem release];  recentFilesMenuItem = nil;
261     [defaultMainMenu release];  defaultMainMenu = nil;
262 #ifdef MM_ENABLE_PLUGINS
263     [plugInMenuItem release];  plugInMenuItem = nil;
264 #endif
265     [appMenuItemTemplate release];  appMenuItemTemplate = nil;
267     [super dealloc];
270 - (void)applicationWillFinishLaunching:(NSNotification *)notification
272     // Remember the default menu so that it can be restored if the user closes
273     // all editor windows.
274     defaultMainMenu = [[NSApp mainMenu] retain];
276     // Store a copy of the default app menu so we can use this as a template
277     // for all other menus.  We make a copy here because the "Services" menu
278     // will not yet have been populated at this time.  If we don't we get
279     // problems trying to set key equivalents later on because they might clash
280     // with items on the "Services" menu.
281     appMenuItemTemplate = [defaultMainMenu itemAtIndex:0];
282     appMenuItemTemplate = [appMenuItemTemplate copy];
284     // Set up the "Open Recent" menu. See
285     //   http://lapcatsoftware.com/blog/2007/07/10/
286     //     working-without-a-nib-part-5-open-recent-menu/
287     // and
288     //   http://www.cocoabuilder.com/archive/message/cocoa/2007/8/15/187793
289     // for more information.
290     //
291     // The menu itself is created in MainMenu.nib but we still seem to have to
292     // hack around a bit to get it to work.  (This has to be done in
293     // applicationWillFinishLaunching at the latest, otherwise it doesn't
294     // work.)
295     NSMenu *fileMenu = [defaultMainMenu findFileMenu];
296     if (fileMenu) {
297         int idx = [fileMenu indexOfItemWithAction:@selector(fileOpen:)];
298         if (idx >= 0 && idx+1 < [fileMenu numberOfItems])
300         recentFilesMenuItem = [fileMenu itemWithTitle:@"Open Recent"];
301         [[recentFilesMenuItem submenu] performSelector:@selector(_setMenuName:)
302                                         withObject:@"NSRecentDocumentsMenu"];
304         // Note: The "Recent Files" menu must be moved around since there is no
305         // -[NSApp setRecentFilesMenu:] method.  We keep a reference to it to
306         // facilitate this move (see setMainMenu: below).
307         [recentFilesMenuItem retain];
308     }
310 #if MM_HANDLE_XCODE_MOD_EVENT
311     [[NSAppleEventManager sharedAppleEventManager]
312             setEventHandler:self
313                 andSelector:@selector(handleXcodeModEvent:replyEvent:)
314               forEventClass:'KAHL'
315                  andEventID:'MOD '];
316 #endif
318     // Register 'mvim://' URL handler
319     [[NSAppleEventManager sharedAppleEventManager]
320             setEventHandler:self
321                 andSelector:@selector(handleGetURLEvent:replyEvent:)
322               forEventClass:kInternetEventClass
323                  andEventID:kAEGetURL];
326 - (void)applicationDidFinishLaunching:(NSNotification *)notification
328     [NSApp setServicesProvider:self];
329 #ifdef MM_ENABLE_PLUGINS
330     [[MMPlugInManager sharedManager] loadAllPlugIns];
331 #endif
333     if ([self maxPreloadCacheSize] > 0) {
334         [self scheduleVimControllerPreloadAfterDelay:2];
335         [self startWatchingVimDir];
336     }
339 - (BOOL)applicationShouldOpenUntitledFile:(NSApplication *)sender
341     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
342     NSAppleEventManager *aem = [NSAppleEventManager sharedAppleEventManager];
343     NSAppleEventDescriptor *desc = [aem currentAppleEvent];
345     // The user default MMUntitledWindow can be set to control whether an
346     // untitled window should open on 'Open' and 'Reopen' events.
347     int untitledWindowFlag = [ud integerForKey:MMUntitledWindowKey];
348     if ([desc eventID] == kAEOpenApplication
349             && (untitledWindowFlag & MMUntitledWindowOnOpen) == 0)
350         return NO;
351     else if ([desc eventID] == kAEReopenApplication
352             && (untitledWindowFlag & MMUntitledWindowOnReopen) == 0)
353         return NO;
355     // When a process is started from the command line, the 'Open' event will
356     // contain a parameter to surpress the opening of an untitled window.
357     desc = [desc paramDescriptorForKeyword:keyAEPropData];
358     desc = [desc paramDescriptorForKeyword:keyMMUntitledWindow];
359     if (desc && ![desc booleanValue])
360         return NO;
362     // Never open an untitled window if there is at least one open window or if
363     // there are processes that are currently launching.
364     if ([vimControllers count] > 0 || [pidArguments count] > 0)
365         return NO;
367     // NOTE!  This way it possible to start the app with the command-line
368     // argument '-nowindow yes' and no window will be opened by default.
369     return ![ud boolForKey:MMNoWindowKey];
372 - (BOOL)applicationOpenUntitledFile:(NSApplication *)sender
374     [self newWindow:self];
375     return YES;
378 - (void)application:(NSApplication *)sender openFiles:(NSArray *)filenames
380     // Extract ODB/Xcode/Spotlight parameters from the current Apple event,
381     // sort the filenames, and then let openFiles:withArguments: do the heavy
382     // lifting.
384     if (!(filenames && [filenames count] > 0))
385         return;
387     // Sort filenames since the Finder doesn't take care in preserving the
388     // order in which files are selected anyway (and "sorted" is more
389     // predictable than "random").
390     if ([filenames count] > 1)
391         filenames = [filenames sortedArrayUsingSelector:
392                 @selector(localizedCompare:)];
394     // Extract ODB/Xcode/Spotlight parameters from the current Apple event
395     NSMutableDictionary *arguments = [self extractArgumentsFromOdocEvent:
396             [[NSAppleEventManager sharedAppleEventManager] currentAppleEvent]];
398     if ([self openFiles:filenames withArguments:arguments]) {
399         [NSApp replyToOpenOrPrint:NSApplicationDelegateReplySuccess];
400     } else {
401         // TODO: Notify user of failure?
402         [NSApp replyToOpenOrPrint:NSApplicationDelegateReplyFailure];
403     }
406 - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender
408     return (MMTerminateWhenLastWindowClosed ==
409             [[NSUserDefaults standardUserDefaults]
410                 integerForKey:MMLastWindowClosedBehaviorKey]);
413 - (NSApplicationTerminateReply)applicationShouldTerminate:
414     (NSApplication *)sender
416     // TODO: Follow Apple's guidelines for 'Graceful Application Termination'
417     // (in particular, allow user to review changes and save).
418     int reply = NSTerminateNow;
419     BOOL modifiedBuffers = NO;
421     // Go through windows, checking for modified buffers.  (Each Vim process
422     // tells MacVim when any buffer has been modified and MacVim sets the
423     // 'documentEdited' flag of the window correspondingly.)
424     NSEnumerator *e = [[NSApp windows] objectEnumerator];
425     id window;
426     while ((window = [e nextObject])) {
427         if ([window isDocumentEdited]) {
428             modifiedBuffers = YES;
429             break;
430         }
431     }
433     if (modifiedBuffers) {
434         NSAlert *alert = [[NSAlert alloc] init];
435         [alert setAlertStyle:NSWarningAlertStyle];
436         [alert addButtonWithTitle:NSLocalizedString(@"Quit",
437                 @"Dialog button")];
438         [alert addButtonWithTitle:NSLocalizedString(@"Cancel",
439                 @"Dialog button")];
440         [alert setMessageText:NSLocalizedString(@"Quit without saving?",
441                 @"Quit dialog with changed buffers, title")];
442         [alert setInformativeText:NSLocalizedString(
443                 @"There are modified buffers, "
444                 "if you quit now all changes will be lost.  Quit anyway?",
445                 @"Quit dialog with changed buffers, text")];
447         if ([alert runModal] != NSAlertFirstButtonReturn)
448             reply = NSTerminateCancel;
450         [alert release];
451     } else {
452         // No unmodified buffers, but give a warning if there are multiple
453         // windows and/or tabs open.
454         int numWindows = [vimControllers count];
455         int numTabs = 0;
457         // Count the number of open tabs
458         e = [vimControllers objectEnumerator];
459         id vc;
460         while ((vc = [e nextObject])) {
461             NSString *eval = [vc evaluateVimExpression:@"tabpagenr('$')"];
462             if (eval) {
463                 int count = [eval intValue];
464                 if (count > 0 && count < INT_MAX)
465                     numTabs += count;
466             }
467         }
469         if (numWindows > 1 || numTabs > 1) {
470             NSAlert *alert = [[NSAlert alloc] init];
471             [alert setAlertStyle:NSWarningAlertStyle];
472             [alert addButtonWithTitle:NSLocalizedString(@"Quit",
473                     @"Dialog button")];
474             [alert addButtonWithTitle:NSLocalizedString(@"Cancel",
475                     @"Dialog button")];
476             [alert setMessageText:NSLocalizedString(
477                     @"Are you sure you want to quit MacVim?",
478                     @"Quit dialog with no changed buffers, title")];
480             NSString *info = nil;
481             if (numWindows > 1) {
482                 if (numTabs > numWindows)
483                     info = [NSString stringWithFormat:NSLocalizedString(
484                             @"There are %d windows open in MacVim, with a "
485                             "total of %d tabs. Do you want to quit anyway?",
486                             @"Quit dialog with no changed buffers, text"),
487                          numWindows, numTabs];
488                 else
489                     info = [NSString stringWithFormat:NSLocalizedString(
490                             @"There are %d windows open in MacVim. "
491                             "Do you want to quit anyway?",
492                             @"Quit dialog with no changed buffers, text"),
493                         numWindows];
495             } else {
496                 info = [NSString stringWithFormat:NSLocalizedString(
497                         @"There are %d tabs open in MacVim. "
498                         "Do you want to quit anyway?",
499                         @"Quit dialog with no changed buffers, text"), 
500                      numTabs];
501             }
503             [alert setInformativeText:info];
505             if ([alert runModal] != NSAlertFirstButtonReturn)
506                 reply = NSTerminateCancel;
508             [alert release];
509         }
510     }
513     // Tell all Vim processes to terminate now (otherwise they'll leave swap
514     // files behind).
515     if (NSTerminateNow == reply) {
516         e = [vimControllers objectEnumerator];
517         id vc;
518         while ((vc = [e nextObject]))
519             [vc sendMessage:TerminateNowMsgID data:nil];
521         e = [cachedVimControllers objectEnumerator];
522         while ((vc = [e nextObject]))
523             [vc sendMessage:TerminateNowMsgID data:nil];
525         // Give Vim processes a chance to terminate before MacVim.  If they
526         // haven't terminated by the time applicationWillTerminate: is sent,
527         // they may be forced to quit (see below).
528         usleep(MMTerminationSleepPeriod);
529     }
531     return reply;
534 - (void)applicationWillTerminate:(NSNotification *)notification
536     [self stopWatchingVimDir];
538 #ifdef MM_ENABLE_PLUGINS
539     [[MMPlugInManager sharedManager] unloadAllPlugIns];
540 #endif
542 #if MM_HANDLE_XCODE_MOD_EVENT
543     [[NSAppleEventManager sharedAppleEventManager]
544             removeEventHandlerForEventClass:'KAHL'
545                                  andEventID:'MOD '];
546 #endif
548     // This will invalidate all connections (since they were spawned from this
549     // connection).
550     [connection invalidate];
552     // Send a SIGINT to all running Vim processes, so that they are sure to
553     // receive the connectionDidDie: notification (a process has to be checking
554     // the run-loop for this to happen).
555     unsigned i, count = [vimControllers count];
556     for (i = 0; i < count; ++i) {
557         MMVimController *controller = [vimControllers objectAtIndex:i];
558         int pid = [controller pid];
559         if (-1 != pid)
560             kill(pid, SIGINT);
561     }
563     if (fontContainerRef) {
564         ATSFontDeactivate(fontContainerRef, NULL, kATSOptionFlagsDefault);
565         fontContainerRef = 0;
566     }
568     [NSApp setDelegate:nil];
571 + (MMAppController *)sharedInstance
573     // Note: The app controller is a singleton which is instantiated in
574     // MainMenu.nib where it is also connected as the delegate of NSApp.
575     id delegate = [NSApp delegate];
576     return [delegate isKindOfClass:self] ? (MMAppController*)delegate : nil;
579 - (NSMenu *)defaultMainMenu
581     return defaultMainMenu;
584 - (NSMenuItem *)appMenuItemTemplate
586     return appMenuItemTemplate;
589 - (void)removeVimController:(id)controller
591     int idx = [vimControllers indexOfObject:controller];
592     if (NSNotFound == idx)
593         return;
595     [controller cleanup];
597     [vimControllers removeObjectAtIndex:idx];
599     if (![vimControllers count]) {
600         // The last editor window just closed so restore the main menu back to
601         // its default state (which is defined in MainMenu.nib).
602         [self setMainMenu:defaultMainMenu];
604         BOOL hide = (MMHideWhenLastWindowClosed ==
605                     [[NSUserDefaults standardUserDefaults]
606                         integerForKey:MMLastWindowClosedBehaviorKey]);
607         if (hide)
608             [NSApp hide:self];
609     }
612 - (void)windowControllerWillOpen:(MMWindowController *)windowController
614     NSPoint topLeft = NSZeroPoint;
615     NSWindow *topWin = [[[self topmostVimController] windowController] window];
616     NSWindow *win = [windowController window];
618     if (!win) return;
620     // If there is a window belonging to a Vim process, cascade from it,
621     // otherwise use the autosaved window position (if any).
622     if (topWin) {
623         NSRect frame = [topWin frame];
624         topLeft = NSMakePoint(frame.origin.x, NSMaxY(frame));
625     } else {
626         NSString *topLeftString = [[NSUserDefaults standardUserDefaults]
627             stringForKey:MMTopLeftPointKey];
628         if (topLeftString)
629             topLeft = NSPointFromString(topLeftString);
630     }
632     if (!NSEqualPoints(topLeft, NSZeroPoint)) {
633         NSPoint oldTopLeft = topLeft;
634         if (topWin)
635             topLeft = [win cascadeTopLeftFromPoint:topLeft];
637         [win setFrameTopLeftPoint:topLeft];
639         if ([win frame].origin.y < [[win screen] frame].origin.y) {
640             // Try to avoid shifting the new window downwards if it means that
641             // the bottom of the window will be off the screen.  E.g. if the
642             // user has set windows to open maximized in the vertical direction
643             // then the new window will cascade horizontally only.
644             topLeft.y = oldTopLeft.y;
645             [win setFrameTopLeftPoint:topLeft];
646         }
647     }
649     if (1 == [vimControllers count]) {
650         // The first window autosaves its position.  (The autosaving
651         // features of Cocoa are not used because we need more control over
652         // what is autosaved and when it is restored.)
653         [windowController setWindowAutosaveKey:MMTopLeftPointKey];
654     }
656     if (openSelectionString) {
657         // TODO: Pass this as a parameter instead!  Get rid of
658         // 'openSelectionString' etc.
659         //
660         // There is some text to paste into this window as a result of the
661         // services menu "Open selection ..." being used.
662         [[windowController vimController] dropString:openSelectionString];
663         [openSelectionString release];
664         openSelectionString = nil;
665     }
667     if (shouldActivateWhenNextWindowOpens) {
668         [NSApp activateIgnoringOtherApps:YES];
669         shouldActivateWhenNextWindowOpens = NO;
670     }
673 - (void)setMainMenu:(NSMenu *)mainMenu
675     if ([NSApp mainMenu] == mainMenu) return;
677     // If the new menu has a "Recent Files" dummy item, then swap the real item
678     // for the dummy.  We are forced to do this since Cocoa initializes the
679     // "Recent Files" menu and there is no way to simply point Cocoa to a new
680     // item each time the menus are swapped.
681     NSMenu *fileMenu = [mainMenu findFileMenu];
682     if (recentFilesMenuItem && fileMenu) {
683         int dummyIdx =
684                 [fileMenu indexOfItemWithAction:@selector(recentFilesDummy:)];
685         if (dummyIdx >= 0) {
686             NSMenuItem *dummyItem = [[fileMenu itemAtIndex:dummyIdx] retain];
687             [fileMenu removeItemAtIndex:dummyIdx];
689             NSMenu *recentFilesParentMenu = [recentFilesMenuItem menu];
690             int idx = [recentFilesParentMenu indexOfItem:recentFilesMenuItem];
691             if (idx >= 0) {
692                 [[recentFilesMenuItem retain] autorelease];
693                 [recentFilesParentMenu removeItemAtIndex:idx];
694                 [recentFilesParentMenu insertItem:dummyItem atIndex:idx];
695             }
697             [fileMenu insertItem:recentFilesMenuItem atIndex:dummyIdx];
698             [dummyItem release];
699         }
700     }
702     // Now set the new menu.  Notice that we keep one menu for each editor
703     // window since each editor can have its own set of menus.  When swapping
704     // menus we have to tell Cocoa where the new "MacVim", "Windows", and
705     // "Services" menu are.
706     [NSApp setMainMenu:mainMenu];
708     // Setting the "MacVim" (or "Application") menu ensures that it is typeset
709     // in boldface.  (The setAppleMenu: method used to be public but is now
710     // private so this will have to be considered a bit of a hack!)
711     NSMenu *appMenu = [mainMenu findApplicationMenu];
712     [NSApp performSelector:@selector(setAppleMenu:) withObject:appMenu];
714     NSMenu *servicesMenu = [mainMenu findServicesMenu];
715     [NSApp setServicesMenu:servicesMenu];
717     NSMenu *windowsMenu = [mainMenu findWindowsMenu];
718     if (windowsMenu) {
719         // Cocoa isn't clever enough to get rid of items it has added to the
720         // "Windows" menu so we have to do it ourselves otherwise there will be
721         // multiple menu items for each window in the "Windows" menu.
722         //   This code assumes that the only items Cocoa add are ones which
723         // send off the action makeKeyAndOrderFront:.  (Cocoa will not add
724         // another separator item if the last item on the "Windows" menu
725         // already is a separator, so we needen't worry about separators.)
726         int i, count = [windowsMenu numberOfItems];
727         for (i = count-1; i >= 0; --i) {
728             NSMenuItem *item = [windowsMenu itemAtIndex:i];
729             if ([item action] == @selector(makeKeyAndOrderFront:))
730                 [windowsMenu removeItem:item];
731         }
732     }
733     [NSApp setWindowsMenu:windowsMenu];
735 #ifdef MM_ENABLE_PLUGINS
736     // Move plugin menu from old to new main menu.
737     [self removePlugInMenu];
738     [self addPlugInMenuToMenu:mainMenu];
739 #endif
742 - (NSArray *)filterOpenFiles:(NSArray *)filenames
744     return [self filterOpenFiles:filenames openFilesDict:nil];
747 - (BOOL)openFiles:(NSArray *)filenames withArguments:(NSDictionary *)args
749     // Opening files works like this:
750     //  a) filter out any already open files
751     //  b) open any remaining files
752     //
753     // A file is opened in an untitled window if there is one (it may be
754     // currently launching, or it may already be visible), otherwise a new
755     // window is opened.
756     //
757     // Each launching Vim process has a dictionary of arguments that are passed
758     // to the process when in checks in (via connectBackend:pid:).  The
759     // arguments for each launching process can be looked up by its PID (in the
760     // pidArguments dictionary).
762     NSMutableDictionary *arguments = (args ? [args mutableCopy]
763                                            : [NSMutableDictionary dictionary]);
765     //
766     // a) Filter out any already open files
767     //
768     NSString *firstFile = [filenames objectAtIndex:0];
769     MMVimController *firstController = nil;
770     NSDictionary *openFilesDict = nil;
771     filenames = [self filterOpenFiles:filenames openFilesDict:&openFilesDict];
773     // Pass arguments to vim controllers that had files open.
774     id key;
775     NSEnumerator *e = [openFilesDict keyEnumerator];
777     // (Indicate that we do not wish to open any files at the moment.)
778     [arguments setObject:[NSNumber numberWithBool:YES] forKey:@"dontOpen"];
780     while ((key = [e nextObject])) {
781         NSArray *files = [openFilesDict objectForKey:key];
782         [arguments setObject:files forKey:@"filenames"];
784         MMVimController *vc = [key pointerValue];
785         [vc passArguments:arguments];
787         // If this controller holds the first file, then remember it for later.
788         if ([files containsObject:firstFile])
789             firstController = vc;
790     }
792     if ([filenames count] == 0) {
793         // Raise the window containing the first file that was already open,
794         // and make sure that the tab containing that file is selected.  Only
795         // do this when there are no more files to open, otherwise sometimes
796         // the window with 'firstFile' will be raised, other times it might be
797         // the window that will open with the files in the 'filenames' array.
798         firstFile = [firstFile stringByEscapingSpecialFilenameCharacters];
799         NSString *input = [NSString stringWithFormat:@"<C-\\><C-N>"
800                 ":let oldswb=&swb|let &swb=\"useopen,usetab\"|"
801                 "tab sb %@|let &swb=oldswb|unl oldswb|"
802                 "cal foreground()|redr|f<CR>", firstFile];
804         [firstController addVimInput:input];
806         return YES;
807     }
809     // Add filenames to "Recent Files" menu, unless they are being edited
810     // remotely (using ODB).
811     if ([arguments objectForKey:@"remoteID"] == nil) {
812         [[NSDocumentController sharedDocumentController]
813                 noteNewRecentFilePaths:filenames];
814     }
816     //
817     // b) Open any remaining files
818     //
819     MMVimController *vc;
820     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
821     BOOL openInCurrentWindow = [ud boolForKey:MMOpenInCurrentWindowKey];
823     // The meaning of "layout" is defined by the WIN_* defines in main.c.
824     int layout = [ud integerForKey:MMOpenLayoutKey];
825     BOOL splitVert = [ud boolForKey:MMVerticalSplitKey];
826     if (splitVert && MMLayoutHorizontalSplit == layout)
827         layout = MMLayoutVerticalSplit;
828     if (layout < 0 || (layout > MMLayoutTabs && openInCurrentWindow))
829         layout = MMLayoutTabs;
831     [arguments setObject:[NSNumber numberWithInt:layout] forKey:@"layout"];
832     [arguments setObject:filenames forKey:@"filenames"];
833     // (Indicate that files should be opened from now on.)
834     [arguments setObject:[NSNumber numberWithBool:NO] forKey:@"dontOpen"];
836     if (openInCurrentWindow && (vc = [self topmostVimController])) {
837         // Open files in an already open window.
838         [[[vc windowController] window] makeKeyAndOrderFront:self];
839         [vc passArguments:arguments];
840         return YES;
841     }
843     BOOL openOk = YES;
844     int numFiles = [filenames count];
845     if (MMLayoutWindows == layout && numFiles > 1) {
846         // Open one file at a time in a new window, but don't open too many at
847         // once (at most cap+1 windows will open).  If the user has increased
848         // the preload cache size we'll take that as a hint that more windows
849         // should be able to open at once.
850         int cap = [self maxPreloadCacheSize] - 1;
851         if (cap < 4) cap = 4;
852         if (cap > numFiles) cap = numFiles;
854         int i;
855         for (i = 0; i < cap; ++i) {
856             NSArray *a = [NSArray arrayWithObject:[filenames objectAtIndex:i]];
857             [arguments setObject:a forKey:@"filenames"];
859             // NOTE: We have to copy the args since we'll mutate them in the
860             // next loop and the below call may retain the arguments while
861             // waiting for a process to start.
862             NSDictionary *args = [[arguments copy] autorelease];
864             openOk = [self openVimControllerWithArguments:args];
865             if (!openOk) break;
866         }
868         // Open remaining files in tabs in a new window.
869         if (openOk && numFiles > cap) {
870             NSRange range = { i, numFiles-cap };
871             NSArray *a = [filenames subarrayWithRange:range];
872             [arguments setObject:a forKey:@"filenames"];
873             [arguments setObject:[NSNumber numberWithInt:MMLayoutTabs]
874                           forKey:@"layout"];
876             openOk = [self openVimControllerWithArguments:arguments];
877         }
878     } else {
879         // Open all files at once.
880         openOk = [self openVimControllerWithArguments:arguments];
881     }
883     return openOk;
886 #ifdef MM_ENABLE_PLUGINS
887 - (void)addItemToPlugInMenu:(NSMenuItem *)item
889     NSMenu *menu = [plugInMenuItem submenu];
890     [menu addItem:item];
891     if ([menu numberOfItems] == 1)
892         [self addPlugInMenuToMenu:[NSApp mainMenu]];
895 - (void)removeItemFromPlugInMenu:(NSMenuItem *)item
897     NSMenu *menu = [plugInMenuItem submenu];
898     [menu removeItem:item];
899     if ([menu numberOfItems] == 0)
900         [self removePlugInMenu];
902 #endif
904 - (IBAction)newWindow:(id)sender
906     // A cached controller requires no loading times and results in the new
907     // window popping up instantaneously.  If the cache is empty it may take
908     // 1-2 seconds to start a new Vim process.
909     MMVimController *vc = [self takeVimControllerFromCache];
910     if (vc) {
911         [[vc backendProxy] acknowledgeConnection];
912     } else {
913         [self launchVimProcessWithArguments:nil];
914     }
917 - (IBAction)newWindowAndActivate:(id)sender
919     [self activateWhenNextWindowOpens];
920     [self newWindow:sender];
923 - (IBAction)fileOpen:(id)sender
925     NSString *dir = nil;
926     BOOL trackPwd = [[NSUserDefaults standardUserDefaults]
927             boolForKey:MMDialogsTrackPwdKey];
928     if (trackPwd) {
929         MMVimController *vc = [self keyVimController];
930         if (vc) dir = [[vc vimState] objectForKey:@"pwd"];
931     }
933     NSOpenPanel *panel = [NSOpenPanel openPanel];
934     [panel setAllowsMultipleSelection:YES];
935     [panel setAccessoryView:openPanelAccessoryView()];
937     int result = [panel runModalForDirectory:dir file:nil types:nil];
938     if (NSOKButton == result)
939         [self application:NSApp openFiles:[panel filenames]];
942 - (IBAction)selectNextWindow:(id)sender
944     unsigned i, count = [vimControllers count];
945     if (!count) return;
947     NSWindow *keyWindow = [NSApp keyWindow];
948     for (i = 0; i < count; ++i) {
949         MMVimController *vc = [vimControllers objectAtIndex:i];
950         if ([[[vc windowController] window] isEqual:keyWindow])
951             break;
952     }
954     if (i < count) {
955         if (++i >= count)
956             i = 0;
957         MMVimController *vc = [vimControllers objectAtIndex:i];
958         [[vc windowController] showWindow:self];
959     }
962 - (IBAction)selectPreviousWindow:(id)sender
964     unsigned i, count = [vimControllers count];
965     if (!count) return;
967     NSWindow *keyWindow = [NSApp keyWindow];
968     for (i = 0; i < count; ++i) {
969         MMVimController *vc = [vimControllers objectAtIndex:i];
970         if ([[[vc windowController] window] isEqual:keyWindow])
971             break;
972     }
974     if (i < count) {
975         if (i > 0) {
976             --i;
977         } else {
978             i = count - 1;
979         }
980         MMVimController *vc = [vimControllers objectAtIndex:i];
981         [[vc windowController] showWindow:self];
982     }
985 - (IBAction)orderFrontPreferencePanel:(id)sender
987     [[MMPreferenceController sharedPrefsWindowController] showWindow:self];
990 - (IBAction)openWebsite:(id)sender
992     [[NSWorkspace sharedWorkspace] openURL:
993             [NSURL URLWithString:MMWebsiteString]];
996 - (IBAction)showVimHelp:(id)sender
998     // Open a new window with the help window maximized.
999     [self launchVimProcessWithArguments:[NSArray arrayWithObjects:
1000             @"-c", @":h gui_mac", @"-c", @":res", nil]];
1003 - (IBAction)zoomAll:(id)sender
1005     [NSApp makeWindowsPerform:@selector(performZoom:) inOrder:YES];
1008 - (IBAction)atsuiButtonClicked:(id)sender
1010     // This action is called when the user clicks the "use ATSUI renderer"
1011     // button in the advanced preferences pane.
1012     [self rebuildPreloadCache];
1015 - (IBAction)loginShellButtonClicked:(id)sender
1017     // This action is called when the user clicks the "use login shell" button
1018     // in the advanced preferences pane.
1019     [self rebuildPreloadCache];
1022 - (IBAction)quickstartButtonClicked:(id)sender
1024     if ([self maxPreloadCacheSize] > 0) {
1025         [self scheduleVimControllerPreloadAfterDelay:1.0];
1026         [self startWatchingVimDir];
1027     } else {
1028         [self cancelVimControllerPreloadRequests];
1029         [self clearPreloadCacheWithCount:-1];
1030         [self stopWatchingVimDir];
1031     }
1034 - (byref id <MMFrontendProtocol>)
1035     connectBackend:(byref in id <MMBackendProtocol>)backend
1036                pid:(int)pid
1038     //NSLog(@"Connect backend (pid=%d)", pid);
1039     NSNumber *pidKey = [NSNumber numberWithInt:pid];
1040     MMVimController *vc = nil;
1042     @try {
1043         [(NSDistantObject*)backend
1044                 setProtocolForProxy:@protocol(MMBackendProtocol)];
1046         vc = [[[MMVimController alloc] initWithBackend:backend pid:pid]
1047                 autorelease];
1049         if (preloadPid == pid) {
1050             // This backend was preloaded, so add it to the cache and schedule
1051             // another vim process to be preloaded.
1052             preloadPid = -1;
1053             [vc setIsPreloading:YES];
1054             [cachedVimControllers addObject:vc];
1055             [self scheduleVimControllerPreloadAfterDelay:1];
1057             return vc;
1058         }
1060         [vimControllers addObject:vc];
1062         id args = [pidArguments objectForKey:pidKey];
1063         if (args && [NSNull null] != args)
1064             [vc passArguments:args];
1066         // HACK!  MacVim does not get activated if it is launched from the
1067         // terminal, so we forcibly activate here unless it is an untitled
1068         // window opening.  Untitled windows are treated differently, else
1069         // MacVim would steal the focus if another app was activated while the
1070         // untitled window was loading.
1071         if (!args || args != [NSNull null])
1072             [NSApp activateIgnoringOtherApps:YES];
1074         if (args)
1075             [pidArguments removeObjectForKey:pidKey];
1077         return vc;
1078     }
1080     @catch (NSException *e) {
1081         NSLog(@"Exception caught in %s: \"%@\"", _cmd, e);
1083         if (vc)
1084             [vimControllers removeObject:vc];
1086         [pidArguments removeObjectForKey:pidKey];
1087     }
1089     return nil;
1092 - (NSArray *)serverList
1094     NSMutableArray *array = [NSMutableArray array];
1096     unsigned i, count = [vimControllers count];
1097     for (i = 0; i < count; ++i) {
1098         MMVimController *controller = [vimControllers objectAtIndex:i];
1099         if ([controller serverName])
1100             [array addObject:[controller serverName]];
1101     }
1103     return array;
1106 - (MMVimController *)keyVimController
1108     NSWindow *keyWindow = [NSApp keyWindow];
1109     if (keyWindow) {
1110         unsigned i, count = [vimControllers count];
1111         for (i = 0; i < count; ++i) {
1112             MMVimController *vc = [vimControllers objectAtIndex:i];
1113             if ([[[vc windowController] window] isEqual:keyWindow])
1114                 return vc;
1115         }
1116     }
1118     return nil;
1121 @end // MMAppController
1126 @implementation MMAppController (MMServices)
1128 - (void)openSelection:(NSPasteboard *)pboard userData:(NSString *)userData
1129                 error:(NSString **)error
1131     if (![[pboard types] containsObject:NSStringPboardType]) {
1132         NSLog(@"WARNING: Pasteboard contains no object of type "
1133                 "NSStringPboardType");
1134         return;
1135     }
1137     MMVimController *vc = [self topmostVimController];
1138     if (vc) {
1139         // Open a new tab first, since dropString: does not do this.
1140         [vc sendMessage:AddNewTabMsgID data:nil];
1141         [vc dropString:[pboard stringForType:NSStringPboardType]];
1142     } else {
1143         // NOTE: There is no window to paste the selection into, so save the
1144         // text, open a new window, and paste the text when the next window
1145         // opens.  (If this is called several times in a row, then all but the
1146         // last call might be ignored.)
1147         if (openSelectionString) [openSelectionString release];
1148         openSelectionString = [[pboard stringForType:NSStringPboardType] copy];
1150         [self newWindow:self];
1151     }
1154 - (void)openFile:(NSPasteboard *)pboard userData:(NSString *)userData
1155            error:(NSString **)error
1157     if (![[pboard types] containsObject:NSStringPboardType]) {
1158         NSLog(@"WARNING: Pasteboard contains no object of type "
1159                 "NSStringPboardType");
1160         return;
1161     }
1163     // TODO: Parse multiple filenames and create array with names.
1164     NSString *string = [pboard stringForType:NSStringPboardType];
1165     string = [string stringByTrimmingCharactersInSet:
1166             [NSCharacterSet whitespaceAndNewlineCharacterSet]];
1167     string = [string stringByStandardizingPath];
1169     NSArray *filenames = [self filterFilesAndNotify:
1170             [NSArray arrayWithObject:string]];
1171     if ([filenames count] > 0) {
1172         MMVimController *vc = nil;
1173         if (userData && [userData isEqual:@"Tab"])
1174             vc = [self topmostVimController];
1176         if (vc) {
1177             [vc dropFiles:filenames forceOpen:YES];
1178         } else {
1179             [self openFiles:filenames withArguments:nil];
1180         }
1181     }
1184 @end // MMAppController (MMServices)
1189 @implementation MMAppController (Private)
1191 - (MMVimController *)topmostVimController
1193     // Find the topmost visible window which has an associated vim controller.
1194     NSEnumerator *e = [[NSApp orderedWindows] objectEnumerator];
1195     id window;
1196     while ((window = [e nextObject]) && [window isVisible]) {
1197         unsigned i, count = [vimControllers count];
1198         for (i = 0; i < count; ++i) {
1199             MMVimController *vc = [vimControllers objectAtIndex:i];
1200             if ([[[vc windowController] window] isEqual:window])
1201                 return vc;
1202         }
1203     }
1205     return nil;
1208 - (int)launchVimProcessWithArguments:(NSArray *)args
1210     int pid = -1;
1211     NSString *path = [[NSBundle mainBundle] pathForAuxiliaryExecutable:@"Vim"];
1213     if (!path) {
1214         NSLog(@"ERROR: Vim executable could not be found inside app bundle!");
1215         return -1;
1216     }
1218     NSArray *taskArgs = [NSArray arrayWithObjects:@"-g", @"-f", nil];
1219     if (args)
1220         taskArgs = [taskArgs arrayByAddingObjectsFromArray:args];
1222     BOOL useLoginShell = [[NSUserDefaults standardUserDefaults]
1223             boolForKey:MMLoginShellKey];
1224     if (useLoginShell) {
1225         // Run process with a login shell, roughly:
1226         //   echo "exec Vim -g -f args" | ARGV0=-`basename $SHELL` $SHELL [-l]
1227         pid = executeInLoginShell(path, taskArgs);
1228     } else {
1229         // Run process directly:
1230         //   Vim -g -f args
1231         NSTask *task = [NSTask launchedTaskWithLaunchPath:path
1232                                                 arguments:taskArgs];
1233         pid = task ? [task processIdentifier] : -1;
1234     }
1236     if (-1 != pid) {
1237         // NOTE: If the process has no arguments, then add a null argument to
1238         // the pidArguments dictionary.  This is later used to detect that a
1239         // process without arguments is being launched.
1240         if (!args)
1241             [pidArguments setObject:[NSNull null]
1242                              forKey:[NSNumber numberWithInt:pid]];
1243     } else {
1244         NSLog(@"WARNING: %s%@ failed (useLoginShell=%d)", _cmd, args,
1245                 useLoginShell);
1246     }
1248     return pid;
1251 - (NSArray *)filterFilesAndNotify:(NSArray *)filenames
1253     // Go trough 'filenames' array and make sure each file exists.  Present
1254     // warning dialog if some file was missing.
1256     NSString *firstMissingFile = nil;
1257     NSMutableArray *files = [NSMutableArray array];
1258     unsigned i, count = [filenames count];
1260     for (i = 0; i < count; ++i) {
1261         NSString *name = [filenames objectAtIndex:i];
1262         if ([[NSFileManager defaultManager] fileExistsAtPath:name]) {
1263             [files addObject:name];
1264         } else if (!firstMissingFile) {
1265             firstMissingFile = name;
1266         }
1267     }
1269     if (firstMissingFile) {
1270         NSAlert *alert = [[NSAlert alloc] init];
1271         [alert addButtonWithTitle:NSLocalizedString(@"OK",
1272                 @"Dialog button")];
1274         NSString *text;
1275         if ([files count] >= count-1) {
1276             [alert setMessageText:NSLocalizedString(@"File not found",
1277                     @"File not found dialog, title")];
1278             text = [NSString stringWithFormat:NSLocalizedString(
1279                     @"Could not open file with name %@.",
1280                     @"File not found dialog, text"), firstMissingFile];
1281         } else {
1282             [alert setMessageText:NSLocalizedString(@"Multiple files not found",
1283                     @"File not found dialog, title")];
1284             text = [NSString stringWithFormat:NSLocalizedString(
1285                     @"Could not open file with name %@, and %d other files.",
1286                     @"File not found dialog, text"),
1287                 firstMissingFile, count-[files count]-1];
1288         }
1290         [alert setInformativeText:text];
1291         [alert setAlertStyle:NSWarningAlertStyle];
1293         [alert runModal];
1294         [alert release];
1296         [NSApp replyToOpenOrPrint:NSApplicationDelegateReplyFailure];
1297     }
1299     return files;
1302 - (NSArray *)filterOpenFiles:(NSArray *)filenames
1303                openFilesDict:(NSDictionary **)openFiles
1305     // Filter out any files in the 'filenames' array that are open and return
1306     // all files that are not already open.  On return, the 'openFiles'
1307     // parameter (if non-nil) will point to a dictionary of open files, indexed
1308     // by Vim controller.
1310     NSMutableDictionary *dict = [NSMutableDictionary dictionary];
1311     NSMutableArray *files = [filenames mutableCopy];
1313     // TODO: Escape special characters in 'files'?
1314     NSString *expr = [NSString stringWithFormat:
1315             @"map([\"%@\"],\"bufloaded(v:val)\")",
1316             [files componentsJoinedByString:@"\",\""]];
1318     unsigned i, count = [vimControllers count];
1319     for (i = 0; i < count && [files count] > 0; ++i) {
1320         MMVimController *vc = [vimControllers objectAtIndex:i];
1322         // Query Vim for which files in the 'files' array are open.
1323         NSString *eval = [vc evaluateVimExpression:expr];
1324         if (!eval) continue;
1326         NSIndexSet *idxSet = [NSIndexSet indexSetWithVimList:eval];
1327         if ([idxSet count] > 0) {
1328             [dict setObject:[files objectsAtIndexes:idxSet]
1329                      forKey:[NSValue valueWithPointer:vc]];
1331             // Remove all the files that were open in this Vim process and
1332             // create a new expression to evaluate.
1333             [files removeObjectsAtIndexes:idxSet];
1334             expr = [NSString stringWithFormat:
1335                     @"map([\"%@\"],\"bufloaded(v:val)\")",
1336                     [files componentsJoinedByString:@"\",\""]];
1337         }
1338     }
1340     if (openFiles != nil)
1341         *openFiles = dict;
1343     return files;
1346 #if MM_HANDLE_XCODE_MOD_EVENT
1347 - (void)handleXcodeModEvent:(NSAppleEventDescriptor *)event
1348                  replyEvent:(NSAppleEventDescriptor *)reply
1350 #if 0
1351     // Xcode sends this event to query MacVim which open files have been
1352     // modified.
1353     NSLog(@"reply:%@", reply);
1354     NSLog(@"event:%@", event);
1356     NSEnumerator *e = [vimControllers objectEnumerator];
1357     id vc;
1358     while ((vc = [e nextObject])) {
1359         DescType type = [reply descriptorType];
1360         unsigned len = [[type data] length];
1361         NSMutableData *data = [NSMutableData data];
1363         [data appendBytes:&type length:sizeof(DescType)];
1364         [data appendBytes:&len length:sizeof(unsigned)];
1365         [data appendBytes:[reply data] length:len];
1367         [vc sendMessage:XcodeModMsgID data:data];
1368     }
1369 #endif
1371 #endif
1373 - (void)handleGetURLEvent:(NSAppleEventDescriptor *)event
1374                replyEvent:(NSAppleEventDescriptor *)reply
1376     NSString *urlString = [[event paramDescriptorForKeyword:keyDirectObject]
1377         stringValue];
1378     NSURL *url = [NSURL URLWithString:urlString];
1380     // We try to be compatible with TextMate's URL scheme here, as documented
1381     // at http://blog.macromates.com/2007/the-textmate-url-scheme/ . Currently,
1382     // this means that:
1383     //
1384     // The format is: mvim://open?<arguments> where arguments can be:
1385     //
1386     // * url â€” the actual file to open (i.e. a file://… URL), if you leave
1387     //         out this argument, the frontmost document is implied.
1388     // * line â€” line number to go to (one based).
1389     // * column â€” column number to go to (one based).
1390     //
1391     // Example: mvim://open?url=file:///etc/profile&line=20
1393     if ([[url host] isEqualToString:@"open"]) {
1394         NSMutableDictionary *dict = [NSMutableDictionary dictionary];
1396         // Parse query ("url=file://...&line=14") into a dictionary
1397         NSArray *queries = [[url query] componentsSeparatedByString:@"&"];
1398         NSEnumerator *enumerator = [queries objectEnumerator];
1399         NSString *param;
1400         while( param = [enumerator nextObject] ) {
1401             NSArray *arr = [param componentsSeparatedByString:@"="];
1402             if ([arr count] == 2) {
1403                 [dict setValue:[[arr lastObject]
1404                             stringByReplacingPercentEscapesUsingEncoding:
1405                                 NSUTF8StringEncoding]
1406                         forKey:[[arr objectAtIndex:0]
1407                             stringByReplacingPercentEscapesUsingEncoding:
1408                                 NSUTF8StringEncoding]];
1409             }
1410         }
1412         // Actually open the file.
1413         NSString *file = [dict objectForKey:@"url"];
1414         if (file != nil) {
1415             NSURL *fileUrl= [NSURL URLWithString:file];
1416             // TextMate only opens files that already exist.
1417             if ([fileUrl isFileURL]
1418                     && [[NSFileManager defaultManager] fileExistsAtPath:
1419                            [fileUrl path]]) {
1420                 // Strip 'file://' path, else application:openFiles: might think
1421                 // the file is not yet open.
1422                 NSArray *filenames = [NSArray arrayWithObject:[fileUrl path]];
1424                 // Look for the line and column options.
1425                 NSDictionary *args = nil;
1426                 NSString *line = [dict objectForKey:@"line"];
1427                 if (line) {
1428                     NSString *column = [dict objectForKey:@"column"];
1429                     if (column)
1430                         args = [NSDictionary dictionaryWithObjectsAndKeys:
1431                                 line, @"cursorLine",
1432                                 column, @"cursorColumn",
1433                                 nil];
1434                     else
1435                         args = [NSDictionary dictionaryWithObject:line
1436                                 forKey:@"cursorLine"];
1437                 }
1439                 [self openFiles:filenames withArguments:args];
1440             }
1441         }
1442     } else {
1443         NSAlert *alert = [[NSAlert alloc] init];
1444         [alert addButtonWithTitle:NSLocalizedString(@"OK",
1445             @"Dialog button")];
1447         [alert setMessageText:NSLocalizedString(@"Unknown URL Scheme",
1448             @"Unknown URL Scheme dialog, title")];
1449         [alert setInformativeText:[NSString stringWithFormat:NSLocalizedString(
1450             @"This version of MacVim does not support \"%@\""
1451             @" in its URL scheme.",
1452             @"Unknown URL Scheme dialog, text"),
1453             [url host]]];
1455         [alert setAlertStyle:NSWarningAlertStyle];
1456         [alert runModal];
1457         [alert release];
1458     }
1462 - (int)findLaunchingProcessWithoutArguments
1464     NSArray *keys = [pidArguments allKeysForObject:[NSNull null]];
1465     if ([keys count] > 0) {
1466         //NSLog(@"found launching process without arguments");
1467         return [[keys objectAtIndex:0] intValue];
1468     }
1470     return -1;
1473 - (MMVimController *)findUnusedEditor
1475     NSEnumerator *e = [vimControllers objectEnumerator];
1476     id vc;
1477     while ((vc = [e nextObject])) {
1478         if ([[[vc vimState] objectForKey:@"unusedEditor"] boolValue])
1479             return vc;
1480     }
1482     return nil;
1485 - (NSMutableDictionary *)extractArgumentsFromOdocEvent:
1486     (NSAppleEventDescriptor *)desc
1488     NSMutableDictionary *dict = [NSMutableDictionary dictionary];
1490     // 1. Extract ODB parameters (if any)
1491     NSAppleEventDescriptor *odbdesc = desc;
1492     if (![odbdesc paramDescriptorForKeyword:keyFileSender]) {
1493         // The ODB paramaters may hide inside the 'keyAEPropData' descriptor.
1494         odbdesc = [odbdesc paramDescriptorForKeyword:keyAEPropData];
1495         if (![odbdesc paramDescriptorForKeyword:keyFileSender])
1496             odbdesc = nil;
1497     }
1499     if (odbdesc) {
1500         NSAppleEventDescriptor *p =
1501                 [odbdesc paramDescriptorForKeyword:keyFileSender];
1502         if (p)
1503             [dict setObject:[NSNumber numberWithUnsignedInt:[p typeCodeValue]]
1504                      forKey:@"remoteID"];
1506         p = [odbdesc paramDescriptorForKeyword:keyFileCustomPath];
1507         if (p)
1508             [dict setObject:[p stringValue] forKey:@"remotePath"];
1510         p = [odbdesc paramDescriptorForKeyword:keyFileSenderToken];
1511         if (p) {
1512             [dict setObject:[NSNumber numberWithUnsignedLong:[p descriptorType]]
1513                      forKey:@"remoteTokenDescType"];
1514             [dict setObject:[p data] forKey:@"remoteTokenData"];
1515         }
1516     }
1518     // 2. Extract Xcode parameters (if any)
1519     NSAppleEventDescriptor *xcodedesc =
1520             [desc paramDescriptorForKeyword:keyAEPosition];
1521     if (xcodedesc) {
1522         NSRange range;
1523         MMSelectionRange *sr = (MMSelectionRange*)[[xcodedesc data] bytes];
1525         if (sr->lineNum < 0) {
1526             // Should select a range of lines.
1527             range.location = sr->startRange + 1;
1528             range.length = sr->endRange - sr->startRange + 1;
1529         } else {
1530             // Should only move cursor to a line.
1531             range.location = sr->lineNum + 1;
1532             range.length = 0;
1533         }
1535         [dict setObject:NSStringFromRange(range) forKey:@"selectionRange"];
1536     }
1538     // 3. Extract Spotlight search text (if any)
1539     NSAppleEventDescriptor *spotlightdesc = 
1540             [desc paramDescriptorForKeyword:keyAESearchText];
1541     if (spotlightdesc)
1542         [dict setObject:[spotlightdesc stringValue] forKey:@"searchText"];
1544     return dict;
1547 #ifdef MM_ENABLE_PLUGINS
1548 - (void)removePlugInMenu
1550     if ([plugInMenuItem menu])
1551         [[plugInMenuItem menu] removeItem:plugInMenuItem];
1554 - (void)addPlugInMenuToMenu:(NSMenu *)mainMenu
1556     NSMenu *windowsMenu = [mainMenu findWindowsMenu];
1558     if ([[plugInMenuItem submenu] numberOfItems] > 0) {
1559         int idx = windowsMenu ? [mainMenu indexOfItemWithSubmenu:windowsMenu]
1560                               : -1;
1561         if (idx > 0) {
1562             [mainMenu insertItem:plugInMenuItem atIndex:idx];
1563         } else {
1564             [mainMenu addItem:plugInMenuItem];
1565         }
1566     }
1568 #endif
1570 - (void)scheduleVimControllerPreloadAfterDelay:(NSTimeInterval)delay
1572     [self performSelector:@selector(preloadVimController:)
1573                withObject:nil
1574                afterDelay:delay];
1577 - (void)cancelVimControllerPreloadRequests
1579     [NSObject cancelPreviousPerformRequestsWithTarget:self
1580             selector:@selector(preloadVimController:)
1581               object:nil];
1584 - (void)preloadVimController:(id)sender
1586     // We only allow preloading of one Vim process at a time (to avoid hogging
1587     // CPU), so schedule another preload in a little while if necessary.
1588     if (-1 != preloadPid) {
1589         [self scheduleVimControllerPreloadAfterDelay:2];
1590         return;
1591     }
1593     if ([cachedVimControllers count] >= [self maxPreloadCacheSize])
1594         return;
1596     preloadPid = [self launchVimProcessWithArguments:
1597             [NSArray arrayWithObject:@"--mmwaitforack"]];
1600 - (int)maxPreloadCacheSize
1602     // The maximum number of Vim processes to keep in the cache can be
1603     // controlled via the user default "MMPreloadCacheSize".
1604     int maxCacheSize = [[NSUserDefaults standardUserDefaults]
1605             integerForKey:MMPreloadCacheSizeKey];
1606     if (maxCacheSize < 0) maxCacheSize = 0;
1607     else if (maxCacheSize > 10) maxCacheSize = 10;
1609     return maxCacheSize;
1612 - (MMVimController *)takeVimControllerFromCache
1614     // NOTE: After calling this message the backend corresponding to the
1615     // returned vim controller must be sent an acknowledgeConnection message,
1616     // else the vim process will be stuck.
1617     //
1618     // This method may return nil even though the cache might be non-empty; the
1619     // caller should handle this by starting a new Vim process.
1621     int i, count = [cachedVimControllers count];
1622     if (0 == count) return nil;
1624     // Locate the first Vim controller with up-to-date rc-files sourced.
1625     NSDate *rcDate = [self rcFilesModificationDate];
1626     for (i = 0; i < count; ++i) {
1627         MMVimController *vc = [cachedVimControllers objectAtIndex:i];
1628         NSDate *date = [vc creationDate];
1629         if ([date compare:rcDate] != NSOrderedAscending)
1630             break;
1631     }
1633     if (i > 0) {
1634         // Clear out cache entries whose vimrc/gvimrc files were sourced before
1635         // the latest modification date for those files.  This ensures that the
1636         // latest rc-files are always sourced for new windows.
1637         [self clearPreloadCacheWithCount:i];
1638     }
1640     if ([cachedVimControllers count] == 0) {
1641         [self scheduleVimControllerPreloadAfterDelay:2.0];
1642         return nil;
1643     }
1645     MMVimController *vc = [cachedVimControllers objectAtIndex:0];
1646     [vimControllers addObject:vc];
1647     [cachedVimControllers removeObjectAtIndex:0];
1648     [vc setIsPreloading:NO];
1650     // If the Vim process has finished loading then the window will displayed
1651     // now, otherwise it will be displayed when the OpenWindowMsgID message is
1652     // received.
1653     [[vc windowController] showWindow];
1655     // Since we've taken one controller from the cache we take the opportunity
1656     // to preload another.
1657     [self scheduleVimControllerPreloadAfterDelay:1];
1659     return vc;
1662 - (void)clearPreloadCacheWithCount:(int)count
1664     // Remove the 'count' first entries in the preload cache.  It is assumed
1665     // that objects are added/removed from the cache in a FIFO manner so that
1666     // this effectively clears the 'count' oldest entries.
1667     // If 'count' is negative, then the entire cache is cleared.
1669     if ([cachedVimControllers count] == 0 || count == 0)
1670         return;
1672     if (count < 0)
1673         count = [cachedVimControllers count];
1675     // Make sure the preloaded Vim processes get killed or they'll just hang
1676     // around being useless until MacVim is terminated.
1677     NSEnumerator *e = [cachedVimControllers objectEnumerator];
1678     MMVimController *vc;
1679     int n = count;
1680     while ((vc = [e nextObject]) && n-- > 0) {
1681         [[NSNotificationCenter defaultCenter] removeObserver:vc];
1682         [vc sendMessage:TerminateNowMsgID data:nil];
1684         // Since the preloaded processes were killed "prematurely" we have to
1685         // manually tell them to cleanup (it is not enough to simply release
1686         // them since deallocation and cleanup are separated).
1687         [vc cleanup];
1688     }
1690     n = count;
1691     while (n-- > 0 && [cachedVimControllers count] > 0)
1692         [cachedVimControllers removeObjectAtIndex:0];
1695 - (void)rebuildPreloadCache
1697     if ([self maxPreloadCacheSize] > 0) {
1698         [self clearPreloadCacheWithCount:-1];
1699         [self cancelVimControllerPreloadRequests];
1700         [self scheduleVimControllerPreloadAfterDelay:1.0];
1701     }
1704 - (NSDate *)rcFilesModificationDate
1706     // Check modification dates for ~/.vimrc and ~/.gvimrc and return the
1707     // latest modification date.  If ~/.vimrc does not exist, check ~/_vimrc
1708     // and similarly for gvimrc.
1709     // Returns distantPath if no rc files were found.
1711     NSDate *date = [NSDate distantPast];
1712     NSFileManager *fm = [NSFileManager defaultManager];
1714     NSString *path = [@"~/.vimrc" stringByExpandingTildeInPath];
1715     NSDictionary *attr = [fm fileAttributesAtPath:path traverseLink:YES];
1716     if (!attr) {
1717         path = [@"~/_vimrc" stringByExpandingTildeInPath];
1718         attr = [fm fileAttributesAtPath:path traverseLink:YES];
1719     }
1720     NSDate *modDate = [attr objectForKey:NSFileModificationDate];
1721     if (modDate)
1722         date = modDate;
1724     path = [@"~/.gvimrc" stringByExpandingTildeInPath];
1725     attr = [fm fileAttributesAtPath:path traverseLink:YES];
1726     if (!attr) {
1727         path = [@"~/_gvimrc" stringByExpandingTildeInPath];
1728         attr = [fm fileAttributesAtPath:path traverseLink:YES];
1729     }
1730     modDate = [attr objectForKey:NSFileModificationDate];
1731     if (modDate)
1732         date = [date laterDate:modDate];
1734     return date;
1737 - (BOOL)openVimControllerWithArguments:(NSDictionary *)arguments
1739     MMVimController *vc = [self findUnusedEditor];
1740     if (vc) {
1741         // Open files in an already open window.
1742         [[[vc windowController] window] makeKeyAndOrderFront:self];
1743         [vc passArguments:arguments];
1744     } else if ((vc = [self takeVimControllerFromCache])) {
1745         // Open files in a new window using a cached vim controller.  This
1746         // requires virtually no loading time so the new window will pop up
1747         // instantaneously.
1748         [vc passArguments:arguments];
1749         [[vc backendProxy] acknowledgeConnection];
1750     } else {
1751         // Open files in a launching Vim process or start a new process.  This
1752         // may take 1-2 seconds so there will be a visible delay before the
1753         // window appears on screen.
1754         int pid = [self findLaunchingProcessWithoutArguments];
1755         if (-1 == pid) {
1756             pid = [self launchVimProcessWithArguments:nil];
1757             if (-1 == pid)
1758                 return NO;
1759         }
1761         // TODO: If the Vim process fails to start, or if it changes PID,
1762         // then the memory allocated for these parameters will leak.
1763         // Ensure that this cannot happen or somehow detect it.
1765         if ([arguments count] > 0)
1766             [pidArguments setObject:arguments
1767                              forKey:[NSNumber numberWithInt:pid]];
1768     }
1770     return YES;
1773 - (void)activateWhenNextWindowOpens
1775     shouldActivateWhenNextWindowOpens = YES;
1778 - (void)startWatchingVimDir
1780     //NSLog(@"%s", _cmd);
1781 #if (MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_4)
1782     if (fsEventStream)
1783         return;
1784     if (NULL == FSEventStreamStart)
1785         return; // FSEvent functions are weakly linked
1787     NSString *path = [@"~/.vim" stringByExpandingTildeInPath];
1788     NSArray *pathsToWatch = [NSArray arrayWithObject:path];
1790     fsEventStream = FSEventStreamCreate(NULL, &fsEventCallback, NULL,
1791             (CFArrayRef)pathsToWatch, kFSEventStreamEventIdSinceNow,
1792             MMEventStreamLatency, kFSEventStreamCreateFlagNone);
1794     FSEventStreamScheduleWithRunLoop(fsEventStream,
1795             [[NSRunLoop currentRunLoop] getCFRunLoop],
1796             kCFRunLoopDefaultMode);
1798     FSEventStreamStart(fsEventStream);
1799     //NSLog(@"Started FS event stream");
1800 #endif
1803 - (void)stopWatchingVimDir
1805     //NSLog(@"%s", _cmd);
1806 #if (MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_4)
1807     if (NULL == FSEventStreamStop)
1808         return; // FSEvent functions are weakly linked
1810     if (fsEventStream) {
1811         FSEventStreamStop(fsEventStream);
1812         FSEventStreamInvalidate(fsEventStream);
1813         FSEventStreamRelease(fsEventStream);
1814         fsEventStream = NULL;
1815         //NSLog(@"Stopped FS event stream");
1816     }
1817 #endif
1821 - (void)handleFSEvent
1823     //NSLog(@"%s", _cmd);
1824     [self clearPreloadCacheWithCount:-1];
1826     // Several FS events may arrive in quick succession so make sure to cancel
1827     // any previous preload requests before making a new one.
1828     [self cancelVimControllerPreloadRequests];
1829     [self scheduleVimControllerPreloadAfterDelay:0.5];
1832 @end // MMAppController (Private)
1837     static int
1838 executeInLoginShell(NSString *path, NSArray *args)
1840     // Start a login shell and execute the command 'path' with arguments 'args'
1841     // in the shell.  This ensures that user environment variables are set even
1842     // when MacVim was started from the Finder.
1844     int pid = -1;
1845     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
1847     // Determine which shell to use to execute the command.  The user
1848     // may decide which shell to use by setting a user default or the
1849     // $SHELL environment variable.
1850     NSString *shell = [ud stringForKey:MMLoginShellCommandKey];
1851     if (!shell || [shell length] == 0)
1852         shell = [[[NSProcessInfo processInfo] environment]
1853             objectForKey:@"SHELL"];
1854     if (!shell)
1855         shell = @"/bin/bash";
1857     //NSLog(@"shell = %@", shell);
1859     // Bash needs the '-l' flag to launch a login shell.  The user may add
1860     // flags by setting a user default.
1861     NSString *shellArgument = [ud stringForKey:MMLoginShellArgumentKey];
1862     if (!shellArgument || [shellArgument length] == 0) {
1863         if ([[shell lastPathComponent] isEqual:@"bash"])
1864             shellArgument = @"-l";
1865         else
1866             shellArgument = nil;
1867     }
1869     //NSLog(@"shellArgument = %@", shellArgument);
1871     // Build input string to pipe to the login shell.
1872     NSMutableString *input = [NSMutableString stringWithFormat:
1873             @"exec \"%@\"", path];
1874     if (args) {
1875         // Append all arguments, making sure they are properly quoted, even
1876         // when they contain single quotes.
1877         NSEnumerator *e = [args objectEnumerator];
1878         id obj;
1880         while ((obj = [e nextObject])) {
1881             NSMutableString *arg = [NSMutableString stringWithString:obj];
1882             [arg replaceOccurrencesOfString:@"'" withString:@"'\"'\"'"
1883                                     options:NSLiteralSearch
1884                                       range:NSMakeRange(0, [arg length])];
1885             [input appendFormat:@" '%@'", arg];
1886         }
1887     }
1889     // Build the argument vector used to start the login shell.
1890     NSString *shellArg0 = [NSString stringWithFormat:@"-%@",
1891              [shell lastPathComponent]];
1892     char *shellArgv[3] = { (char *)[shellArg0 UTF8String], NULL, NULL };
1893     if (shellArgument)
1894         shellArgv[1] = (char *)[shellArgument UTF8String];
1896     // Get the C string representation of the shell path before the fork since
1897     // we must not call Foundation functions after a fork.
1898     const char *shellPath = [shell fileSystemRepresentation];
1900     // Fork and execute the process.
1901     int ds[2];
1902     if (pipe(ds)) return -1;
1904     pid = fork();
1905     if (pid == -1) {
1906         return -1;
1907     } else if (pid == 0) {
1908         // Child process
1909         if (close(ds[1]) == -1) exit(255);
1910         if (dup2(ds[0], 0) == -1) exit(255);
1912         execv(shellPath, shellArgv);
1914         // Never reached unless execv fails
1915         exit(255);
1916     } else {
1917         // Parent process
1918         if (close(ds[0]) == -1) return -1;
1920         // Send input to execute to the child process
1921         [input appendString:@"\n"];
1922         int bytes = [input lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
1924         if (write(ds[1], [input UTF8String], bytes) != bytes) return -1;
1925         if (close(ds[1]) == -1) return -1;
1926     }
1928     return pid;