Added preference panel
[MacVim.git] / src / MacVim / MMAppController.m
blob442b42df8aa4309a5e4d23230e84025d97681c15
1 /* vi:set ts=8 sts=4 sw=4 ft=objc:
2  *
3  * VIM - Vi IMproved            by Bram Moolenaar
4  *                              MacVim GUI port by Bjorn Winckler
5  *
6  * Do ":help uganda"  in Vim to read copying and usage conditions.
7  * Do ":help credits" in Vim to see a list of people who contributed.
8  * See README.txt for an overview of the Vim source code.
9  */
11  * MMAppController
12  *
13  * MMAppController is the delegate of NSApp and as such handles file open
14  * requests, application termination, etc.  It sets up a named NSConnection on
15  * which it listens to incoming connections from Vim processes.  It also
16  * coordinates all MMVimControllers.
17  *
18  * A new Vim process is started by calling launchVimProcessWithArguments:.
19  * When the Vim process is initialized it notifies the app controller by
20  * sending a connectBackend:pid: message.  At this point a new MMVimController
21  * is allocated.  Afterwards, the Vim process communicates directly with its
22  * MMVimController.
23  *
24  * A Vim process started from the command line connects directly by sending the
25  * connectBackend:pid: message (launchVimProcessWithArguments: is never called
26  * in this case).
27  */
29 #import "MMAppController.h"
30 #import "MMVimController.h"
31 #import "MMWindowController.h"
32 #import "MMPreferenceController.h"
35 #define MM_HANDLE_XCODE_MOD_EVENT 0
39 // Default timeout intervals on all connections.
40 static NSTimeInterval MMRequestTimeout = 5;
41 static NSTimeInterval MMReplyTimeout = 5;
44 #pragma options align=mac68k
45 typedef struct
47     short unused1;      // 0 (not used)
48     short lineNum;      // line to select (< 0 to specify range)
49     long  startRange;   // start of selection range (if line < 0)
50     long  endRange;     // end of selection range (if line < 0)
51     long  unused2;      // 0 (not used)
52     long  theDate;      // modification date/time
53 } MMSelectionRange;
54 #pragma options align=reset
57 @interface MMAppController (MMServices)
58 - (void)openSelection:(NSPasteboard *)pboard userData:(NSString *)userData
59                 error:(NSString **)error;
60 - (void)openFile:(NSPasteboard *)pboard userData:(NSString *)userData
61            error:(NSString **)error;
62 @end
65 @interface MMAppController (Private)
66 - (MMVimController *)keyVimController;
67 - (MMVimController *)topmostVimController;
68 - (int)launchVimProcessWithArguments:(NSArray *)args;
69 - (NSArray *)filterFilesAndNotify:(NSArray *)files;
70 - (NSArray *)filterOpenFiles:(NSArray *)filenames
71                    arguments:(NSDictionary *)args;
72 #if MM_HANDLE_XCODE_MOD_EVENT
73 - (void)handleXcodeModEvent:(NSAppleEventDescriptor *)event
74                  replyEvent:(NSAppleEventDescriptor *)reply;
75 #endif
76 - (int)findLaunchingProcessWithoutArguments;
77 - (MMVimController *)findUntitledWindow;
78 - (NSMutableDictionary *)extractArgumentsFromOdocEvent:
79     (NSAppleEventDescriptor *)desc;
80 - (void)passArguments:(NSDictionary *)args toVimController:(MMVimController*)vc;
81 @end
83 @interface NSMenu (MMExtras)
84 - (void)recurseSetAutoenablesItems:(BOOL)on;
85 @end
87 @interface NSNumber (MMExtras)
88 - (int)tag;
89 @end
93 @implementation MMAppController
95 + (void)initialize
97     NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:
98         [NSNumber numberWithBool:NO],   MMNoWindowKey,
99         [NSNumber numberWithInt:64],    MMTabMinWidthKey,
100         [NSNumber numberWithInt:6*64],  MMTabMaxWidthKey,
101         [NSNumber numberWithInt:132],   MMTabOptimumWidthKey,
102         [NSNumber numberWithInt:2],     MMTextInsetLeftKey,
103         [NSNumber numberWithInt:1],     MMTextInsetRightKey,
104         [NSNumber numberWithInt:1],     MMTextInsetTopKey,
105         [NSNumber numberWithInt:1],     MMTextInsetBottomKey,
106         [NSNumber numberWithBool:NO],   MMTerminateAfterLastWindowClosedKey,
107         @"MMTypesetter",                MMTypesetterKey,
108         [NSNumber numberWithFloat:1],   MMCellWidthMultiplierKey,
109         [NSNumber numberWithFloat:-1],  MMBaselineOffsetKey,
110         [NSNumber numberWithBool:YES],  MMTranslateCtrlClickKey,
111         [NSNumber numberWithBool:NO],   MMOpenFilesInTabsKey,
112         [NSNumber numberWithBool:NO],   MMNoFontSubstitutionKey,
113         [NSNumber numberWithBool:NO],   MMLoginShellKey,
114         [NSNumber numberWithBool:NO],   MMAtsuiRendererKey,
115         [NSNumber numberWithInt:MMUntitledWindowAlways],
116                                         MMUntitledWindowKey,
117         [NSNumber numberWithBool:NO],   MMTexturedWindowKey,
118         nil];
120     [[NSUserDefaults standardUserDefaults] registerDefaults:dict];
122     NSArray *types = [NSArray arrayWithObject:NSStringPboardType];
123     [NSApp registerServicesMenuSendTypes:types returnTypes:types];
126 - (id)init
128     if ((self = [super init])) {
129         fontContainerRef = loadFonts();
131         vimControllers = [NSMutableArray new];
132         pidArguments = [NSMutableDictionary new];
134         // NOTE!  If the name of the connection changes here it must also be
135         // updated in MMBackend.m.
136         NSConnection *connection = [NSConnection defaultConnection];
137         NSString *name = [NSString stringWithFormat:@"%@-connection",
138                  [[NSBundle mainBundle] bundleIdentifier]];
139         //NSLog(@"Registering connection with name '%@'", name);
140         if ([connection registerName:name]) {
141             [connection setRequestTimeout:MMRequestTimeout];
142             [connection setReplyTimeout:MMReplyTimeout];
143             [connection setRootObject:self];
145             // NOTE: When the user is resizing the window the AppKit puts the
146             // run loop in event tracking mode.  Unless the connection listens
147             // to request in this mode, live resizing won't work.
148             [connection addRequestMode:NSEventTrackingRunLoopMode];
149         } else {
150             NSLog(@"WARNING: Failed to register connection with name '%@'",
151                     name);
152         }
153     }
155     return self;
158 - (void)dealloc
160     //NSLog(@"MMAppController dealloc");
162     [pidArguments release];  pidArguments = nil;
163     [vimControllers release];  vimControllers = nil;
164     [openSelectionString release];  openSelectionString = nil;
166     [super dealloc];
169 #if MM_HANDLE_XCODE_MOD_EVENT
170 - (void)applicationWillFinishLaunching:(NSNotification *)notification
172     [[NSAppleEventManager sharedAppleEventManager]
173             setEventHandler:self
174                 andSelector:@selector(handleXcodeModEvent:replyEvent:)
175               forEventClass:'KAHL'
176                  andEventID:'MOD '];
178 #endif
180 - (void)applicationDidFinishLaunching:(NSNotification *)notification
182     [NSApp setServicesProvider:self];
185 - (BOOL)applicationShouldOpenUntitledFile:(NSApplication *)sender
187     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
188     NSAppleEventManager *aem = [NSAppleEventManager sharedAppleEventManager];
189     NSAppleEventDescriptor *desc = [aem currentAppleEvent];
191     // The user default MMUntitledWindow can be set to control whether an
192     // untitled window should open on 'Open' and 'Reopen' events.
193     int untitledWindowFlag = [ud integerForKey:MMUntitledWindowKey];
194     if ([desc eventID] == kAEOpenApplication
195             && (untitledWindowFlag & MMUntitledWindowOnOpen) == 0)
196         return NO;
197     else if ([desc eventID] == kAEReopenApplication
198             && (untitledWindowFlag & MMUntitledWindowOnReopen) == 0)
199         return NO;
201     // When a process is started from the command line, the 'Open' event will
202     // contain a parameter to surpress the opening of an untitled window.
203     desc = [desc paramDescriptorForKeyword:keyAEPropData];
204     desc = [desc paramDescriptorForKeyword:keyMMUntitledWindow];
205     if (desc && ![desc booleanValue])
206         return NO;
208     // Never open an untitled window if there is at least one open window or if
209     // there are processes that are currently launching.
210     if ([vimControllers count] > 0 || [pidArguments count] > 0)
211         return NO;
213     // NOTE!  This way it possible to start the app with the command-line
214     // argument '-nowindow yes' and no window will be opened by default.
215     return ![ud boolForKey:MMNoWindowKey];
218 - (BOOL)applicationOpenUntitledFile:(NSApplication *)sender
220     [self newWindow:self];
221     return YES;
224 - (void)application:(NSApplication *)sender openFiles:(NSArray *)filenames
226     // Opening files works like this:
227     //  a) extract ODB/Xcode/Spotlight parameters from the current Apple event
228     //  b) filter out any already open files (see filterOpenFiles::)
229     //  c) open any remaining files
230     //
231     // A file is opened in an untitled window if there is one (it may be
232     // currently launching, or it may already be visible), otherwise a new
233     // window is opened.
234     //
235     // Each launching Vim process has a dictionary of arguments that are passed
236     // to the process when in checks in (via connectBackend:pid:).  The
237     // arguments for each launching process can be looked up by its PID (in the
238     // pidArguments dictionary).
240     NSMutableDictionary *arguments = [self extractArgumentsFromOdocEvent:
241             [[NSAppleEventManager sharedAppleEventManager] currentAppleEvent]];
243     // Filter out files that are already open
244     filenames = [self filterOpenFiles:filenames arguments:arguments];
246     // Open any files that remain
247     if ([filenames count]) {
248         MMVimController *vc;
249         BOOL openInTabs = [[NSUserDefaults standardUserDefaults]
250             boolForKey:MMOpenFilesInTabsKey];
252         [arguments setObject:filenames forKey:@"filenames"];
253         [arguments setObject:[NSNumber numberWithBool:YES] forKey:@"openFiles"];
255         if ((openInTabs && (vc = [self topmostVimController]))
256                || (vc = [self findUntitledWindow])) {
257             // Open files in an already open window.
258             [self passArguments:arguments toVimController:vc];
259         } else {
260             // Open files in a launching Vim process or start a new process.
261             int pid = [self findLaunchingProcessWithoutArguments];
262             if (!pid) {
263                 // Pass the filenames to the process straight away.
264                 //
265                 // TODO: It would be nicer if all arguments were passed to the
266                 // Vim process in connectBackend::, but if we don't pass the
267                 // filename arguments here, the window 'flashes' once when it
268                 // opens.  This is due to the 'welcome' screen first being
269                 // displayed, then quickly thereafter the files are opened.
270                 NSArray *fileArgs = [NSArray arrayWithObject:@"-p"];
271                 fileArgs = [fileArgs arrayByAddingObjectsFromArray:filenames];
273                 pid = [self launchVimProcessWithArguments:fileArgs];
275                 // Make sure these files aren't opened again when
276                 // connectBackend:pid: is called.
277                 [arguments setObject:[NSNumber numberWithBool:NO]
278                               forKey:@"openFiles"];
279             }
281             // TODO: If the Vim process fails to start, or if it changes PID,
282             // then the memory allocated for these parameters will leak.
283             // Ensure that this cannot happen or somehow detect it.
285             if ([arguments count] > 0)
286                 [pidArguments setObject:arguments
287                                  forKey:[NSNumber numberWithInt:pid]];
288         }
289     }
291     [NSApp replyToOpenOrPrint:NSApplicationDelegateReplySuccess];
292     // NSApplicationDelegateReplySuccess = 0,
293     // NSApplicationDelegateReplyCancel = 1,
294     // NSApplicationDelegateReplyFailure = 2
297 - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender
299     return [[NSUserDefaults standardUserDefaults]
300             boolForKey:MMTerminateAfterLastWindowClosedKey];
303 - (NSApplicationTerminateReply)applicationShouldTerminate:
304     (NSApplication *)sender
306     // TODO: Follow Apple's guidelines for 'Graceful Application Termination'
307     // (in particular, allow user to review changes and save).
308     int reply = NSTerminateNow;
309     BOOL modifiedBuffers = NO;
311     // Go through windows, checking for modified buffers.  (Each Vim process
312     // tells MacVim when any buffer has been modified and MacVim sets the
313     // 'documentEdited' flag of the window correspondingly.)
314     NSEnumerator *e = [[NSApp windows] objectEnumerator];
315     id window;
316     while ((window = [e nextObject])) {
317         if ([window isDocumentEdited]) {
318             modifiedBuffers = YES;
319             break;
320         }
321     }
323     if (modifiedBuffers) {
324         NSAlert *alert = [[NSAlert alloc] init];
325         [alert addButtonWithTitle:@"Quit"];
326         [alert addButtonWithTitle:@"Cancel"];
327         [alert setMessageText:@"Quit without saving?"];
328         [alert setInformativeText:@"There are modified buffers, "
329             "if you quit now all changes will be lost.  Quit anyway?"];
330         [alert setAlertStyle:NSWarningAlertStyle];
332         if ([alert runModal] != NSAlertFirstButtonReturn)
333             reply = NSTerminateCancel;
335         [alert release];
336     }
338     // Tell all Vim processes to terminate now (otherwise they'll leave swap
339     // files behind).
340     if (NSTerminateNow == reply) {
341         e = [vimControllers objectEnumerator];
342         id vc;
343         while ((vc = [e nextObject]))
344             [vc sendMessage:TerminateNowMsgID data:nil];
345     }
347     return reply;
350 - (void)applicationWillTerminate:(NSNotification *)notification
352 #if MM_HANDLE_XCODE_MOD_EVENT
353     [[NSAppleEventManager sharedAppleEventManager]
354             removeEventHandlerForEventClass:'KAHL'
355                                  andEventID:'MOD '];
356 #endif
358     // This will invalidate all connections (since they were spawned from the
359     // default connection).
360     [[NSConnection defaultConnection] invalidate];
362     // Send a SIGINT to all running Vim processes, so that they are sure to
363     // receive the connectionDidDie: notification (a process has to be checking
364     // the run-loop for this to happen).
365     unsigned i, count = [vimControllers count];
366     for (i = 0; i < count; ++i) {
367         MMVimController *controller = [vimControllers objectAtIndex:i];
368         int pid = [controller pid];
369         if (pid > 0)
370             kill(pid, SIGINT);
371     }
373     if (fontContainerRef) {
374         ATSFontDeactivate(fontContainerRef, NULL, kATSOptionFlagsDefault);
375         fontContainerRef = 0;
376     }
378     // TODO: Is this a correct way of releasing the MMAppController?
379     // (It doesn't seem like dealloc is ever called.)
380     [NSApp setDelegate:nil];
381     [self autorelease];
384 - (void)removeVimController:(id)controller
386     //NSLog(@"%s%@", _cmd, controller);
388     [[controller windowController] close];
390     [vimControllers removeObject:controller];
392     if (![vimControllers count]) {
393         // Turn on autoenabling of menus (because no Vim is open to handle it),
394         // but do not touch the MacVim menu.  Note that the menus must be
395         // enabled first otherwise autoenabling does not work.
396         NSMenu *mainMenu = [NSApp mainMenu];
397         int i, count = [mainMenu numberOfItems];
398         for (i = 1; i < count; ++i) {
399             NSMenuItem *item = [mainMenu itemAtIndex:i];
400             [item setEnabled:YES];
401             [[item submenu] recurseSetAutoenablesItems:YES];
402         }
403     }
406 - (void)windowControllerWillOpen:(MMWindowController *)windowController
408     NSPoint topLeft = NSZeroPoint;
409     NSWindow *keyWin = [NSApp keyWindow];
410     NSWindow *win = [windowController window];
412     if (!win) return;
414     // If there is a key window, cascade from it, otherwise use the autosaved
415     // window position (if any).
416     if (keyWin) {
417         NSRect frame = [keyWin frame];
418         topLeft = NSMakePoint(frame.origin.x, NSMaxY(frame));
419     } else {
420         NSString *topLeftString = [[NSUserDefaults standardUserDefaults]
421             stringForKey:MMTopLeftPointKey];
422         if (topLeftString)
423             topLeft = NSPointFromString(topLeftString);
424     }
426     if (!NSEqualPoints(topLeft, NSZeroPoint)) {
427         if (keyWin)
428             topLeft = [win cascadeTopLeftFromPoint:topLeft];
430         [win setFrameTopLeftPoint:topLeft];
431     }
433     if (openSelectionString) {
434         // TODO: Pass this as a parameter instead!  Get rid of
435         // 'openSelectionString' etc.
436         //
437         // There is some text to paste into this window as a result of the
438         // services menu "Open selection ..." being used.
439         [[windowController vimController] dropString:openSelectionString];
440         [openSelectionString release];
441         openSelectionString = nil;
442     }
445 - (IBAction)newWindow:(id)sender
447     [self launchVimProcessWithArguments:nil];
450 - (IBAction)fileOpen:(id)sender
452     NSOpenPanel *panel = [NSOpenPanel openPanel];
453     [panel setAllowsMultipleSelection:YES];
455     int result = [panel runModalForTypes:nil];
456     if (NSOKButton == result)
457         [self application:NSApp openFiles:[panel filenames]];
460 - (IBAction)selectNextWindow:(id)sender
462     unsigned i, count = [vimControllers count];
463     if (!count) return;
465     NSWindow *keyWindow = [NSApp keyWindow];
466     for (i = 0; i < count; ++i) {
467         MMVimController *vc = [vimControllers objectAtIndex:i];
468         if ([[[vc windowController] window] isEqual:keyWindow])
469             break;
470     }
472     if (i < count) {
473         if (++i >= count)
474             i = 0;
475         MMVimController *vc = [vimControllers objectAtIndex:i];
476         [[vc windowController] showWindow:self];
477     }
480 - (IBAction)selectPreviousWindow:(id)sender
482     unsigned i, count = [vimControllers count];
483     if (!count) return;
485     NSWindow *keyWindow = [NSApp keyWindow];
486     for (i = 0; i < count; ++i) {
487         MMVimController *vc = [vimControllers objectAtIndex:i];
488         if ([[[vc windowController] window] isEqual:keyWindow])
489             break;
490     }
492     if (i < count) {
493         if (i > 0) {
494             --i;
495         } else {
496             i = count - 1;
497         }
498         MMVimController *vc = [vimControllers objectAtIndex:i];
499         [[vc windowController] showWindow:self];
500     }
503 - (IBAction)fontSizeUp:(id)sender
505     [[NSFontManager sharedFontManager] modifyFont:
506             [NSNumber numberWithInt:NSSizeUpFontAction]];
509 - (IBAction)fontSizeDown:(id)sender
511     [[NSFontManager sharedFontManager] modifyFont:
512             [NSNumber numberWithInt:NSSizeDownFontAction]];
515 - (IBAction)orderFrontPreferencePanel:(id)sender
517     [[MMPreferenceController sharedPreferenceController] showWindow:self];
520 - (byref id <MMFrontendProtocol>)
521     connectBackend:(byref in id <MMBackendProtocol>)backend
522                pid:(int)pid
524     //NSLog(@"Connect backend (pid=%d)", pid);
525     NSNumber *pidKey = [NSNumber numberWithInt:pid];
526     MMVimController *vc = nil;
528     @try {
529         [(NSDistantObject*)backend
530                 setProtocolForProxy:@protocol(MMBackendProtocol)];
532         vc = [[[MMVimController alloc]
533                 initWithBackend:backend pid:pid] autorelease];
535         if (![vimControllers count]) {
536             // The first window autosaves its position.  (The autosaving
537             // features of Cocoa are not used because we need more control over
538             // what is autosaved and when it is restored.)
539             [[vc windowController] setWindowAutosaveKey:MMTopLeftPointKey];
540         }
542         [vimControllers addObject:vc];
544         id args = [pidArguments objectForKey:pidKey];
545         if (args && [NSNull null] != args)
546             [self passArguments:args toVimController:vc];
548         // HACK!  MacVim does not get activated if it is launched from the
549         // terminal, so we forcibly activate here unless it is an untitled
550         // window opening.  Untitled windows are treated differently, else
551         // MacVim would steal the focus if another app was activated while the
552         // untitled window was loading.
553         if (!args || args != [NSNull null])
554             [NSApp activateIgnoringOtherApps:YES];
556         if (args)
557             [pidArguments removeObjectForKey:pidKey];
559         return vc;
560     }
562     @catch (NSException *e) {
563         NSLog(@"Exception caught in %s: \"%@\"", _cmd, e);
565         if (vc)
566             [vimControllers removeObject:vc];
568         [pidArguments removeObjectForKey:pidKey];
569     }
571     return nil;
574 - (NSArray *)serverList
576     NSMutableArray *array = [NSMutableArray array];
578     unsigned i, count = [vimControllers count];
579     for (i = 0; i < count; ++i) {
580         MMVimController *controller = [vimControllers objectAtIndex:i];
581         if ([controller serverName])
582             [array addObject:[controller serverName]];
583     }
585     return array;
588 @end // MMAppController
593 @implementation MMAppController (MMServices)
595 - (void)openSelection:(NSPasteboard *)pboard userData:(NSString *)userData
596                 error:(NSString **)error
598     if (![[pboard types] containsObject:NSStringPboardType]) {
599         NSLog(@"WARNING: Pasteboard contains no object of type "
600                 "NSStringPboardType");
601         return;
602     }
604     MMVimController *vc = [self topmostVimController];
605     if (vc) {
606         // Open a new tab first, since dropString: does not do this.
607         [vc sendMessage:AddNewTabMsgID data:nil];
608         [vc dropString:[pboard stringForType:NSStringPboardType]];
609     } else {
610         // NOTE: There is no window to paste the selection into, so save the
611         // text, open a new window, and paste the text when the next window
612         // opens.  (If this is called several times in a row, then all but the
613         // last call might be ignored.)
614         if (openSelectionString) [openSelectionString release];
615         openSelectionString = [[pboard stringForType:NSStringPboardType] copy];
617         [self newWindow:self];
618     }
621 - (void)openFile:(NSPasteboard *)pboard userData:(NSString *)userData
622            error:(NSString **)error
624     if (![[pboard types] containsObject:NSStringPboardType]) {
625         NSLog(@"WARNING: Pasteboard contains no object of type "
626                 "NSStringPboardType");
627         return;
628     }
630     // TODO: Parse multiple filenames and create array with names.
631     NSString *string = [pboard stringForType:NSStringPboardType];
632     string = [string stringByTrimmingCharactersInSet:
633             [NSCharacterSet whitespaceAndNewlineCharacterSet]];
634     string = [string stringByStandardizingPath];
636     NSArray *filenames = [self filterFilesAndNotify:
637             [NSArray arrayWithObject:string]];
638     if ([filenames count] > 0) {
639         MMVimController *vc = nil;
640         if (userData && [userData isEqual:@"Tab"])
641             vc = [self topmostVimController];
643         if (vc) {
644             [vc dropFiles:filenames forceOpen:YES];
645         } else {
646             [self application:NSApp openFiles:filenames];
647         }
648     }
651 @end // MMAppController (MMServices)
656 @implementation MMAppController (Private)
658 - (MMVimController *)keyVimController
660     NSWindow *keyWindow = [NSApp keyWindow];
661     if (keyWindow) {
662         unsigned i, count = [vimControllers count];
663         for (i = 0; i < count; ++i) {
664             MMVimController *vc = [vimControllers objectAtIndex:i];
665             if ([[[vc windowController] window] isEqual:keyWindow])
666                 return vc;
667         }
668     }
670     return nil;
673 - (MMVimController *)topmostVimController
675     NSArray *windows = [NSApp orderedWindows];
676     if ([windows count] > 0) {
677         NSWindow *window = [windows objectAtIndex:0];
678         unsigned i, count = [vimControllers count];
679         for (i = 0; i < count; ++i) {
680             MMVimController *vc = [vimControllers objectAtIndex:i];
681             if ([[[vc windowController] window] isEqual:window])
682                 return vc;
683         }
684     }
686     return nil;
689 - (int)launchVimProcessWithArguments:(NSArray *)args
691     NSString *taskPath = nil;
692     NSArray *taskArgs = nil;
693     NSString *path = [[NSBundle mainBundle] pathForAuxiliaryExecutable:@"Vim"];
695     if (!path) {
696         NSLog(@"ERROR: Vim executable could not be found inside app bundle!");
697         return 0;
698     }
700     if ([[NSUserDefaults standardUserDefaults] boolForKey:MMLoginShellKey]) {
701         // Run process with a login shell
702         //   $SHELL -l -c "exec Vim -g -f args"
703         // (-g for GUI, -f for foreground, i.e. don't fork)
705         NSMutableString *execArg = [NSMutableString
706             stringWithFormat:@"exec \"%@\" -g -f", path];
707         if (args) {
708             // Append all arguments while making sure that arguments containing
709             // spaces are enclosed in quotes.
710             NSCharacterSet *space = [NSCharacterSet whitespaceCharacterSet];
711             unsigned i, count = [args count];
713             for (i = 0; i < count; ++i) {
714                 NSString *arg = [args objectAtIndex:i];
715                 if (NSNotFound != [arg rangeOfCharacterFromSet:space].location)
716                     [execArg appendFormat:@" \"%@\"", arg];
717                 else
718                     [execArg appendFormat:@" %@", arg];
719             }
720         }
722         // Launch the process with a login shell so that users environment
723         // settings get sourced.  This does not always happen when MacVim is
724         // started.
725         taskArgs = [NSArray arrayWithObjects:@"-l", @"-c", execArg, nil];
726         taskPath = [[[NSProcessInfo processInfo] environment]
727             objectForKey:@"SHELL"];
728         if (!taskPath)
729             taskPath = @"/bin/sh";
730     } else {
731         // Run process directly:
732         //   Vim -g -f args
733         // (-g for GUI, -f for foreground, i.e. don't fork)
734         taskPath = path;
735         taskArgs = [NSArray arrayWithObjects:@"-g", @"-f", nil];
736         if (args)
737             taskArgs = [taskArgs arrayByAddingObjectsFromArray:args];
738     }
740     NSTask *task =[NSTask launchedTaskWithLaunchPath:taskPath
741                                            arguments:taskArgs];
742     //NSLog(@"launch %@ with args=%@ (pid=%d)", taskPath, taskArgs,
743     //        [task processIdentifier]);
745     int pid = [task processIdentifier];
747     // If the process has no arguments, then add a null argument to the
748     // pidArguments dictionary.  This is later used to detect that a process
749     // without arguments is being launched.
750     if (!args)
751         [pidArguments setObject:[NSNull null]
752                          forKey:[NSNumber numberWithInt:pid]];
754     return pid;
757 - (NSArray *)filterFilesAndNotify:(NSArray *)filenames
759     // Go trough 'filenames' array and make sure each file exists.  Present
760     // warning dialog if some file was missing.
762     NSString *firstMissingFile = nil;
763     NSMutableArray *files = [NSMutableArray array];
764     unsigned i, count = [filenames count];
766     for (i = 0; i < count; ++i) {
767         NSString *name = [filenames objectAtIndex:i];
768         if ([[NSFileManager defaultManager] fileExistsAtPath:name]) {
769             [files addObject:name];
770         } else if (!firstMissingFile) {
771             firstMissingFile = name;
772         }
773     }
775     if (firstMissingFile) {
776         NSAlert *alert = [[NSAlert alloc] init];
777         [alert addButtonWithTitle:@"OK"];
779         NSString *text;
780         if ([files count] >= count-1) {
781             [alert setMessageText:@"File not found"];
782             text = [NSString stringWithFormat:@"Could not open file with "
783                 "name %@.", firstMissingFile];
784         } else {
785             [alert setMessageText:@"Multiple files not found"];
786             text = [NSString stringWithFormat:@"Could not open file with "
787                 "name %@, and %d other files.", firstMissingFile,
788                 count-[files count]-1];
789         }
791         [alert setInformativeText:text];
792         [alert setAlertStyle:NSWarningAlertStyle];
794         [alert runModal];
795         [alert release];
797         [NSApp replyToOpenOrPrint:NSApplicationDelegateReplyFailure];
798     }
800     return files;
803 - (NSArray *)filterOpenFiles:(NSArray *)filenames
804                    arguments:(NSDictionary *)args
806     // Check if any of the files in the 'filenames' array are open in any Vim
807     // process.  Remove the files that are open from the 'filenames' array and
808     // return it.  If all files were filtered out, then raise the first file in
809     // the Vim process it is open.  Files that are filtered are sent an odb
810     // open event in case theID is not zero.
812     NSMutableDictionary *localArgs =
813             [NSMutableDictionary dictionaryWithDictionary:args];
814     MMVimController *raiseController = nil;
815     NSString *raiseFile = nil;
816     NSMutableArray *files = [filenames mutableCopy];
817     NSString *expr = [NSString stringWithFormat:
818             @"map([\"%@\"],\"bufloaded(v:val)\")",
819             [files componentsJoinedByString:@"\",\""]];
820     unsigned i, count = [vimControllers count];
822     // Ensure that the files aren't opened when passing arguments.
823     [localArgs setObject:[NSNumber numberWithBool:NO] forKey:@"openFiles"];
825     for (i = 0; i < count && [files count]; ++i) {
826         MMVimController *controller = [vimControllers objectAtIndex:i];
827         id proxy = [controller backendProxy];
829         @try {
830             // Query Vim for which files in the 'files' array are open.
831             NSString *eval = [proxy evaluateExpression:expr];
832             NSIndexSet *idxSet = [NSIndexSet indexSetWithVimList:eval];
833             if ([idxSet count]) {
834                 if (!raiseFile) {
835                     // Remember the file and which Vim that has it open so that
836                     // we can raise it later on.
837                     raiseController = controller;
838                     raiseFile = [files objectAtIndex:[idxSet firstIndex]];
839                     [[raiseFile retain] autorelease];
840                 }
842                 // Pass (ODB/Xcode/Spotlight) arguments to this process.
843                 [localArgs setObject:[files objectsAtIndexes:idxSet]
844                               forKey:@"filenames"];
845                 [self passArguments:localArgs toVimController:controller];
847                 // Remove all the files that were open in this Vim process and
848                 // create a new expression to evaluate.
849                 [files removeObjectsAtIndexes:idxSet];
850                 expr = [NSString stringWithFormat:
851                         @"map([\"%@\"],\"bufloaded(v:val)\")",
852                         [files componentsJoinedByString:@"\",\""]];
853             }
854         }
855         @catch (NSException *e) {
856             // Do nothing ...
857         }
858     }
860     if (![files count] && raiseFile) {
861         // Raise the window containing the first file that was already open,
862         // and make sure that the tab containing that file is selected.  Only
863         // do this if there are no more files to open, otherwise sometimes the
864         // window with 'raiseFile' will be raised, other times it might be the
865         // window that will open with the files in the 'files' array.
866         raiseFile = [raiseFile stringByEscapingSpecialFilenameCharacters];
867         NSString *input = [NSString stringWithFormat:@"<C-\\><C-N>"
868             ":let oldswb=&swb|let &swb=\"useopen,usetab\"|"
869             "tab sb %@|let &swb=oldswb|unl oldswb|"
870             "cal foreground()|redr|f<CR>", raiseFile];
872         [raiseController addVimInput:input];
873     }
875     return files;
878 #if MM_HANDLE_XCODE_MOD_EVENT
879 - (void)handleXcodeModEvent:(NSAppleEventDescriptor *)event
880                  replyEvent:(NSAppleEventDescriptor *)reply
882 #if 0
883     // Xcode sends this event to query MacVim which open files have been
884     // modified.
885     NSLog(@"reply:%@", reply);
886     NSLog(@"event:%@", event);
888     NSEnumerator *e = [vimControllers objectEnumerator];
889     id vc;
890     while ((vc = [e nextObject])) {
891         DescType type = [reply descriptorType];
892         unsigned len = [[type data] length];
893         NSMutableData *data = [NSMutableData data];
895         [data appendBytes:&type length:sizeof(DescType)];
896         [data appendBytes:&len length:sizeof(unsigned)];
897         [data appendBytes:[reply data] length:len];
899         [vc sendMessage:XcodeModMsgID data:data];
900     }
901 #endif
903 #endif
905 - (int)findLaunchingProcessWithoutArguments
907     NSArray *keys = [pidArguments allKeysForObject:[NSNull null]];
908     if ([keys count] > 0) {
909         //NSLog(@"found launching process without arguments");
910         return [[keys objectAtIndex:0] intValue];
911     }
913     return 0;
916 - (MMVimController *)findUntitledWindow
918     NSEnumerator *e = [vimControllers objectEnumerator];
919     id vc;
920     while ((vc = [e nextObject])) {
921         // TODO: This is a moronic test...should query the Vim process if there
922         // are any open buffers or something like that instead.
923         NSString *title = [[[vc windowController] window] title];
924         if ([title hasPrefix:@"[No Name]"]) {
925             //NSLog(@"found untitled window");
926             return vc;
927         }
928     }
930     return nil;
933 - (NSMutableDictionary *)extractArgumentsFromOdocEvent:
934     (NSAppleEventDescriptor *)desc
936     NSMutableDictionary *dict = [NSMutableDictionary dictionary];
938     // 1. Extract ODB parameters (if any)
939     NSAppleEventDescriptor *odbdesc = desc;
940     if (![odbdesc paramDescriptorForKeyword:keyFileSender]) {
941         // The ODB paramaters may hide inside the 'keyAEPropData' descriptor.
942         odbdesc = [odbdesc paramDescriptorForKeyword:keyAEPropData];
943         if (![odbdesc paramDescriptorForKeyword:keyFileSender])
944             odbdesc = nil;
945     }
947     if (odbdesc) {
948         NSAppleEventDescriptor *p =
949                 [odbdesc paramDescriptorForKeyword:keyFileSender];
950         if (p)
951             [dict setObject:[NSNumber numberWithUnsignedInt:[p typeCodeValue]]
952                      forKey:@"remoteID"];
954         p = [odbdesc paramDescriptorForKeyword:keyFileCustomPath];
955         if (p)
956             [dict setObject:[p stringValue] forKey:@"remotePath"];
958         p = [odbdesc paramDescriptorForKeyword:keyFileSenderToken];
959         if (p)
960             [dict setObject:p forKey:@"remotePath"];
961     }
963     // 2. Extract Xcode parameters (if any)
964     NSAppleEventDescriptor *xcodedesc =
965             [desc paramDescriptorForKeyword:keyAEPosition];
966     if (xcodedesc) {
967         NSRange range;
968         MMSelectionRange *sr = (MMSelectionRange*)[[xcodedesc data] bytes];
970         if (sr->lineNum < 0) {
971             // Should select a range of lines.
972             range.location = sr->startRange + 1;
973             range.length = sr->endRange - sr->startRange + 1;
974         } else {
975             // Should only move cursor to a line.
976             range.location = sr->lineNum + 1;
977             range.length = 0;
978         }
980         [dict setObject:NSStringFromRange(range) forKey:@"selectionRange"];
981     }
983     // 3. Extract Spotlight search text (if any)
984     NSAppleEventDescriptor *spotlightdesc = 
985             [desc paramDescriptorForKeyword:keyAESearchText];
986     if (spotlightdesc)
987         [dict setObject:[spotlightdesc stringValue] forKey:@"searchText"];
989     return dict;
992 - (void)passArguments:(NSDictionary *)args toVimController:(MMVimController*)vc
994     if (!args) return;
996     // Pass filenames to open if required (the 'openFiles' argument can be used
997     // to disallow opening of the files).
998     NSArray *filenames = [args objectForKey:@"filenames"];
999     if (filenames && [[args objectForKey:@"openFiles"] boolValue]) {
1000         NSString *tabDrop = buildTabDropCommand(filenames);
1001         [vc addVimInput:tabDrop];
1002     }
1004     // Pass ODB data
1005     if (filenames && [args objectForKey:@"remoteID"]) {
1006         [vc odbEdit:filenames
1007              server:[[args objectForKey:@"remoteID"] unsignedIntValue]
1008                path:[args objectForKey:@"remotePath"]
1009               token:[args objectForKey:@"remoteToken"]];
1010     }
1012     // Pass range of lines to select
1013     if ([args objectForKey:@"selectionRange"]) {
1014         NSRange selectionRange = NSRangeFromString(
1015                 [args objectForKey:@"selectionRange"]);
1016         [vc addVimInput:buildSelectRangeCommand(selectionRange)];
1017     }
1019     // Pass search text
1020     NSString *searchText = [args objectForKey:@"searchText"];
1021     if (searchText)
1022         [vc addVimInput:buildSearchTextCommand(searchText)];
1025 @end // MMAppController (Private)
1030 @implementation NSMenu (MMExtras)
1032 - (void)recurseSetAutoenablesItems:(BOOL)on
1034     [self setAutoenablesItems:on];
1036     int i, count = [self numberOfItems];
1037     for (i = 0; i < count; ++i) {
1038         NSMenuItem *item = [self itemAtIndex:i];
1039         [item setEnabled:YES];
1040         NSMenu *submenu = [item submenu];
1041         if (submenu) {
1042             [submenu recurseSetAutoenablesItems:on];
1043         }
1044     }
1047 @end  // NSMenu (MMExtras)
1052 @implementation NSNumber (MMExtras)
1053 - (int)tag
1055     return [self intValue];
1057 @end // NSNumber (MMExtras)