File->Close sends performClose:
[MacVim.git] / src / MacVim / MMAppController.m
blob59961a05a8ceda98fa3bdf47c6e360959fa6f4ce
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"
34 #define MM_HANDLE_XCODE_MOD_EVENT 0
38 // Default timeout intervals on all connections.
39 static NSTimeInterval MMRequestTimeout = 5;
40 static NSTimeInterval MMReplyTimeout = 5;
43 #pragma options align=mac68k
44 typedef struct
46     short unused1;      // 0 (not used)
47     short lineNum;      // line to select (< 0 to specify range)
48     long  startRange;   // start of selection range (if line < 0)
49     long  endRange;     // end of selection range (if line < 0)
50     long  unused2;      // 0 (not used)
51     long  theDate;      // modification date/time
52 } MMSelectionRange;
53 #pragma options align=reset
56 @interface MMAppController (MMServices)
57 - (void)openSelection:(NSPasteboard *)pboard userData:(NSString *)userData
58                 error:(NSString **)error;
59 - (void)openFile:(NSPasteboard *)pboard userData:(NSString *)userData
60            error:(NSString **)error;
61 @end
64 @interface MMAppController (Private)
65 - (MMVimController *)keyVimController;
66 - (MMVimController *)topmostVimController;
67 - (int)launchVimProcessWithArguments:(NSArray *)args;
68 - (NSArray *)filterFilesAndNotify:(NSArray *)files;
69 - (NSArray *)filterOpenFiles:(NSArray *)filenames
70                    arguments:(NSDictionary *)args;
71 #if MM_HANDLE_XCODE_MOD_EVENT
72 - (void)handleXcodeModEvent:(NSAppleEventDescriptor *)event
73                  replyEvent:(NSAppleEventDescriptor *)reply;
74 #endif
75 - (int)findLaunchingProcessWithoutArguments;
76 - (MMVimController *)findUntitledWindow;
77 - (NSMutableDictionary *)extractArgumentsFromOdocEvent:
78     (NSAppleEventDescriptor *)desc;
79 - (void)passArguments:(NSDictionary *)args toVimController:(MMVimController*)vc;
80 @end
82 @interface NSMenu (MMExtras)
83 - (void)recurseSetAutoenablesItems:(BOOL)on;
84 @end
86 @interface NSNumber (MMExtras)
87 - (int)tag;
88 @end
92 @implementation MMAppController
94 + (void)initialize
96     NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:
97         [NSNumber numberWithBool:NO],   MMNoWindowKey,
98         [NSNumber numberWithInt:64],    MMTabMinWidthKey,
99         [NSNumber numberWithInt:6*64],  MMTabMaxWidthKey,
100         [NSNumber numberWithInt:132],   MMTabOptimumWidthKey,
101         [NSNumber numberWithInt:2],     MMTextInsetLeftKey,
102         [NSNumber numberWithInt:1],     MMTextInsetRightKey,
103         [NSNumber numberWithInt:1],     MMTextInsetTopKey,
104         [NSNumber numberWithInt:1],     MMTextInsetBottomKey,
105         [NSNumber numberWithBool:NO],   MMTerminateAfterLastWindowClosedKey,
106         @"MMTypesetter",                MMTypesetterKey,
107         [NSNumber numberWithFloat:1],   MMCellWidthMultiplierKey,
108         [NSNumber numberWithFloat:-1],  MMBaselineOffsetKey,
109         [NSNumber numberWithBool:YES],  MMTranslateCtrlClickKey,
110         [NSNumber numberWithBool:NO],   MMOpenFilesInTabsKey,
111         [NSNumber numberWithBool:NO],   MMNoFontSubstitutionKey,
112         [NSNumber numberWithBool:NO],   MMLoginShellKey,
113         [NSNumber numberWithBool:NO],   MMAtsuiRendererKey,
114         [NSNumber numberWithInt:MMUntitledWindowAlways],
115                                         MMUntitledWindowKey,
116         [NSNumber numberWithBool:NO],   MMTexturedWindowKey,
117         nil];
119     [[NSUserDefaults standardUserDefaults] registerDefaults:dict];
121     NSArray *types = [NSArray arrayWithObject:NSStringPboardType];
122     [NSApp registerServicesMenuSendTypes:types returnTypes:types];
125 - (id)init
127     if ((self = [super init])) {
128         fontContainerRef = loadFonts();
130         vimControllers = [NSMutableArray new];
131         pidArguments = [NSMutableDictionary new];
133         // NOTE!  If the name of the connection changes here it must also be
134         // updated in MMBackend.m.
135         NSConnection *connection = [NSConnection defaultConnection];
136         NSString *name = [NSString stringWithFormat:@"%@-connection",
137                  [[NSBundle mainBundle] bundleIdentifier]];
138         //NSLog(@"Registering connection with name '%@'", name);
139         if ([connection registerName:name]) {
140             [connection setRequestTimeout:MMRequestTimeout];
141             [connection setReplyTimeout:MMReplyTimeout];
142             [connection setRootObject:self];
144             // NOTE: When the user is resizing the window the AppKit puts the
145             // run loop in event tracking mode.  Unless the connection listens
146             // to request in this mode, live resizing won't work.
147             [connection addRequestMode:NSEventTrackingRunLoopMode];
148         } else {
149             NSLog(@"WARNING: Failed to register connection with name '%@'",
150                     name);
151         }
152     }
154     return self;
157 - (void)dealloc
159     //NSLog(@"MMAppController dealloc");
161     [pidArguments release];  pidArguments = nil;
162     [vimControllers release];  vimControllers = nil;
163     [openSelectionString release];  openSelectionString = nil;
165     [super dealloc];
168 #if MM_HANDLE_XCODE_MOD_EVENT
169 - (void)applicationWillFinishLaunching:(NSNotification *)notification
171     [[NSAppleEventManager sharedAppleEventManager]
172             setEventHandler:self
173                 andSelector:@selector(handleXcodeModEvent:replyEvent:)
174               forEventClass:'KAHL'
175                  andEventID:'MOD '];
177 #endif
179 - (void)applicationDidFinishLaunching:(NSNotification *)notification
181     [NSApp setServicesProvider:self];
184 - (BOOL)applicationShouldOpenUntitledFile:(NSApplication *)sender
186     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
187     NSAppleEventManager *aem = [NSAppleEventManager sharedAppleEventManager];
188     NSAppleEventDescriptor *desc = [aem currentAppleEvent];
190     // The user default MMUntitledWindow can be set to control whether an
191     // untitled window should open on 'Open' and 'Reopen' events.
192     int untitledWindowFlag = [ud integerForKey:MMUntitledWindowKey];
193     if ([desc eventID] == kAEOpenApplication
194             && (untitledWindowFlag & MMUntitledWindowOnOpen) == 0)
195         return NO;
196     else if ([desc eventID] == kAEReopenApplication
197             && (untitledWindowFlag & MMUntitledWindowOnReopen) == 0)
198         return NO;
200     // When a process is started from the command line, the 'Open' event will
201     // contain a parameter to surpress the opening of an untitled window.
202     desc = [desc paramDescriptorForKeyword:keyAEPropData];
203     desc = [desc paramDescriptorForKeyword:keyMMUntitledWindow];
204     if (desc && ![desc booleanValue])
205         return NO;
207     // Never open an untitled window if there is at least one open window or if
208     // there are processes that are currently launching.
209     if ([vimControllers count] > 0 || [pidArguments count] > 0)
210         return NO;
212     // NOTE!  This way it possible to start the app with the command-line
213     // argument '-nowindow yes' and no window will be opened by default.
214     return ![ud boolForKey:MMNoWindowKey];
217 - (BOOL)applicationOpenUntitledFile:(NSApplication *)sender
219     [self newWindow:self];
220     return YES;
223 - (void)application:(NSApplication *)sender openFiles:(NSArray *)filenames
225     // Opening files works like this:
226     //  a) extract ODB/Xcode/Spotlight parameters from the current Apple event
227     //  b) filter out any already open files (see filterOpenFiles::)
228     //  c) open any remaining files
229     //
230     // A file is opened in an untitled window if there is one (it may be
231     // currently launching, or it may already be visible), otherwise a new
232     // window is opened.
233     //
234     // Each launching Vim process has a dictionary of arguments that are passed
235     // to the process when in checks in (via connectBackend:pid:).  The
236     // arguments for each launching process can be looked up by its PID (in the
237     // pidArguments dictionary).
239     NSMutableDictionary *arguments = [self extractArgumentsFromOdocEvent:
240             [[NSAppleEventManager sharedAppleEventManager] currentAppleEvent]];
242     // Filter out files that are already open
243     filenames = [self filterOpenFiles:filenames arguments:arguments];
245     // Open any files that remain
246     if ([filenames count]) {
247         MMVimController *vc;
248         BOOL openInTabs = [[NSUserDefaults standardUserDefaults]
249             boolForKey:MMOpenFilesInTabsKey];
251         [arguments setObject:filenames forKey:@"filenames"];
252         [arguments setObject:[NSNumber numberWithBool:YES] forKey:@"openFiles"];
254         if ((openInTabs && (vc = [self topmostVimController]))
255                || (vc = [self findUntitledWindow])) {
256             // Open files in an already open window.
257             [self passArguments:arguments toVimController:vc];
258         } else {
259             // Open files in a launching Vim process or start a new process.
260             int pid = [self findLaunchingProcessWithoutArguments];
261             if (!pid) {
262                 // Pass the filenames to the process straight away.
263                 //
264                 // TODO: It would be nicer if all arguments were passed to the
265                 // Vim process in connectBackend::, but if we don't pass the
266                 // filename arguments here, the window 'flashes' once when it
267                 // opens.  This is due to the 'welcome' screen first being
268                 // displayed, then quickly thereafter the files are opened.
269                 NSArray *fileArgs = [NSArray arrayWithObject:@"-p"];
270                 fileArgs = [fileArgs arrayByAddingObjectsFromArray:filenames];
272                 pid = [self launchVimProcessWithArguments:fileArgs];
274                 // Make sure these files aren't opened again when
275                 // connectBackend:pid: is called.
276                 [arguments setObject:[NSNumber numberWithBool:NO]
277                               forKey:@"openFiles"];
278             }
280             // TODO: If the Vim process fails to start, or if it changes PID,
281             // then the memory allocated for these parameters will leak.
282             // Ensure that this cannot happen or somehow detect it.
284             if ([arguments count] > 0)
285                 [pidArguments setObject:arguments
286                                  forKey:[NSNumber numberWithInt:pid]];
287         }
288     }
290     [NSApp replyToOpenOrPrint:NSApplicationDelegateReplySuccess];
291     // NSApplicationDelegateReplySuccess = 0,
292     // NSApplicationDelegateReplyCancel = 1,
293     // NSApplicationDelegateReplyFailure = 2
296 - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender
298     return [[NSUserDefaults standardUserDefaults]
299             boolForKey:MMTerminateAfterLastWindowClosedKey];
302 - (NSApplicationTerminateReply)applicationShouldTerminate:
303     (NSApplication *)sender
305     // TODO: Follow Apple's guidelines for 'Graceful Application Termination'
306     // (in particular, allow user to review changes and save).
307     int reply = NSTerminateNow;
308     BOOL modifiedBuffers = NO;
310     // Go through windows, checking for modified buffers.  (Each Vim process
311     // tells MacVim when any buffer has been modified and MacVim sets the
312     // 'documentEdited' flag of the window correspondingly.)
313     NSEnumerator *e = [[NSApp windows] objectEnumerator];
314     id window;
315     while ((window = [e nextObject])) {
316         if ([window isDocumentEdited]) {
317             modifiedBuffers = YES;
318             break;
319         }
320     }
322     if (modifiedBuffers) {
323         NSAlert *alert = [[NSAlert alloc] init];
324         [alert addButtonWithTitle:@"Quit"];
325         [alert addButtonWithTitle:@"Cancel"];
326         [alert setMessageText:@"Quit without saving?"];
327         [alert setInformativeText:@"There are modified buffers, "
328             "if you quit now all changes will be lost.  Quit anyway?"];
329         [alert setAlertStyle:NSWarningAlertStyle];
331         if ([alert runModal] != NSAlertFirstButtonReturn)
332             reply = NSTerminateCancel;
334         [alert release];
335     }
337     // Tell all Vim processes to terminate now (otherwise they'll leave swap
338     // files behind).
339     if (NSTerminateNow == reply) {
340         e = [vimControllers objectEnumerator];
341         id vc;
342         while ((vc = [e nextObject]))
343             [vc sendMessage:TerminateNowMsgID data:nil];
344     }
346     return reply;
349 - (void)applicationWillTerminate:(NSNotification *)notification
351 #if MM_HANDLE_XCODE_MOD_EVENT
352     [[NSAppleEventManager sharedAppleEventManager]
353             removeEventHandlerForEventClass:'KAHL'
354                                  andEventID:'MOD '];
355 #endif
357     // This will invalidate all connections (since they were spawned from the
358     // default connection).
359     [[NSConnection defaultConnection] invalidate];
361     // Send a SIGINT to all running Vim processes, so that they are sure to
362     // receive the connectionDidDie: notification (a process has to be checking
363     // the run-loop for this to happen).
364     unsigned i, count = [vimControllers count];
365     for (i = 0; i < count; ++i) {
366         MMVimController *controller = [vimControllers objectAtIndex:i];
367         int pid = [controller pid];
368         if (pid > 0)
369             kill(pid, SIGINT);
370     }
372     if (fontContainerRef) {
373         ATSFontDeactivate(fontContainerRef, NULL, kATSOptionFlagsDefault);
374         fontContainerRef = 0;
375     }
377     // TODO: Is this a correct way of releasing the MMAppController?
378     // (It doesn't seem like dealloc is ever called.)
379     [NSApp setDelegate:nil];
380     [self autorelease];
383 - (void)removeVimController:(id)controller
385     //NSLog(@"%s%@", _cmd, controller);
387     [[controller windowController] close];
389     [vimControllers removeObject:controller];
391     if (![vimControllers count]) {
392         // Turn on autoenabling of menus (because no Vim is open to handle it),
393         // but do not touch the MacVim menu.  Note that the menus must be
394         // enabled first otherwise autoenabling does not work.
395         NSMenu *mainMenu = [NSApp mainMenu];
396         int i, count = [mainMenu numberOfItems];
397         for (i = 1; i < count; ++i) {
398             NSMenuItem *item = [mainMenu itemAtIndex:i];
399             [item setEnabled:YES];
400             [[item submenu] recurseSetAutoenablesItems:YES];
401         }
402     }
405 - (void)windowControllerWillOpen:(MMWindowController *)windowController
407     NSPoint topLeft = NSZeroPoint;
408     NSWindow *keyWin = [NSApp keyWindow];
409     NSWindow *win = [windowController window];
411     if (!win) return;
413     // If there is a key window, cascade from it, otherwise use the autosaved
414     // window position (if any).
415     if (keyWin) {
416         NSRect frame = [keyWin frame];
417         topLeft = NSMakePoint(frame.origin.x, NSMaxY(frame));
418     } else {
419         NSString *topLeftString = [[NSUserDefaults standardUserDefaults]
420             stringForKey:MMTopLeftPointKey];
421         if (topLeftString)
422             topLeft = NSPointFromString(topLeftString);
423     }
425     if (!NSEqualPoints(topLeft, NSZeroPoint)) {
426         if (keyWin)
427             topLeft = [win cascadeTopLeftFromPoint:topLeft];
429         [win setFrameTopLeftPoint:topLeft];
430     }
432     if (openSelectionString) {
433         // TODO: Pass this as a parameter instead!  Get rid of
434         // 'openSelectionString' etc.
435         //
436         // There is some text to paste into this window as a result of the
437         // services menu "Open selection ..." being used.
438         [[windowController vimController] dropString:openSelectionString];
439         [openSelectionString release];
440         openSelectionString = nil;
441     }
444 - (IBAction)newWindow:(id)sender
446     [self launchVimProcessWithArguments:nil];
449 - (IBAction)fileOpen:(id)sender
451     NSOpenPanel *panel = [NSOpenPanel openPanel];
452     [panel setAllowsMultipleSelection:YES];
454     int result = [panel runModalForTypes:nil];
455     if (NSOKButton == result)
456         [self application:NSApp openFiles:[panel filenames]];
459 - (IBAction)selectNextWindow:(id)sender
461     unsigned i, count = [vimControllers count];
462     if (!count) return;
464     NSWindow *keyWindow = [NSApp keyWindow];
465     for (i = 0; i < count; ++i) {
466         MMVimController *vc = [vimControllers objectAtIndex:i];
467         if ([[[vc windowController] window] isEqual:keyWindow])
468             break;
469     }
471     if (i < count) {
472         if (++i >= count)
473             i = 0;
474         MMVimController *vc = [vimControllers objectAtIndex:i];
475         [[vc windowController] showWindow:self];
476     }
479 - (IBAction)selectPreviousWindow:(id)sender
481     unsigned i, count = [vimControllers count];
482     if (!count) return;
484     NSWindow *keyWindow = [NSApp keyWindow];
485     for (i = 0; i < count; ++i) {
486         MMVimController *vc = [vimControllers objectAtIndex:i];
487         if ([[[vc windowController] window] isEqual:keyWindow])
488             break;
489     }
491     if (i < count) {
492         if (i > 0) {
493             --i;
494         } else {
495             i = count - 1;
496         }
497         MMVimController *vc = [vimControllers objectAtIndex:i];
498         [[vc windowController] showWindow:self];
499     }
502 - (IBAction)fontSizeUp:(id)sender
504     [[NSFontManager sharedFontManager] modifyFont:
505             [NSNumber numberWithInt:NSSizeUpFontAction]];
508 - (IBAction)fontSizeDown:(id)sender
510     [[NSFontManager sharedFontManager] modifyFont:
511             [NSNumber numberWithInt:NSSizeDownFontAction]];
514 - (byref id <MMFrontendProtocol>)
515     connectBackend:(byref in id <MMBackendProtocol>)backend
516                pid:(int)pid
518     //NSLog(@"Connect backend (pid=%d)", pid);
519     NSNumber *pidKey = [NSNumber numberWithInt:pid];
520     MMVimController *vc = nil;
522     @try {
523         [(NSDistantObject*)backend
524                 setProtocolForProxy:@protocol(MMBackendProtocol)];
526         vc = [[[MMVimController alloc]
527                 initWithBackend:backend pid:pid] autorelease];
529         if (![vimControllers count]) {
530             // The first window autosaves its position.  (The autosaving
531             // features of Cocoa are not used because we need more control over
532             // what is autosaved and when it is restored.)
533             [[vc windowController] setWindowAutosaveKey:MMTopLeftPointKey];
534         }
536         [vimControllers addObject:vc];
538         id args = [pidArguments objectForKey:pidKey];
539         if (args && [NSNull null] != args)
540             [self passArguments:args toVimController:vc];
542         // HACK!  MacVim does not get activated if it is launched from the
543         // terminal, so we forcibly activate here unless it is an untitled
544         // window opening.  Untitled windows are treated differently, else
545         // MacVim would steal the focus if another app was activated while the
546         // untitled window was loading.
547         if (!args || args != [NSNull null])
548             [NSApp activateIgnoringOtherApps:YES];
550         if (args)
551             [pidArguments removeObjectForKey:pidKey];
553         return vc;
554     }
556     @catch (NSException *e) {
557         NSLog(@"Exception caught in %s: \"%@\"", _cmd, e);
559         if (vc)
560             [vimControllers removeObject:vc];
562         [pidArguments removeObjectForKey:pidKey];
563     }
565     return nil;
568 - (NSArray *)serverList
570     NSMutableArray *array = [NSMutableArray array];
572     unsigned i, count = [vimControllers count];
573     for (i = 0; i < count; ++i) {
574         MMVimController *controller = [vimControllers objectAtIndex:i];
575         if ([controller serverName])
576             [array addObject:[controller serverName]];
577     }
579     return array;
582 @end // MMAppController
587 @implementation MMAppController (MMServices)
589 - (void)openSelection:(NSPasteboard *)pboard userData:(NSString *)userData
590                 error:(NSString **)error
592     if (![[pboard types] containsObject:NSStringPboardType]) {
593         NSLog(@"WARNING: Pasteboard contains no object of type "
594                 "NSStringPboardType");
595         return;
596     }
598     MMVimController *vc = [self topmostVimController];
599     if (vc) {
600         // Open a new tab first, since dropString: does not do this.
601         [vc sendMessage:AddNewTabMsgID data:nil];
602         [vc dropString:[pboard stringForType:NSStringPboardType]];
603     } else {
604         // NOTE: There is no window to paste the selection into, so save the
605         // text, open a new window, and paste the text when the next window
606         // opens.  (If this is called several times in a row, then all but the
607         // last call might be ignored.)
608         if (openSelectionString) [openSelectionString release];
609         openSelectionString = [[pboard stringForType:NSStringPboardType] copy];
611         [self newWindow:self];
612     }
615 - (void)openFile:(NSPasteboard *)pboard userData:(NSString *)userData
616            error:(NSString **)error
618     if (![[pboard types] containsObject:NSStringPboardType]) {
619         NSLog(@"WARNING: Pasteboard contains no object of type "
620                 "NSStringPboardType");
621         return;
622     }
624     // TODO: Parse multiple filenames and create array with names.
625     NSString *string = [pboard stringForType:NSStringPboardType];
626     string = [string stringByTrimmingCharactersInSet:
627             [NSCharacterSet whitespaceAndNewlineCharacterSet]];
628     string = [string stringByStandardizingPath];
630     NSArray *filenames = [self filterFilesAndNotify:
631             [NSArray arrayWithObject:string]];
632     if ([filenames count] > 0) {
633         MMVimController *vc = nil;
634         if (userData && [userData isEqual:@"Tab"])
635             vc = [self topmostVimController];
637         if (vc) {
638             [vc dropFiles:filenames forceOpen:YES];
639         } else {
640             [self application:NSApp openFiles:filenames];
641         }
642     }
645 @end // MMAppController (MMServices)
650 @implementation MMAppController (Private)
652 - (MMVimController *)keyVimController
654     NSWindow *keyWindow = [NSApp keyWindow];
655     if (keyWindow) {
656         unsigned i, count = [vimControllers count];
657         for (i = 0; i < count; ++i) {
658             MMVimController *vc = [vimControllers objectAtIndex:i];
659             if ([[[vc windowController] window] isEqual:keyWindow])
660                 return vc;
661         }
662     }
664     return nil;
667 - (MMVimController *)topmostVimController
669     NSArray *windows = [NSApp orderedWindows];
670     if ([windows count] > 0) {
671         NSWindow *window = [windows objectAtIndex:0];
672         unsigned i, count = [vimControllers count];
673         for (i = 0; i < count; ++i) {
674             MMVimController *vc = [vimControllers objectAtIndex:i];
675             if ([[[vc windowController] window] isEqual:window])
676                 return vc;
677         }
678     }
680     return nil;
683 - (int)launchVimProcessWithArguments:(NSArray *)args
685     NSString *taskPath = nil;
686     NSArray *taskArgs = nil;
687     NSString *path = [[NSBundle mainBundle] pathForAuxiliaryExecutable:@"Vim"];
689     if (!path) {
690         NSLog(@"ERROR: Vim executable could not be found inside app bundle!");
691         return 0;
692     }
694     if ([[NSUserDefaults standardUserDefaults] boolForKey:MMLoginShellKey]) {
695         // Run process with a login shell
696         //   $SHELL -l -c "exec Vim -g -f args"
697         // (-g for GUI, -f for foreground, i.e. don't fork)
699         NSMutableString *execArg = [NSMutableString
700             stringWithFormat:@"exec \"%@\" -g -f", path];
701         if (args) {
702             // Append all arguments while making sure that arguments containing
703             // spaces are enclosed in quotes.
704             NSCharacterSet *space = [NSCharacterSet whitespaceCharacterSet];
705             unsigned i, count = [args count];
707             for (i = 0; i < count; ++i) {
708                 NSString *arg = [args objectAtIndex:i];
709                 if (NSNotFound != [arg rangeOfCharacterFromSet:space].location)
710                     [execArg appendFormat:@" \"%@\"", arg];
711                 else
712                     [execArg appendFormat:@" %@", arg];
713             }
714         }
716         // Launch the process with a login shell so that users environment
717         // settings get sourced.  This does not always happen when MacVim is
718         // started.
719         taskArgs = [NSArray arrayWithObjects:@"-l", @"-c", execArg, nil];
720         taskPath = [[[NSProcessInfo processInfo] environment]
721             objectForKey:@"SHELL"];
722         if (!taskPath)
723             taskPath = @"/bin/sh";
724     } else {
725         // Run process directly:
726         //   Vim -g -f args
727         // (-g for GUI, -f for foreground, i.e. don't fork)
728         taskPath = path;
729         taskArgs = [NSArray arrayWithObjects:@"-g", @"-f", nil];
730         if (args)
731             taskArgs = [taskArgs arrayByAddingObjectsFromArray:args];
732     }
734     NSTask *task =[NSTask launchedTaskWithLaunchPath:taskPath
735                                            arguments:taskArgs];
736     //NSLog(@"launch %@ with args=%@ (pid=%d)", taskPath, taskArgs,
737     //        [task processIdentifier]);
739     int pid = [task processIdentifier];
741     // If the process has no arguments, then add a null argument to the
742     // pidArguments dictionary.  This is later used to detect that a process
743     // without arguments is being launched.
744     if (!args)
745         [pidArguments setObject:[NSNull null]
746                          forKey:[NSNumber numberWithInt:pid]];
748     return pid;
751 - (NSArray *)filterFilesAndNotify:(NSArray *)filenames
753     // Go trough 'filenames' array and make sure each file exists.  Present
754     // warning dialog if some file was missing.
756     NSString *firstMissingFile = nil;
757     NSMutableArray *files = [NSMutableArray array];
758     unsigned i, count = [filenames count];
760     for (i = 0; i < count; ++i) {
761         NSString *name = [filenames objectAtIndex:i];
762         if ([[NSFileManager defaultManager] fileExistsAtPath:name]) {
763             [files addObject:name];
764         } else if (!firstMissingFile) {
765             firstMissingFile = name;
766         }
767     }
769     if (firstMissingFile) {
770         NSAlert *alert = [[NSAlert alloc] init];
771         [alert addButtonWithTitle:@"OK"];
773         NSString *text;
774         if ([files count] >= count-1) {
775             [alert setMessageText:@"File not found"];
776             text = [NSString stringWithFormat:@"Could not open file with "
777                 "name %@.", firstMissingFile];
778         } else {
779             [alert setMessageText:@"Multiple files not found"];
780             text = [NSString stringWithFormat:@"Could not open file with "
781                 "name %@, and %d other files.", firstMissingFile,
782                 count-[files count]-1];
783         }
785         [alert setInformativeText:text];
786         [alert setAlertStyle:NSWarningAlertStyle];
788         [alert runModal];
789         [alert release];
791         [NSApp replyToOpenOrPrint:NSApplicationDelegateReplyFailure];
792     }
794     return files;
797 - (NSArray *)filterOpenFiles:(NSArray *)filenames
798                    arguments:(NSDictionary *)args
800     // Check if any of the files in the 'filenames' array are open in any Vim
801     // process.  Remove the files that are open from the 'filenames' array and
802     // return it.  If all files were filtered out, then raise the first file in
803     // the Vim process it is open.  Files that are filtered are sent an odb
804     // open event in case theID is not zero.
806     NSMutableDictionary *localArgs =
807             [NSMutableDictionary dictionaryWithDictionary:args];
808     MMVimController *raiseController = nil;
809     NSString *raiseFile = nil;
810     NSMutableArray *files = [filenames mutableCopy];
811     NSString *expr = [NSString stringWithFormat:
812             @"map([\"%@\"],\"bufloaded(v:val)\")",
813             [files componentsJoinedByString:@"\",\""]];
814     unsigned i, count = [vimControllers count];
816     // Ensure that the files aren't opened when passing arguments.
817     [localArgs setObject:[NSNumber numberWithBool:NO] forKey:@"openFiles"];
819     for (i = 0; i < count && [files count]; ++i) {
820         MMVimController *controller = [vimControllers objectAtIndex:i];
821         id proxy = [controller backendProxy];
823         @try {
824             // Query Vim for which files in the 'files' array are open.
825             NSString *eval = [proxy evaluateExpression:expr];
826             NSIndexSet *idxSet = [NSIndexSet indexSetWithVimList:eval];
827             if ([idxSet count]) {
828                 if (!raiseFile) {
829                     // Remember the file and which Vim that has it open so that
830                     // we can raise it later on.
831                     raiseController = controller;
832                     raiseFile = [files objectAtIndex:[idxSet firstIndex]];
833                     [[raiseFile retain] autorelease];
834                 }
836                 // Pass (ODB/Xcode/Spotlight) arguments to this process.
837                 [localArgs setObject:[files objectsAtIndexes:idxSet]
838                               forKey:@"filenames"];
839                 [self passArguments:localArgs toVimController:controller];
841                 // Remove all the files that were open in this Vim process and
842                 // create a new expression to evaluate.
843                 [files removeObjectsAtIndexes:idxSet];
844                 expr = [NSString stringWithFormat:
845                         @"map([\"%@\"],\"bufloaded(v:val)\")",
846                         [files componentsJoinedByString:@"\",\""]];
847             }
848         }
849         @catch (NSException *e) {
850             // Do nothing ...
851         }
852     }
854     if (![files count] && raiseFile) {
855         // Raise the window containing the first file that was already open,
856         // and make sure that the tab containing that file is selected.  Only
857         // do this if there are no more files to open, otherwise sometimes the
858         // window with 'raiseFile' will be raised, other times it might be the
859         // window that will open with the files in the 'files' array.
860         raiseFile = [raiseFile stringByEscapingSpecialFilenameCharacters];
861         NSString *input = [NSString stringWithFormat:@"<C-\\><C-N>"
862             ":let oldswb=&swb|let &swb=\"useopen,usetab\"|"
863             "tab sb %@|let &swb=oldswb|unl oldswb|"
864             "cal foreground()|redr|f<CR>", raiseFile];
866         [raiseController addVimInput:input];
867     }
869     return files;
872 #if MM_HANDLE_XCODE_MOD_EVENT
873 - (void)handleXcodeModEvent:(NSAppleEventDescriptor *)event
874                  replyEvent:(NSAppleEventDescriptor *)reply
876 #if 0
877     // Xcode sends this event to query MacVim which open files have been
878     // modified.
879     NSLog(@"reply:%@", reply);
880     NSLog(@"event:%@", event);
882     NSEnumerator *e = [vimControllers objectEnumerator];
883     id vc;
884     while ((vc = [e nextObject])) {
885         DescType type = [reply descriptorType];
886         unsigned len = [[type data] length];
887         NSMutableData *data = [NSMutableData data];
889         [data appendBytes:&type length:sizeof(DescType)];
890         [data appendBytes:&len length:sizeof(unsigned)];
891         [data appendBytes:[reply data] length:len];
893         [vc sendMessage:XcodeModMsgID data:data];
894     }
895 #endif
897 #endif
899 - (int)findLaunchingProcessWithoutArguments
901     NSArray *keys = [pidArguments allKeysForObject:[NSNull null]];
902     if ([keys count] > 0) {
903         //NSLog(@"found launching process without arguments");
904         return [[keys objectAtIndex:0] intValue];
905     }
907     return 0;
910 - (MMVimController *)findUntitledWindow
912     NSEnumerator *e = [vimControllers objectEnumerator];
913     id vc;
914     while ((vc = [e nextObject])) {
915         // TODO: This is a moronic test...should query the Vim process if there
916         // are any open buffers or something like that instead.
917         NSString *title = [[[vc windowController] window] title];
918         if ([title hasPrefix:@"[No Name]"]) {
919             //NSLog(@"found untitled window");
920             return vc;
921         }
922     }
924     return nil;
927 - (NSMutableDictionary *)extractArgumentsFromOdocEvent:
928     (NSAppleEventDescriptor *)desc
930     NSMutableDictionary *dict = [NSMutableDictionary dictionary];
932     // 1. Extract ODB parameters (if any)
933     NSAppleEventDescriptor *odbdesc = desc;
934     if (![odbdesc paramDescriptorForKeyword:keyFileSender]) {
935         // The ODB paramaters may hide inside the 'keyAEPropData' descriptor.
936         odbdesc = [odbdesc paramDescriptorForKeyword:keyAEPropData];
937         if (![odbdesc paramDescriptorForKeyword:keyFileSender])
938             odbdesc = nil;
939     }
941     if (odbdesc) {
942         NSAppleEventDescriptor *p =
943                 [odbdesc paramDescriptorForKeyword:keyFileSender];
944         if (p)
945             [dict setObject:[NSNumber numberWithUnsignedInt:[p typeCodeValue]]
946                      forKey:@"remoteID"];
948         p = [odbdesc paramDescriptorForKeyword:keyFileCustomPath];
949         if (p)
950             [dict setObject:[p stringValue] forKey:@"remotePath"];
952         p = [odbdesc paramDescriptorForKeyword:keyFileSenderToken];
953         if (p)
954             [dict setObject:p forKey:@"remotePath"];
955     }
957     // 2. Extract Xcode parameters (if any)
958     NSAppleEventDescriptor *xcodedesc =
959             [desc paramDescriptorForKeyword:keyAEPosition];
960     if (xcodedesc) {
961         NSRange range;
962         MMSelectionRange *sr = (MMSelectionRange*)[[xcodedesc data] bytes];
964         if (sr->lineNum < 0) {
965             // Should select a range of lines.
966             range.location = sr->startRange + 1;
967             range.length = sr->endRange - sr->startRange + 1;
968         } else {
969             // Should only move cursor to a line.
970             range.location = sr->lineNum + 1;
971             range.length = 0;
972         }
974         [dict setObject:NSStringFromRange(range) forKey:@"selectionRange"];
975     }
977     // 3. Extract Spotlight search text (if any)
978     NSAppleEventDescriptor *spotlightdesc = 
979             [desc paramDescriptorForKeyword:keyAESearchText];
980     if (spotlightdesc)
981         [dict setObject:[spotlightdesc stringValue] forKey:@"searchText"];
983     return dict;
986 - (void)passArguments:(NSDictionary *)args toVimController:(MMVimController*)vc
988     if (!args) return;
990     // Pass filenames to open if required (the 'openFiles' argument can be used
991     // to disallow opening of the files).
992     NSArray *filenames = [args objectForKey:@"filenames"];
993     if (filenames && [[args objectForKey:@"openFiles"] boolValue]) {
994         NSString *tabDrop = buildTabDropCommand(filenames);
995         [vc addVimInput:tabDrop];
996     }
998     // Pass ODB data
999     if (filenames && [args objectForKey:@"remoteID"]) {
1000         [vc odbEdit:filenames
1001              server:[[args objectForKey:@"remoteID"] unsignedIntValue]
1002                path:[args objectForKey:@"remotePath"]
1003               token:[args objectForKey:@"remoteToken"]];
1004     }
1006     // Pass range of lines to select
1007     if ([args objectForKey:@"selectionRange"]) {
1008         NSRange selectionRange = NSRangeFromString(
1009                 [args objectForKey:@"selectionRange"]);
1010         [vc addVimInput:buildSelectRangeCommand(selectionRange)];
1011     }
1013     // Pass search text
1014     NSString *searchText = [args objectForKey:@"searchText"];
1015     if (searchText)
1016         [vc addVimInput:buildSearchTextCommand(searchText)];
1019 @end // MMAppController (Private)
1024 @implementation NSMenu (MMExtras)
1026 - (void)recurseSetAutoenablesItems:(BOOL)on
1028     [self setAutoenablesItems:on];
1030     int i, count = [self numberOfItems];
1031     for (i = 0; i < count; ++i) {
1032         NSMenuItem *item = [self itemAtIndex:i];
1033         [item setEnabled:YES];
1034         NSMenu *submenu = [item submenu];
1035         if (submenu) {
1036             [submenu recurseSetAutoenablesItems:on];
1037         }
1038     }
1041 @end  // NSMenu (MMExtras)
1046 @implementation NSNumber (MMExtras)
1047 - (int)tag
1049     return [self intValue];
1051 @end // NSNumber (MMExtras)