File->Open pops up a modal 'open files' dialog
[MacVim.git] / src / MacVim / MMAppController.m
blobc52228e2bc8b3daa1cd8abeafeae8481168cb123
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"
36 // Default timeout intervals on all connections.
37 static NSTimeInterval MMRequestTimeout = 5;
38 static NSTimeInterval MMReplyTimeout = 5;
41 #pragma options align=mac68k
42 typedef struct
44     short unused1;      // 0 (not used)
45     short lineNum;      // line to select (< 0 to specify range)
46     long  startRange;   // start of selection range (if line < 0)
47     long  endRange;     // end of selection range (if line < 0)
48     long  unused2;      // 0 (not used)
49     long  theDate;      // modification date/time
50 } MMSelectionRange;
51 #pragma options align=reset
54 @interface MMAppController (MMServices)
55 - (void)openSelection:(NSPasteboard *)pboard userData:(NSString *)userData
56                 error:(NSString **)error;
57 - (void)openFile:(NSPasteboard *)pboard userData:(NSString *)userData
58            error:(NSString **)error;
59 @end
62 @interface MMAppController (Private)
63 - (MMVimController *)keyVimController;
64 - (MMVimController *)topmostVimController;
65 - (int)launchVimProcessWithArguments:(NSArray *)args;
66 - (NSArray *)filterFilesAndNotify:(NSArray *)files;
67 - (NSArray *)filterOpenFiles:(NSArray *)filenames remote:(OSType)theID
68                         path:(NSString *)path
69                        token:(NSAppleEventDescriptor *)token
70               selectionRange:(MMSelectionRange *)selRange;
71 - (void)handleXcodeModEvent:(NSAppleEventDescriptor *)event
72                  replyEvent:(NSAppleEventDescriptor *)reply;
73 - (NSString *)inputStringFromSelectionRange:(MMSelectionRange *)selRange;
74 @end
76 @interface NSMenu (MMExtras)
77 - (void)recurseSetAutoenablesItems:(BOOL)on;
78 @end
80 @interface NSNumber (MMExtras)
81 - (int)tag;
82 @end
86 @implementation MMAppController
88 + (void)initialize
90     NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:
91         [NSNumber numberWithBool:NO],   MMNoWindowKey,
92         [NSNumber numberWithInt:64],    MMTabMinWidthKey,
93         [NSNumber numberWithInt:6*64],  MMTabMaxWidthKey,
94         [NSNumber numberWithInt:132],   MMTabOptimumWidthKey,
95         [NSNumber numberWithInt:2],     MMTextInsetLeftKey,
96         [NSNumber numberWithInt:1],     MMTextInsetRightKey,
97         [NSNumber numberWithInt:1],     MMTextInsetTopKey,
98         [NSNumber numberWithInt:1],     MMTextInsetBottomKey,
99         [NSNumber numberWithBool:NO],   MMTerminateAfterLastWindowClosedKey,
100         @"MMTypesetter",                MMTypesetterKey,
101         [NSNumber numberWithFloat:1],   MMCellWidthMultiplierKey,
102         [NSNumber numberWithFloat:-1],  MMBaselineOffsetKey,
103         [NSNumber numberWithBool:YES],  MMTranslateCtrlClickKey,
104         [NSNumber numberWithBool:NO],   MMOpenFilesInTabsKey,
105         [NSNumber numberWithBool:NO],   MMNoFontSubstitutionKey,
106         [NSNumber numberWithBool:NO],   MMLoginShellKey,
107         nil];
109     [[NSUserDefaults standardUserDefaults] registerDefaults:dict];
111     NSArray *types = [NSArray arrayWithObject:NSStringPboardType];
112     [NSApp registerServicesMenuSendTypes:types returnTypes:types];
115 - (id)init
117     if ((self = [super init])) {
118         fontContainerRef = loadFonts();
120         vimControllers = [NSMutableArray new];
121         pidArguments = [NSMutableDictionary new];
123         // NOTE!  If the name of the connection changes here it must also be
124         // updated in MMBackend.m.
125         NSConnection *connection = [NSConnection defaultConnection];
126         NSString *name = [NSString stringWithFormat:@"%@-connection",
127                  [[NSBundle mainBundle] bundleIdentifier]];
128         //NSLog(@"Registering connection with name '%@'", name);
129         if ([connection registerName:name]) {
130             [connection setRequestTimeout:MMRequestTimeout];
131             [connection setReplyTimeout:MMReplyTimeout];
132             [connection setRootObject:self];
134             // NOTE: When the user is resizing the window the AppKit puts the
135             // run loop in event tracking mode.  Unless the connection listens
136             // to request in this mode, live resizing won't work.
137             [connection addRequestMode:NSEventTrackingRunLoopMode];
138         } else {
139             NSLog(@"WARNING: Failed to register connection with name '%@'",
140                     name);
141         }
142     }
144     return self;
147 - (void)dealloc
149     //NSLog(@"MMAppController dealloc");
151     [pidArguments release];
152     [vimControllers release];
153     [openSelectionString release];
155     [super dealloc];
158 - (void)applicationWillFinishLaunching:(NSNotification *)notification
160     [[NSAppleEventManager sharedAppleEventManager]
161             setEventHandler:self
162                 andSelector:@selector(handleXcodeModEvent:replyEvent:)
163               forEventClass:'KAHL'
164                  andEventID:'MOD '];
167 - (void)applicationDidFinishLaunching:(NSNotification *)notification
169     [NSApp setServicesProvider:self];
172 - (BOOL)applicationShouldOpenUntitledFile:(NSApplication *)sender
174     // NOTE!  This way it possible to start the app with the command-line
175     // argument '-nowindow yes' and no window will be opened by default.
176     untitledWindowOpening =
177         ![[NSUserDefaults standardUserDefaults] boolForKey:MMNoWindowKey];
178     return untitledWindowOpening;
181 - (BOOL)applicationOpenUntitledFile:(NSApplication *)sender
183     [self newWindow:self];
184     return YES;
187 - (void)application:(NSApplication *)sender openFiles:(NSArray *)filenames
189     OSType remoteID = 0;
190     NSString *remotePath = nil;
191     NSAppleEventManager *aem;
192     NSAppleEventDescriptor *remoteToken = nil;
193     NSAppleEventDescriptor *odbdesc = nil;
194     NSAppleEventDescriptor *xcodedesc = nil;
195     MMSelectionRange *selRange = NULL;
197     aem = [NSAppleEventManager sharedAppleEventManager];
198     odbdesc = [aem currentAppleEvent];
199     if (![odbdesc paramDescriptorForKeyword:keyFileSender]) {
200         // The ODB paramaters may hide inside the 'keyAEPropData' descriptor.
201         odbdesc = [odbdesc paramDescriptorForKeyword:keyAEPropData];
202         if (![odbdesc paramDescriptorForKeyword:keyFileSender])
203             odbdesc = nil;
204     }
206     if (odbdesc) {
207         remoteID = [[odbdesc paramDescriptorForKeyword:keyFileSender]
208                 typeCodeValue];
209         remotePath = [[odbdesc paramDescriptorForKeyword:keyFileCustomPath]
210                 stringValue];
211         remoteToken = [[odbdesc paramDescriptorForKeyword:keyFileSenderToken]
212                 copy];
213     }
215     xcodedesc = [[aem currentAppleEvent]
216             paramDescriptorForKeyword:keyAEPosition];
217     if (xcodedesc)
218         selRange = (MMSelectionRange*)[[xcodedesc data] bytes];
220     filenames = [self filterOpenFiles:filenames remote:remoteID path:remotePath
221                                 token:remoteToken selectionRange:selRange];
222     if ([filenames count]) {
223         MMVimController *vc;
224         BOOL openInTabs = [[NSUserDefaults standardUserDefaults]
225             boolForKey:MMOpenFilesInTabsKey];
227         if (openInTabs && (vc = [self topmostVimController])) {
228             // Open files in tabs in the topmost window.
229             [vc dropFiles:filenames forceOpen:YES];
230             if (odbdesc)
231                 [vc odbEdit:filenames server:remoteID path:remotePath
232                       token:remoteToken];
233             if (selRange)
234                 [vc addVimInput:[self inputStringFromSelectionRange:selRange]];
235         } else {
236             // Open files in tabs in a new window.
237             NSMutableArray *args = [NSMutableArray arrayWithObject:@"-p"];
238             [args addObjectsFromArray:filenames];
239             int pid = [self launchVimProcessWithArguments:args];
241             // The Vim process starts asynchronously.  Some arguments cannot be
242             // on the command line, so store them in a dictionary and pass them
243             // to the process once it has started.
244             //
245             // TODO: If the Vim process fails to start, or if it changes PID,
246             // then the memory allocated for these parameters will leak.
247             // Ensure that this cannot happen or somehow detect it.
248             NSMutableDictionary *argDict = nil;
249             if (odbdesc) {
250                 // The remote token can be arbitrary data so it is cannot
251                 // (without encoding it as text) be passed on the command line.
252                 argDict = [NSMutableDictionary dictionaryWithObjectsAndKeys:
253                         filenames, @"filenames",
254                         [NSNumber numberWithUnsignedInt:remoteID], @"remoteID",
255                         nil];
256                 if (remotePath)
257                     [argDict setObject:remotePath forKey:@"remotePath"];
258                 if (remoteToken)
259                     [argDict setObject:remoteToken forKey:@"remoteToken"];
260             }
262             if (selRange) {
263                 if (!argDict)
264                     argDict = [NSMutableDictionary
265                         dictionaryWithObject:[xcodedesc data]
266                                       forKey:@"selectionRangeData"];
267                 else
268                     [argDict setObject:[xcodedesc data]
269                                 forKey:@"selectionRangeData"];
270             }
272             if (argDict)
273                 [pidArguments setObject:argDict
274                                  forKey:[NSNumber numberWithInt:pid]];
275         }
276     }
278     [NSApp replyToOpenOrPrint:NSApplicationDelegateReplySuccess];
279     // NSApplicationDelegateReplySuccess = 0,
280     // NSApplicationDelegateReplyCancel = 1,
281     // NSApplicationDelegateReplyFailure = 2
284 - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender
286     return [[NSUserDefaults standardUserDefaults]
287             boolForKey:MMTerminateAfterLastWindowClosedKey];
290 - (NSApplicationTerminateReply)applicationShouldTerminate:
291     (NSApplication *)sender
293     // TODO: Follow Apple's guidelines for 'Graceful Application Termination'
294     // (in particular, allow user to review changes and save).
295     int reply = NSTerminateNow;
296     BOOL modifiedBuffers = NO;
298     // Go through windows, checking for modified buffers.  (Each Vim process
299     // tells MacVim when any buffer has been modified and MacVim sets the
300     // 'documentEdited' flag of the window correspondingly.)
301     NSEnumerator *e = [[NSApp windows] objectEnumerator];
302     id window;
303     while ((window = [e nextObject])) {
304         if ([window isDocumentEdited]) {
305             modifiedBuffers = YES;
306             break;
307         }
308     }
310     if (modifiedBuffers) {
311         NSAlert *alert = [[NSAlert alloc] init];
312         [alert addButtonWithTitle:@"Quit"];
313         [alert addButtonWithTitle:@"Cancel"];
314         [alert setMessageText:@"Quit without saving?"];
315         [alert setInformativeText:@"There are modified buffers, "
316             "if you quit now all changes will be lost.  Quit anyway?"];
317         [alert setAlertStyle:NSWarningAlertStyle];
319         if ([alert runModal] != NSAlertFirstButtonReturn)
320             reply = NSTerminateCancel;
322         [alert release];
323     }
325     // Tell all Vim processes to terminate now (otherwise they'll leave swap
326     // files behind).
327     if (NSTerminateNow == reply) {
328         e = [vimControllers objectEnumerator];
329         id vc;
330         while ((vc = [e nextObject]))
331             [vc sendMessage:TerminateNowMsgID data:nil];
332     }
334     return reply;
337 - (void)applicationWillTerminate:(NSNotification *)notification
339     [[NSAppleEventManager sharedAppleEventManager]
340             removeEventHandlerForEventClass:'KAHL'
341                                  andEventID:'MOD '];
343     // This will invalidate all connections (since they were spawned from the
344     // default connection).
345     [[NSConnection defaultConnection] invalidate];
347     // Send a SIGINT to all running Vim processes, so that they are sure to
348     // receive the connectionDidDie: notification (a process has to be checking
349     // the run-loop for this to happen).
350     unsigned i, count = [vimControllers count];
351     for (i = 0; i < count; ++i) {
352         MMVimController *controller = [vimControllers objectAtIndex:i];
353         int pid = [controller pid];
354         if (pid > 0)
355             kill(pid, SIGINT);
356     }
358     if (fontContainerRef) {
359         ATSFontDeactivate(fontContainerRef, NULL, kATSOptionFlagsDefault);
360         fontContainerRef = 0;
361     }
363     // TODO: Is this a correct way of releasing the MMAppController?
364     // (It doesn't seem like dealloc is ever called.)
365     [NSApp setDelegate:nil];
366     [self autorelease];
369 - (void)removeVimController:(id)controller
371     //NSLog(@"%s%@", _cmd, controller);
373     [[controller windowController] close];
375     [vimControllers removeObject:controller];
377     if (![vimControllers count]) {
378         // Turn on autoenabling of menus (because no Vim is open to handle it),
379         // but do not touch the MacVim menu.  Note that the menus must be
380         // enabled first otherwise autoenabling does not work.
381         NSMenu *mainMenu = [NSApp mainMenu];
382         int i, count = [mainMenu numberOfItems];
383         for (i = 1; i < count; ++i) {
384             NSMenuItem *item = [mainMenu itemAtIndex:i];
385             [item setEnabled:YES];
386             [[item submenu] recurseSetAutoenablesItems:YES];
387         }
388     }
391 - (void)windowControllerWillOpen:(MMWindowController *)windowController
393     NSPoint topLeft = NSZeroPoint;
394     NSWindow *keyWin = [NSApp keyWindow];
395     NSWindow *win = [windowController window];
397     if (!win) return;
399     // If there is a key window, cascade from it, otherwise use the autosaved
400     // window position (if any).
401     if (keyWin) {
402         NSRect frame = [keyWin frame];
403         topLeft = NSMakePoint(frame.origin.x, NSMaxY(frame));
404     } else {
405         NSString *topLeftString = [[NSUserDefaults standardUserDefaults]
406             stringForKey:MMTopLeftPointKey];
407         if (topLeftString)
408             topLeft = NSPointFromString(topLeftString);
409     }
411     if (!NSEqualPoints(topLeft, NSZeroPoint)) {
412         if (keyWin)
413             topLeft = [win cascadeTopLeftFromPoint:topLeft];
415         [win setFrameTopLeftPoint:topLeft];
416     }
418     if (openSelectionString) {
419         // There is some text to paste into this window as a result of the
420         // services menu "Open selection ..." being used.
421         [[windowController vimController] dropString:openSelectionString];
422         [openSelectionString release];
423         openSelectionString = nil;
424     }
427 - (IBAction)newWindow:(id)sender
429     [self launchVimProcessWithArguments:nil];
432 - (IBAction)fileOpen:(id)sender
434     NSOpenPanel *panel = [NSOpenPanel openPanel];
435     [panel setAllowsMultipleSelection:YES];
437     int result = [panel runModalForTypes:nil];
438     if (NSOKButton == result)
439         [self application:NSApp openFiles:[panel filenames]];
442 - (IBAction)selectNextWindow:(id)sender
444     unsigned i, count = [vimControllers count];
445     if (!count) return;
447     NSWindow *keyWindow = [NSApp keyWindow];
448     for (i = 0; i < count; ++i) {
449         MMVimController *vc = [vimControllers objectAtIndex:i];
450         if ([[[vc windowController] window] isEqual:keyWindow])
451             break;
452     }
454     if (i < count) {
455         if (++i >= count)
456             i = 0;
457         MMVimController *vc = [vimControllers objectAtIndex:i];
458         [[vc windowController] showWindow:self];
459     }
462 - (IBAction)selectPreviousWindow:(id)sender
464     unsigned i, count = [vimControllers count];
465     if (!count) return;
467     NSWindow *keyWindow = [NSApp keyWindow];
468     for (i = 0; i < count; ++i) {
469         MMVimController *vc = [vimControllers objectAtIndex:i];
470         if ([[[vc windowController] window] isEqual:keyWindow])
471             break;
472     }
474     if (i < count) {
475         if (i > 0) {
476             --i;
477         } else {
478             i = count - 1;
479         }
480         MMVimController *vc = [vimControllers objectAtIndex:i];
481         [[vc windowController] showWindow:self];
482     }
485 - (IBAction)fontSizeUp:(id)sender
487     [[NSFontManager sharedFontManager] modifyFont:
488             [NSNumber numberWithInt:NSSizeUpFontAction]];
491 - (IBAction)fontSizeDown:(id)sender
493     [[NSFontManager sharedFontManager] modifyFont:
494             [NSNumber numberWithInt:NSSizeDownFontAction]];
497 - (byref id <MMFrontendProtocol>)
498     connectBackend:(byref in id <MMBackendProtocol>)backend
499                pid:(int)pid
501     MMVimController *vc = nil;
502     //NSLog(@"Connect backend (pid=%d)", pid);
504     @try {
505         [(NSDistantObject*)backend
506                 setProtocolForProxy:@protocol(MMBackendProtocol)];
508         vc = [[[MMVimController alloc]
509                 initWithBackend:backend pid:pid] autorelease];
511         if (![vimControllers count]) {
512             // The first window autosaves its position.  (The autosaving
513             // features of Cocoa are not used because we need more control over
514             // what is autosaved and when it is restored.)
515             [[vc windowController] setWindowAutosaveKey:MMTopLeftPointKey];
516         }
518         [vimControllers addObject:vc];
520         // HACK!  MacVim does not get activated if it is launched from the
521         // terminal, so we forcibly activate here unless it is an untitled
522         // window opening (i.e. MacVim was opened from the Finder).  Untitled
523         // windows are treated differently, else MacVim would steal the focus
524         // if another app was activated while the untitled window was loading.
525         if (!untitledWindowOpening)
526             [NSApp activateIgnoringOtherApps:YES];
528         untitledWindowOpening = NO;
530         // Arguments to a new Vim process that cannot be passed on the command
531         // line are stored in a dictionary and passed to the Vim process here.
532         NSNumber *key = [NSNumber numberWithInt:pid];
533         NSDictionary *args = [pidArguments objectForKey:key];
534         if (args) {
535             if ([args objectForKey:@"remoteID"]) {
536                 [vc odbEdit:[args objectForKey:@"filenames"]
537                      server:[[args objectForKey:@"remoteID"] unsignedIntValue]
538                        path:[args objectForKey:@"remotePath"]
539                       token:[args objectForKey:@"remoteToken"]];
540             }
542             if ([args objectForKey:@"selectionRangeData"]) {
543                 MMSelectionRange *selRange = (MMSelectionRange*)
544                         [[args objectForKey:@"selectionRangeData"] bytes];
545                 [vc addVimInput:[self inputStringFromSelectionRange:selRange]];
546             }
548             [pidArguments removeObjectForKey:key];
549         }
551         return vc;
552     }
554     @catch (NSException *e) {
555         NSLog(@"Exception caught in %s: \"%@\"", _cmd, e);
557         if (vc)
558             [vimControllers removeObject:vc];
559     }
561     return nil;
564 - (NSArray *)serverList
566     NSMutableArray *array = [NSMutableArray array];
568     unsigned i, count = [vimControllers count];
569     for (i = 0; i < count; ++i) {
570         MMVimController *controller = [vimControllers objectAtIndex:i];
571         if ([controller serverName])
572             [array addObject:[controller serverName]];
573     }
575     return array;
578 @end // MMAppController
583 @implementation MMAppController (MMServices)
585 - (void)openSelection:(NSPasteboard *)pboard userData:(NSString *)userData
586                 error:(NSString **)error
588     if (![[pboard types] containsObject:NSStringPboardType]) {
589         NSLog(@"WARNING: Pasteboard contains no object of type "
590                 "NSStringPboardType");
591         return;
592     }
594     MMVimController *vc = [self topmostVimController];
595     if (vc) {
596         // Open a new tab first, since dropString: does not do this.
597         [vc sendMessage:AddNewTabMsgID data:nil];
598         [vc dropString:[pboard stringForType:NSStringPboardType]];
599     } else {
600         // NOTE: There is no window to paste the selection into, so save the
601         // text, open a new window, and paste the text when the next window
602         // opens.  (If this is called several times in a row, then all but the
603         // last call might be ignored.)
604         if (openSelectionString) [openSelectionString release];
605         openSelectionString = [[pboard stringForType:NSStringPboardType] copy];
607         [self newWindow:self];
608     }
611 - (void)openFile:(NSPasteboard *)pboard userData:(NSString *)userData
612            error:(NSString **)error
614     if (![[pboard types] containsObject:NSStringPboardType]) {
615         NSLog(@"WARNING: Pasteboard contains no object of type "
616                 "NSStringPboardType");
617         return;
618     }
620     // TODO: Parse multiple filenames and create array with names.
621     NSString *string = [pboard stringForType:NSStringPboardType];
622     string = [string stringByTrimmingCharactersInSet:
623             [NSCharacterSet whitespaceAndNewlineCharacterSet]];
624     string = [string stringByStandardizingPath];
626     NSArray *filenames = [self filterFilesAndNotify:
627             [NSArray arrayWithObject:string]];
628     if ([filenames count] > 0) {
629         MMVimController *vc = nil;
630         if (userData && [userData isEqual:@"Tab"])
631             vc = [self topmostVimController];
633         if (vc) {
634             [vc dropFiles:filenames forceOpen:YES];
635         } else {
636             [self application:NSApp openFiles:filenames];
637         }
638     }
641 @end // MMAppController (MMServices)
646 @implementation MMAppController (Private)
648 - (MMVimController *)keyVimController
650     NSWindow *keyWindow = [NSApp keyWindow];
651     if (keyWindow) {
652         unsigned i, count = [vimControllers count];
653         for (i = 0; i < count; ++i) {
654             MMVimController *vc = [vimControllers objectAtIndex:i];
655             if ([[[vc windowController] window] isEqual:keyWindow])
656                 return vc;
657         }
658     }
660     return nil;
663 - (MMVimController *)topmostVimController
665     NSArray *windows = [NSApp orderedWindows];
666     if ([windows count] > 0) {
667         NSWindow *window = [windows objectAtIndex:0];
668         unsigned i, count = [vimControllers count];
669         for (i = 0; i < count; ++i) {
670             MMVimController *vc = [vimControllers objectAtIndex:i];
671             if ([[[vc windowController] window] isEqual:window])
672                 return vc;
673         }
674     }
676     return nil;
679 - (int)launchVimProcessWithArguments:(NSArray *)args
681     NSString *taskPath = nil;
682     NSArray *taskArgs = nil;
683     NSString *path = [[NSBundle mainBundle] pathForAuxiliaryExecutable:@"Vim"];
685     if (!path) {
686         NSLog(@"ERROR: Vim executable could not be found inside app bundle!");
687         return 0;
688     }
690     if ([[NSUserDefaults standardUserDefaults] boolForKey:MMLoginShellKey]) {
691         // Run process with a login shell
692         //   $SHELL -l -c "exec Vim -g -f args"
693         // (-g for GUI, -f for foreground, i.e. don't fork)
695         NSMutableString *execArg = [NSMutableString
696             stringWithFormat:@"exec \"%@\" -g -f", path];
697         if (args) {
698             // Append all arguments while making sure that arguments containing
699             // spaces are enclosed in quotes.
700             NSCharacterSet *space = [NSCharacterSet whitespaceCharacterSet];
701             unsigned i, count = [args count];
703             for (i = 0; i < count; ++i) {
704                 NSString *arg = [args objectAtIndex:i];
705                 if (NSNotFound != [arg rangeOfCharacterFromSet:space].location)
706                     [execArg appendFormat:@" \"%@\"", arg];
707                 else
708                     [execArg appendFormat:@" %@", arg];
709             }
710         }
712         // Launch the process with a login shell so that users environment
713         // settings get sourced.  This does not always happen when MacVim is
714         // started.
715         taskArgs = [NSArray arrayWithObjects:@"-l", @"-c", execArg, nil];
716         taskPath = [[[NSProcessInfo processInfo] environment]
717             objectForKey:@"SHELL"];
718         if (!taskPath)
719             taskPath = @"/bin/sh";
720     } else {
721         // Run process directly:
722         //   Vim -g -f args
723         // (-g for GUI, -f for foreground, i.e. don't fork)
724         taskPath = path;
725         taskArgs = [NSArray arrayWithObjects:@"-g", @"-f", nil];
726         if (args)
727             taskArgs = [taskArgs arrayByAddingObjectsFromArray:args];
728     }
730     NSTask *task =[NSTask launchedTaskWithLaunchPath:taskPath
731                                            arguments:taskArgs];
732     //NSLog(@"launch %@ with args=%@ (pid=%d)", taskPath, taskArgs,
733     //        [task processIdentifier]);
735     return [task processIdentifier];
738 - (NSArray *)filterFilesAndNotify:(NSArray *)filenames
740     // Go trough 'filenames' array and make sure each file exists.  Present
741     // warning dialog if some file was missing.
743     NSString *firstMissingFile = nil;
744     NSMutableArray *files = [NSMutableArray array];
745     unsigned i, count = [filenames count];
747     for (i = 0; i < count; ++i) {
748         NSString *name = [filenames objectAtIndex:i];
749         if ([[NSFileManager defaultManager] fileExistsAtPath:name]) {
750             [files addObject:name];
751         } else if (!firstMissingFile) {
752             firstMissingFile = name;
753         }
754     }
756     if (firstMissingFile) {
757         NSAlert *alert = [[NSAlert alloc] init];
758         [alert addButtonWithTitle:@"OK"];
760         NSString *text;
761         if ([files count] >= count-1) {
762             [alert setMessageText:@"File not found"];
763             text = [NSString stringWithFormat:@"Could not open file with "
764                 "name %@.", firstMissingFile];
765         } else {
766             [alert setMessageText:@"Multiple files not found"];
767             text = [NSString stringWithFormat:@"Could not open file with "
768                 "name %@, and %d other files.", firstMissingFile,
769                 count-[files count]-1];
770         }
772         [alert setInformativeText:text];
773         [alert setAlertStyle:NSWarningAlertStyle];
775         [alert runModal];
776         [alert release];
778         [NSApp replyToOpenOrPrint:NSApplicationDelegateReplyFailure];
779     }
781     return files;
784 - (NSArray *)filterOpenFiles:(NSArray *)filenames remote:(OSType)theID
785                         path:(NSString *)path
786                        token:(NSAppleEventDescriptor *)token
787               selectionRange:(MMSelectionRange *)selRange
789     // Check if any of the files in the 'filenames' array are open in any Vim
790     // process.  Remove the files that are open from the 'filenames' array and
791     // return it.  If all files were filtered out, then raise the first file in
792     // the Vim process it is open.  Files that are filtered are sent an odb
793     // open event in case theID is not zero.
795     MMVimController *raiseController = nil;
796     NSString *raiseFile = nil;
797     NSMutableArray *files = [filenames mutableCopy];
798     NSString *expr = [NSString stringWithFormat:
799             @"map([\"%@\"],\"bufloaded(v:val)\")",
800             [files componentsJoinedByString:@"\",\""]];
801     unsigned i, count = [vimControllers count];
803     for (i = 0; i < count && [files count]; ++i) {
804         MMVimController *controller = [vimControllers objectAtIndex:i];
805         id proxy = [controller backendProxy];
807         @try {
808             NSString *eval = [proxy evaluateExpression:expr];
809             NSIndexSet *idxSet = [NSIndexSet indexSetWithVimList:eval];
810             if ([idxSet count]) {
811                 if (!raiseFile) {
812                     // Remember the file and which Vim that has it open so that
813                     // we can raise it later on.
814                     raiseController = controller;
815                     raiseFile = [files objectAtIndex:[idxSet firstIndex]];
816                     [[raiseFile retain] autorelease];
817                 }
819                 // Send an odb open event to the Vim process.
820                 if (theID != 0)
821                     [controller odbEdit:[files objectsAtIndexes:idxSet]
822                                  server:theID path:path token:token];
824                 // Remove all the files that were open in this Vim process and
825                 // create a new expression to evaluate.
826                 [files removeObjectsAtIndexes:idxSet];
827                 expr = [NSString stringWithFormat:
828                         @"map([\"%@\"],\"bufloaded(v:val)\")",
829                         [files componentsJoinedByString:@"\",\""]];
830             }
831         }
832         @catch (NSException *e) {
833             // Do nothing ...
834         }
835     }
837     if (![files count] && raiseFile) {
838         // Raise the window containing the first file that was already open,
839         // and make sure that the tab containing that file is selected.  Only
840         // do this if there are no more files to open, otherwise sometimes the
841         // window with 'raiseFile' will be raised, other times it might be the
842         // window that will open with the files in the 'files' array.
843         raiseFile = [raiseFile stringByEscapingSpecialFilenameCharacters];
844         NSString *input = [NSString stringWithFormat:@"<C-\\><C-N>"
845             ":let oldswb=&swb|let &swb=\"useopen,usetab\"|"
846             "tab sb %@|let &swb=oldswb|unl oldswb|"
847             "cal foreground()|redr|f<CR>", raiseFile];
849         if (selRange)
850             input = [input stringByAppendingString:
851                     [self inputStringFromSelectionRange:selRange]];
853         [raiseController addVimInput:input];
854     }
856     return files;
859 - (void)handleXcodeModEvent:(NSAppleEventDescriptor *)event
860                  replyEvent:(NSAppleEventDescriptor *)reply
862 #if 0
863     // Xcode sends this event to query MacVim which open files have been
864     // modified.
865     NSLog(@"reply:%@", reply);
866     NSLog(@"event:%@", event);
868     NSEnumerator *e = [vimControllers objectEnumerator];
869     id vc;
870     while ((vc = [e nextObject])) {
871         DescType type = [reply descriptorType];
872         unsigned len = [[type data] length];
873         NSMutableData *data = [NSMutableData data];
875         [data appendBytes:&type length:sizeof(DescType)];
876         [data appendBytes:&len length:sizeof(unsigned)];
877         [data appendBytes:[reply data] length:len];
879         [vc sendMessage:XcodeModMsgID data:data];
880     }
881 #endif
884 - (NSString *)inputStringFromSelectionRange:(MMSelectionRange *)selRange
886     if (!selRange)
887         return [NSString string];
889     NSString *input;
890     if (selRange->lineNum < 0) {
891         input = [NSString stringWithFormat:@"<C-\\><C-N>%dGV%dG",
892               selRange->endRange+1, selRange->startRange+1];
893     } else {
894         input = [NSString stringWithFormat:@"<C-\\><C-N>%dGz.",
895               selRange->lineNum+1];
896     }
898     return input;
901 @end // MMAppController (Private)
906 @implementation NSMenu (MMExtras)
908 - (void)recurseSetAutoenablesItems:(BOOL)on
910     [self setAutoenablesItems:on];
912     int i, count = [self numberOfItems];
913     for (i = 0; i < count; ++i) {
914         NSMenuItem *item = [self itemAtIndex:i];
915         [item setEnabled:YES];
916         NSMenu *submenu = [item submenu];
917         if (submenu) {
918             [submenu recurseSetAutoenablesItems:on];
919         }
920     }
923 @end  // NSMenu (MMExtras)
928 @implementation NSNumber (MMExtras)
929 - (int)tag
931     return [self intValue];
933 @end // NSNumber (MMExtras)