Add MMZoomBoth user default
[MacVim.git] / src / MacVim / MMAppController.m
blob05d2cd4c531f2ba0d49b9e2bb021c5378c882f08
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         [NSNumber numberWithBool:NO],   MMZoomBothKey,
119         nil];
121     [[NSUserDefaults standardUserDefaults] registerDefaults:dict];
123     NSArray *types = [NSArray arrayWithObject:NSStringPboardType];
124     [NSApp registerServicesMenuSendTypes:types returnTypes:types];
127 - (id)init
129     if ((self = [super init])) {
130         fontContainerRef = loadFonts();
132         vimControllers = [NSMutableArray new];
133         pidArguments = [NSMutableDictionary new];
135         // NOTE!  If the name of the connection changes here it must also be
136         // updated in MMBackend.m.
137         NSConnection *connection = [NSConnection defaultConnection];
138         NSString *name = [NSString stringWithFormat:@"%@-connection",
139                  [[NSBundle mainBundle] bundleIdentifier]];
140         //NSLog(@"Registering connection with name '%@'", name);
141         if ([connection registerName:name]) {
142             [connection setRequestTimeout:MMRequestTimeout];
143             [connection setReplyTimeout:MMReplyTimeout];
144             [connection setRootObject:self];
146             // NOTE: When the user is resizing the window the AppKit puts the
147             // run loop in event tracking mode.  Unless the connection listens
148             // to request in this mode, live resizing won't work.
149             [connection addRequestMode:NSEventTrackingRunLoopMode];
150         } else {
151             NSLog(@"WARNING: Failed to register connection with name '%@'",
152                     name);
153         }
154     }
156     return self;
159 - (void)dealloc
161     //NSLog(@"MMAppController dealloc");
163     [pidArguments release];  pidArguments = nil;
164     [vimControllers release];  vimControllers = nil;
165     [openSelectionString release];  openSelectionString = nil;
167     [super dealloc];
170 #if MM_HANDLE_XCODE_MOD_EVENT
171 - (void)applicationWillFinishLaunching:(NSNotification *)notification
173     [[NSAppleEventManager sharedAppleEventManager]
174             setEventHandler:self
175                 andSelector:@selector(handleXcodeModEvent:replyEvent:)
176               forEventClass:'KAHL'
177                  andEventID:'MOD '];
179 #endif
181 - (void)applicationDidFinishLaunching:(NSNotification *)notification
183     [NSApp setServicesProvider:self];
186 - (BOOL)applicationShouldOpenUntitledFile:(NSApplication *)sender
188     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
189     NSAppleEventManager *aem = [NSAppleEventManager sharedAppleEventManager];
190     NSAppleEventDescriptor *desc = [aem currentAppleEvent];
192     // The user default MMUntitledWindow can be set to control whether an
193     // untitled window should open on 'Open' and 'Reopen' events.
194     int untitledWindowFlag = [ud integerForKey:MMUntitledWindowKey];
195     if ([desc eventID] == kAEOpenApplication
196             && (untitledWindowFlag & MMUntitledWindowOnOpen) == 0)
197         return NO;
198     else if ([desc eventID] == kAEReopenApplication
199             && (untitledWindowFlag & MMUntitledWindowOnReopen) == 0)
200         return NO;
202     // When a process is started from the command line, the 'Open' event will
203     // contain a parameter to surpress the opening of an untitled window.
204     desc = [desc paramDescriptorForKeyword:keyAEPropData];
205     desc = [desc paramDescriptorForKeyword:keyMMUntitledWindow];
206     if (desc && ![desc booleanValue])
207         return NO;
209     // Never open an untitled window if there is at least one open window or if
210     // there are processes that are currently launching.
211     if ([vimControllers count] > 0 || [pidArguments count] > 0)
212         return NO;
214     // NOTE!  This way it possible to start the app with the command-line
215     // argument '-nowindow yes' and no window will be opened by default.
216     return ![ud boolForKey:MMNoWindowKey];
219 - (BOOL)applicationOpenUntitledFile:(NSApplication *)sender
221     [self newWindow:self];
222     return YES;
225 - (void)application:(NSApplication *)sender openFiles:(NSArray *)filenames
227     // Opening files works like this:
228     //  a) extract ODB/Xcode/Spotlight parameters from the current Apple event
229     //  b) filter out any already open files (see filterOpenFiles::)
230     //  c) open any remaining files
231     //
232     // A file is opened in an untitled window if there is one (it may be
233     // currently launching, or it may already be visible), otherwise a new
234     // window is opened.
235     //
236     // Each launching Vim process has a dictionary of arguments that are passed
237     // to the process when in checks in (via connectBackend:pid:).  The
238     // arguments for each launching process can be looked up by its PID (in the
239     // pidArguments dictionary).
241     NSMutableDictionary *arguments = [self extractArgumentsFromOdocEvent:
242             [[NSAppleEventManager sharedAppleEventManager] currentAppleEvent]];
244     // Filter out files that are already open
245     filenames = [self filterOpenFiles:filenames arguments:arguments];
247     // Open any files that remain
248     if ([filenames count]) {
249         MMVimController *vc;
250         BOOL openInTabs = [[NSUserDefaults standardUserDefaults]
251             boolForKey:MMOpenFilesInTabsKey];
253         [arguments setObject:filenames forKey:@"filenames"];
254         [arguments setObject:[NSNumber numberWithBool:YES] forKey:@"openFiles"];
256         if ((openInTabs && (vc = [self topmostVimController]))
257                || (vc = [self findUntitledWindow])) {
258             // Open files in an already open window.
259             [[[vc windowController] window] makeKeyAndOrderFront:self];
260             [self passArguments:arguments toVimController:vc];
261         } else {
262             // Open files in a launching Vim process or start a new process.
263             int pid = [self findLaunchingProcessWithoutArguments];
264             if (!pid) {
265                 // Pass the filenames to the process straight away.
266                 //
267                 // TODO: It would be nicer if all arguments were passed to the
268                 // Vim process in connectBackend::, but if we don't pass the
269                 // filename arguments here, the window 'flashes' once when it
270                 // opens.  This is due to the 'welcome' screen first being
271                 // displayed, then quickly thereafter the files are opened.
272                 NSArray *fileArgs = [NSArray arrayWithObject:@"-p"];
273                 fileArgs = [fileArgs arrayByAddingObjectsFromArray:filenames];
275                 pid = [self launchVimProcessWithArguments:fileArgs];
277                 // Make sure these files aren't opened again when
278                 // connectBackend:pid: is called.
279                 [arguments setObject:[NSNumber numberWithBool:NO]
280                               forKey:@"openFiles"];
281             }
283             // TODO: If the Vim process fails to start, or if it changes PID,
284             // then the memory allocated for these parameters will leak.
285             // Ensure that this cannot happen or somehow detect it.
287             if ([arguments count] > 0)
288                 [pidArguments setObject:arguments
289                                  forKey:[NSNumber numberWithInt:pid]];
290         }
291     }
293     [NSApp replyToOpenOrPrint:NSApplicationDelegateReplySuccess];
294     // NSApplicationDelegateReplySuccess = 0,
295     // NSApplicationDelegateReplyCancel = 1,
296     // NSApplicationDelegateReplyFailure = 2
299 - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender
301     return [[NSUserDefaults standardUserDefaults]
302             boolForKey:MMTerminateAfterLastWindowClosedKey];
305 - (NSApplicationTerminateReply)applicationShouldTerminate:
306     (NSApplication *)sender
308     // TODO: Follow Apple's guidelines for 'Graceful Application Termination'
309     // (in particular, allow user to review changes and save).
310     int reply = NSTerminateNow;
311     BOOL modifiedBuffers = NO;
313     // Go through windows, checking for modified buffers.  (Each Vim process
314     // tells MacVim when any buffer has been modified and MacVim sets the
315     // 'documentEdited' flag of the window correspondingly.)
316     NSEnumerator *e = [[NSApp windows] objectEnumerator];
317     id window;
318     while ((window = [e nextObject])) {
319         if ([window isDocumentEdited]) {
320             modifiedBuffers = YES;
321             break;
322         }
323     }
325     if (modifiedBuffers) {
326         NSAlert *alert = [[NSAlert alloc] init];
327         [alert setAlertStyle:NSWarningAlertStyle];
328         [alert addButtonWithTitle:@"Quit"];
329         [alert addButtonWithTitle:@"Cancel"];
330         [alert setMessageText:@"Quit without saving?"];
331         [alert setInformativeText:@"There are modified buffers, "
332             "if you quit now all changes will be lost.  Quit anyway?"];
334         if ([alert runModal] != NSAlertFirstButtonReturn)
335             reply = NSTerminateCancel;
337         [alert release];
338     } else {
339         // No unmodified buffers, but give a warning if there are multiple
340         // windows and/or tabs open.
341         int numWindows = [vimControllers count];
342         int numTabs = 0;
344         // Count the number of open tabs
345         e = [vimControllers objectEnumerator];
346         id vc;
347         while ((vc = [e nextObject])) {
348             NSString *eval = [vc evaluateVimExpression:@"tabpagenr('$')"];
349             if (eval) {
350                 int count = [eval intValue];
351                 if (count > 0 && count < INT_MAX)
352                     numTabs += count;
353             }
354         }
356         if (numWindows > 1 || numTabs > 1) {
357             NSAlert *alert = [[NSAlert alloc] init];
358             [alert setAlertStyle:NSWarningAlertStyle];
359             [alert addButtonWithTitle:@"Quit"];
360             [alert addButtonWithTitle:@"Cancel"];
361             [alert setMessageText:@"Are you sure you want to quit MacVim?"];
363             NSString *info = nil;
364             if (numWindows > 1) {
365                 if (numTabs > numWindows)
366                     info = [NSString stringWithFormat:@"There are %d windows "
367                         "open in MacVim, with a total of %d tabs. Do you want "
368                         "to quit anyway?", numWindows, numTabs];
369                 else
370                     info = [NSString stringWithFormat:@"There are %d windows "
371                         "open in MacVim. Do you want to quit anyway?",
372                         numWindows];
374             } else {
375                 info = [NSString stringWithFormat:@"There are %d tabs open "
376                     "in MacVim. Do you want to quit anyway?", numTabs];
377             }
379             [alert setInformativeText:info];
381             if ([alert runModal] != NSAlertFirstButtonReturn)
382                 reply = NSTerminateCancel;
384             [alert release];
385         }
386     }
389     // Tell all Vim processes to terminate now (otherwise they'll leave swap
390     // files behind).
391     if (NSTerminateNow == reply) {
392         e = [vimControllers objectEnumerator];
393         id vc;
394         while ((vc = [e nextObject]))
395             [vc sendMessage:TerminateNowMsgID data:nil];
396     }
398     return reply;
401 - (void)applicationWillTerminate:(NSNotification *)notification
403 #if MM_HANDLE_XCODE_MOD_EVENT
404     [[NSAppleEventManager sharedAppleEventManager]
405             removeEventHandlerForEventClass:'KAHL'
406                                  andEventID:'MOD '];
407 #endif
409     // This will invalidate all connections (since they were spawned from the
410     // default connection).
411     [[NSConnection defaultConnection] invalidate];
413     // Send a SIGINT to all running Vim processes, so that they are sure to
414     // receive the connectionDidDie: notification (a process has to be checking
415     // the run-loop for this to happen).
416     unsigned i, count = [vimControllers count];
417     for (i = 0; i < count; ++i) {
418         MMVimController *controller = [vimControllers objectAtIndex:i];
419         int pid = [controller pid];
420         if (pid > 0)
421             kill(pid, SIGINT);
422     }
424     if (fontContainerRef) {
425         ATSFontDeactivate(fontContainerRef, NULL, kATSOptionFlagsDefault);
426         fontContainerRef = 0;
427     }
429     [NSApp setDelegate:nil];
432 - (void)removeVimController:(id)controller
434     //NSLog(@"%s%@", _cmd, controller);
436     [[controller windowController] close];
438     [vimControllers removeObject:controller];
440     if (![vimControllers count]) {
441         // Turn on autoenabling of menus (because no Vim is open to handle it),
442         // but do not touch the MacVim menu.  Note that the menus must be
443         // enabled first otherwise autoenabling does not work.
444         NSMenu *mainMenu = [NSApp mainMenu];
445         int i, count = [mainMenu numberOfItems];
446         for (i = 1; i < count; ++i) {
447             NSMenuItem *item = [mainMenu itemAtIndex:i];
448             [item setEnabled:YES];
449             [[item submenu] recurseSetAutoenablesItems:YES];
450         }
451     }
454 - (void)windowControllerWillOpen:(MMWindowController *)windowController
456     NSPoint topLeft = NSZeroPoint;
457     NSWindow *keyWin = [NSApp keyWindow];
458     NSWindow *win = [windowController window];
460     if (!win) return;
462     // If there is a key window, cascade from it, otherwise use the autosaved
463     // window position (if any).
464     if (keyWin) {
465         NSRect frame = [keyWin frame];
466         topLeft = NSMakePoint(frame.origin.x, NSMaxY(frame));
467     } else {
468         NSString *topLeftString = [[NSUserDefaults standardUserDefaults]
469             stringForKey:MMTopLeftPointKey];
470         if (topLeftString)
471             topLeft = NSPointFromString(topLeftString);
472     }
474     if (!NSEqualPoints(topLeft, NSZeroPoint)) {
475         if (keyWin)
476             topLeft = [win cascadeTopLeftFromPoint:topLeft];
478         [win setFrameTopLeftPoint:topLeft];
479     }
481     if (openSelectionString) {
482         // TODO: Pass this as a parameter instead!  Get rid of
483         // 'openSelectionString' etc.
484         //
485         // There is some text to paste into this window as a result of the
486         // services menu "Open selection ..." being used.
487         [[windowController vimController] dropString:openSelectionString];
488         [openSelectionString release];
489         openSelectionString = nil;
490     }
493 - (IBAction)newWindow:(id)sender
495     [self launchVimProcessWithArguments:nil];
498 - (IBAction)fileOpen:(id)sender
500     NSOpenPanel *panel = [NSOpenPanel openPanel];
501     [panel setAllowsMultipleSelection:YES];
503     int result = [panel runModalForTypes:nil];
504     if (NSOKButton == result)
505         [self application:NSApp openFiles:[panel filenames]];
508 - (IBAction)selectNextWindow:(id)sender
510     unsigned i, count = [vimControllers count];
511     if (!count) return;
513     NSWindow *keyWindow = [NSApp keyWindow];
514     for (i = 0; i < count; ++i) {
515         MMVimController *vc = [vimControllers objectAtIndex:i];
516         if ([[[vc windowController] window] isEqual:keyWindow])
517             break;
518     }
520     if (i < count) {
521         if (++i >= count)
522             i = 0;
523         MMVimController *vc = [vimControllers objectAtIndex:i];
524         [[vc windowController] showWindow:self];
525     }
528 - (IBAction)selectPreviousWindow:(id)sender
530     unsigned i, count = [vimControllers count];
531     if (!count) return;
533     NSWindow *keyWindow = [NSApp keyWindow];
534     for (i = 0; i < count; ++i) {
535         MMVimController *vc = [vimControllers objectAtIndex:i];
536         if ([[[vc windowController] window] isEqual:keyWindow])
537             break;
538     }
540     if (i < count) {
541         if (i > 0) {
542             --i;
543         } else {
544             i = count - 1;
545         }
546         MMVimController *vc = [vimControllers objectAtIndex:i];
547         [[vc windowController] showWindow:self];
548     }
551 - (IBAction)fontSizeUp:(id)sender
553     [[NSFontManager sharedFontManager] modifyFont:
554             [NSNumber numberWithInt:NSSizeUpFontAction]];
557 - (IBAction)fontSizeDown:(id)sender
559     [[NSFontManager sharedFontManager] modifyFont:
560             [NSNumber numberWithInt:NSSizeDownFontAction]];
563 - (IBAction)orderFrontPreferencePanel:(id)sender
565     [[MMPreferenceController sharedPreferenceController] showWindow:self];
568 - (byref id <MMFrontendProtocol>)
569     connectBackend:(byref in id <MMBackendProtocol>)backend
570                pid:(int)pid
572     //NSLog(@"Connect backend (pid=%d)", pid);
573     NSNumber *pidKey = [NSNumber numberWithInt:pid];
574     MMVimController *vc = nil;
576     @try {
577         [(NSDistantObject*)backend
578                 setProtocolForProxy:@protocol(MMBackendProtocol)];
580         vc = [[[MMVimController alloc]
581                 initWithBackend:backend pid:pid] autorelease];
583         if (![vimControllers count]) {
584             // The first window autosaves its position.  (The autosaving
585             // features of Cocoa are not used because we need more control over
586             // what is autosaved and when it is restored.)
587             [[vc windowController] setWindowAutosaveKey:MMTopLeftPointKey];
588         }
590         [vimControllers addObject:vc];
592         id args = [pidArguments objectForKey:pidKey];
593         if (args && [NSNull null] != args)
594             [self passArguments:args toVimController:vc];
596         // HACK!  MacVim does not get activated if it is launched from the
597         // terminal, so we forcibly activate here unless it is an untitled
598         // window opening.  Untitled windows are treated differently, else
599         // MacVim would steal the focus if another app was activated while the
600         // untitled window was loading.
601         if (!args || args != [NSNull null])
602             [NSApp activateIgnoringOtherApps:YES];
604         if (args)
605             [pidArguments removeObjectForKey:pidKey];
607         return vc;
608     }
610     @catch (NSException *e) {
611         NSLog(@"Exception caught in %s: \"%@\"", _cmd, e);
613         if (vc)
614             [vimControllers removeObject:vc];
616         [pidArguments removeObjectForKey:pidKey];
617     }
619     return nil;
622 - (NSArray *)serverList
624     NSMutableArray *array = [NSMutableArray array];
626     unsigned i, count = [vimControllers count];
627     for (i = 0; i < count; ++i) {
628         MMVimController *controller = [vimControllers objectAtIndex:i];
629         if ([controller serverName])
630             [array addObject:[controller serverName]];
631     }
633     return array;
636 @end // MMAppController
641 @implementation MMAppController (MMServices)
643 - (void)openSelection:(NSPasteboard *)pboard userData:(NSString *)userData
644                 error:(NSString **)error
646     if (![[pboard types] containsObject:NSStringPboardType]) {
647         NSLog(@"WARNING: Pasteboard contains no object of type "
648                 "NSStringPboardType");
649         return;
650     }
652     MMVimController *vc = [self topmostVimController];
653     if (vc) {
654         // Open a new tab first, since dropString: does not do this.
655         [vc sendMessage:AddNewTabMsgID data:nil];
656         [vc dropString:[pboard stringForType:NSStringPboardType]];
657     } else {
658         // NOTE: There is no window to paste the selection into, so save the
659         // text, open a new window, and paste the text when the next window
660         // opens.  (If this is called several times in a row, then all but the
661         // last call might be ignored.)
662         if (openSelectionString) [openSelectionString release];
663         openSelectionString = [[pboard stringForType:NSStringPboardType] copy];
665         [self newWindow:self];
666     }
669 - (void)openFile:(NSPasteboard *)pboard userData:(NSString *)userData
670            error:(NSString **)error
672     if (![[pboard types] containsObject:NSStringPboardType]) {
673         NSLog(@"WARNING: Pasteboard contains no object of type "
674                 "NSStringPboardType");
675         return;
676     }
678     // TODO: Parse multiple filenames and create array with names.
679     NSString *string = [pboard stringForType:NSStringPboardType];
680     string = [string stringByTrimmingCharactersInSet:
681             [NSCharacterSet whitespaceAndNewlineCharacterSet]];
682     string = [string stringByStandardizingPath];
684     NSArray *filenames = [self filterFilesAndNotify:
685             [NSArray arrayWithObject:string]];
686     if ([filenames count] > 0) {
687         MMVimController *vc = nil;
688         if (userData && [userData isEqual:@"Tab"])
689             vc = [self topmostVimController];
691         if (vc) {
692             [vc dropFiles:filenames forceOpen:YES];
693         } else {
694             [self application:NSApp openFiles:filenames];
695         }
696     }
699 @end // MMAppController (MMServices)
704 @implementation MMAppController (Private)
706 - (MMVimController *)keyVimController
708     NSWindow *keyWindow = [NSApp keyWindow];
709     if (keyWindow) {
710         unsigned i, count = [vimControllers count];
711         for (i = 0; i < count; ++i) {
712             MMVimController *vc = [vimControllers objectAtIndex:i];
713             if ([[[vc windowController] window] isEqual:keyWindow])
714                 return vc;
715         }
716     }
718     return nil;
721 - (MMVimController *)topmostVimController
723     NSArray *windows = [NSApp orderedWindows];
724     if ([windows count] > 0) {
725         NSWindow *window = [windows objectAtIndex:0];
726         unsigned i, count = [vimControllers count];
727         for (i = 0; i < count; ++i) {
728             MMVimController *vc = [vimControllers objectAtIndex:i];
729             if ([[[vc windowController] window] isEqual:window])
730                 return vc;
731         }
732     }
734     return nil;
737 - (int)launchVimProcessWithArguments:(NSArray *)args
739     NSString *taskPath = nil;
740     NSArray *taskArgs = nil;
741     NSString *path = [[NSBundle mainBundle] pathForAuxiliaryExecutable:@"Vim"];
743     if (!path) {
744         NSLog(@"ERROR: Vim executable could not be found inside app bundle!");
745         return 0;
746     }
748     if ([[NSUserDefaults standardUserDefaults] boolForKey:MMLoginShellKey]) {
749         // Run process with a login shell
750         //   $SHELL -l -c "exec Vim -g -f args"
751         // (-g for GUI, -f for foreground, i.e. don't fork)
753         NSMutableString *execArg = [NSMutableString
754             stringWithFormat:@"exec \"%@\" -g -f", path];
755         if (args) {
756             // Append all arguments while making sure that arguments containing
757             // spaces are enclosed in quotes.
758             NSCharacterSet *space = [NSCharacterSet whitespaceCharacterSet];
759             unsigned i, count = [args count];
761             for (i = 0; i < count; ++i) {
762                 NSString *arg = [args objectAtIndex:i];
763                 if (NSNotFound != [arg rangeOfCharacterFromSet:space].location)
764                     [execArg appendFormat:@" \"%@\"", arg];
765                 else
766                     [execArg appendFormat:@" %@", arg];
767             }
768         }
770         // Launch the process with a login shell so that users environment
771         // settings get sourced.  This does not always happen when MacVim is
772         // started.
773         taskArgs = [NSArray arrayWithObjects:@"-l", @"-c", execArg, nil];
774         taskPath = [[[NSProcessInfo processInfo] environment]
775             objectForKey:@"SHELL"];
776         if (!taskPath)
777             taskPath = @"/bin/sh";
778     } else {
779         // Run process directly:
780         //   Vim -g -f args
781         // (-g for GUI, -f for foreground, i.e. don't fork)
782         taskPath = path;
783         taskArgs = [NSArray arrayWithObjects:@"-g", @"-f", nil];
784         if (args)
785             taskArgs = [taskArgs arrayByAddingObjectsFromArray:args];
786     }
788     NSTask *task =[NSTask launchedTaskWithLaunchPath:taskPath
789                                            arguments:taskArgs];
790     //NSLog(@"launch %@ with args=%@ (pid=%d)", taskPath, taskArgs,
791     //        [task processIdentifier]);
793     int pid = [task processIdentifier];
795     // If the process has no arguments, then add a null argument to the
796     // pidArguments dictionary.  This is later used to detect that a process
797     // without arguments is being launched.
798     if (!args)
799         [pidArguments setObject:[NSNull null]
800                          forKey:[NSNumber numberWithInt:pid]];
802     return pid;
805 - (NSArray *)filterFilesAndNotify:(NSArray *)filenames
807     // Go trough 'filenames' array and make sure each file exists.  Present
808     // warning dialog if some file was missing.
810     NSString *firstMissingFile = nil;
811     NSMutableArray *files = [NSMutableArray array];
812     unsigned i, count = [filenames count];
814     for (i = 0; i < count; ++i) {
815         NSString *name = [filenames objectAtIndex:i];
816         if ([[NSFileManager defaultManager] fileExistsAtPath:name]) {
817             [files addObject:name];
818         } else if (!firstMissingFile) {
819             firstMissingFile = name;
820         }
821     }
823     if (firstMissingFile) {
824         NSAlert *alert = [[NSAlert alloc] init];
825         [alert addButtonWithTitle:@"OK"];
827         NSString *text;
828         if ([files count] >= count-1) {
829             [alert setMessageText:@"File not found"];
830             text = [NSString stringWithFormat:@"Could not open file with "
831                 "name %@.", firstMissingFile];
832         } else {
833             [alert setMessageText:@"Multiple files not found"];
834             text = [NSString stringWithFormat:@"Could not open file with "
835                 "name %@, and %d other files.", firstMissingFile,
836                 count-[files count]-1];
837         }
839         [alert setInformativeText:text];
840         [alert setAlertStyle:NSWarningAlertStyle];
842         [alert runModal];
843         [alert release];
845         [NSApp replyToOpenOrPrint:NSApplicationDelegateReplyFailure];
846     }
848     return files;
851 - (NSArray *)filterOpenFiles:(NSArray *)filenames
852                    arguments:(NSDictionary *)args
854     // Check if any of the files in the 'filenames' array are open in any Vim
855     // process.  Remove the files that are open from the 'filenames' array and
856     // return it.  If all files were filtered out, then raise the first file in
857     // the Vim process it is open.  Files that are filtered are sent an odb
858     // open event in case theID is not zero.
860     NSMutableDictionary *localArgs =
861             [NSMutableDictionary dictionaryWithDictionary:args];
862     MMVimController *raiseController = nil;
863     NSString *raiseFile = nil;
864     NSMutableArray *files = [filenames mutableCopy];
865     NSString *expr = [NSString stringWithFormat:
866             @"map([\"%@\"],\"bufloaded(v:val)\")",
867             [files componentsJoinedByString:@"\",\""]];
868     unsigned i, count = [vimControllers count];
870     // Ensure that the files aren't opened when passing arguments.
871     [localArgs setObject:[NSNumber numberWithBool:NO] forKey:@"openFiles"];
873     for (i = 0; i < count && [files count]; ++i) {
874         MMVimController *controller = [vimControllers objectAtIndex:i];
876         // Query Vim for which files in the 'files' array are open.
877         NSString *eval = [controller evaluateVimExpression:expr];
878         if (!eval) continue;
880         NSIndexSet *idxSet = [NSIndexSet indexSetWithVimList:eval];
881         if ([idxSet count]) {
882             if (!raiseFile) {
883                 // Remember the file and which Vim that has it open so that
884                 // we can raise it later on.
885                 raiseController = controller;
886                 raiseFile = [files objectAtIndex:[idxSet firstIndex]];
887                 [[raiseFile retain] autorelease];
888             }
890             // Pass (ODB/Xcode/Spotlight) arguments to this process.
891             [localArgs setObject:[files objectsAtIndexes:idxSet]
892                           forKey:@"filenames"];
893             [self passArguments:localArgs toVimController:controller];
895             // Remove all the files that were open in this Vim process and
896             // create a new expression to evaluate.
897             [files removeObjectsAtIndexes:idxSet];
898             expr = [NSString stringWithFormat:
899                     @"map([\"%@\"],\"bufloaded(v:val)\")",
900                     [files componentsJoinedByString:@"\",\""]];
901         }
902     }
904     if (![files count] && raiseFile) {
905         // Raise the window containing the first file that was already open,
906         // and make sure that the tab containing that file is selected.  Only
907         // do this if there are no more files to open, otherwise sometimes the
908         // window with 'raiseFile' will be raised, other times it might be the
909         // window that will open with the files in the 'files' array.
910         raiseFile = [raiseFile stringByEscapingSpecialFilenameCharacters];
911         NSString *input = [NSString stringWithFormat:@"<C-\\><C-N>"
912             ":let oldswb=&swb|let &swb=\"useopen,usetab\"|"
913             "tab sb %@|let &swb=oldswb|unl oldswb|"
914             "cal foreground()|redr|f<CR>", raiseFile];
916         [raiseController addVimInput:input];
917     }
919     return files;
922 #if MM_HANDLE_XCODE_MOD_EVENT
923 - (void)handleXcodeModEvent:(NSAppleEventDescriptor *)event
924                  replyEvent:(NSAppleEventDescriptor *)reply
926 #if 0
927     // Xcode sends this event to query MacVim which open files have been
928     // modified.
929     NSLog(@"reply:%@", reply);
930     NSLog(@"event:%@", event);
932     NSEnumerator *e = [vimControllers objectEnumerator];
933     id vc;
934     while ((vc = [e nextObject])) {
935         DescType type = [reply descriptorType];
936         unsigned len = [[type data] length];
937         NSMutableData *data = [NSMutableData data];
939         [data appendBytes:&type length:sizeof(DescType)];
940         [data appendBytes:&len length:sizeof(unsigned)];
941         [data appendBytes:[reply data] length:len];
943         [vc sendMessage:XcodeModMsgID data:data];
944     }
945 #endif
947 #endif
949 - (int)findLaunchingProcessWithoutArguments
951     NSArray *keys = [pidArguments allKeysForObject:[NSNull null]];
952     if ([keys count] > 0) {
953         //NSLog(@"found launching process without arguments");
954         return [[keys objectAtIndex:0] intValue];
955     }
957     return 0;
960 - (MMVimController *)findUntitledWindow
962     NSEnumerator *e = [vimControllers objectEnumerator];
963     id vc;
964     while ((vc = [e nextObject])) {
965         // TODO: This is a moronic test...should query the Vim process if there
966         // are any open buffers or something like that instead.
967         NSString *title = [[[vc windowController] window] title];
968         if ([title hasPrefix:@"[No Name] - VIM"]) {
969             //NSLog(@"found untitled window");
970             return vc;
971         }
972     }
974     return nil;
977 - (NSMutableDictionary *)extractArgumentsFromOdocEvent:
978     (NSAppleEventDescriptor *)desc
980     NSMutableDictionary *dict = [NSMutableDictionary dictionary];
982     // 1. Extract ODB parameters (if any)
983     NSAppleEventDescriptor *odbdesc = desc;
984     if (![odbdesc paramDescriptorForKeyword:keyFileSender]) {
985         // The ODB paramaters may hide inside the 'keyAEPropData' descriptor.
986         odbdesc = [odbdesc paramDescriptorForKeyword:keyAEPropData];
987         if (![odbdesc paramDescriptorForKeyword:keyFileSender])
988             odbdesc = nil;
989     }
991     if (odbdesc) {
992         NSAppleEventDescriptor *p =
993                 [odbdesc paramDescriptorForKeyword:keyFileSender];
994         if (p)
995             [dict setObject:[NSNumber numberWithUnsignedInt:[p typeCodeValue]]
996                      forKey:@"remoteID"];
998         p = [odbdesc paramDescriptorForKeyword:keyFileCustomPath];
999         if (p)
1000             [dict setObject:[p stringValue] forKey:@"remotePath"];
1002         p = [odbdesc paramDescriptorForKeyword:keyFileSenderToken];
1003         if (p)
1004             [dict setObject:p forKey:@"remotePath"];
1005     }
1007     // 2. Extract Xcode parameters (if any)
1008     NSAppleEventDescriptor *xcodedesc =
1009             [desc paramDescriptorForKeyword:keyAEPosition];
1010     if (xcodedesc) {
1011         NSRange range;
1012         MMSelectionRange *sr = (MMSelectionRange*)[[xcodedesc data] bytes];
1014         if (sr->lineNum < 0) {
1015             // Should select a range of lines.
1016             range.location = sr->startRange + 1;
1017             range.length = sr->endRange - sr->startRange + 1;
1018         } else {
1019             // Should only move cursor to a line.
1020             range.location = sr->lineNum + 1;
1021             range.length = 0;
1022         }
1024         [dict setObject:NSStringFromRange(range) forKey:@"selectionRange"];
1025     }
1027     // 3. Extract Spotlight search text (if any)
1028     NSAppleEventDescriptor *spotlightdesc = 
1029             [desc paramDescriptorForKeyword:keyAESearchText];
1030     if (spotlightdesc)
1031         [dict setObject:[spotlightdesc stringValue] forKey:@"searchText"];
1033     return dict;
1036 - (void)passArguments:(NSDictionary *)args toVimController:(MMVimController*)vc
1038     if (!args) return;
1040     // Pass filenames to open if required (the 'openFiles' argument can be used
1041     // to disallow opening of the files).
1042     NSArray *filenames = [args objectForKey:@"filenames"];
1043     if (filenames && [[args objectForKey:@"openFiles"] boolValue]) {
1044         NSString *tabDrop = buildTabDropCommand(filenames);
1045         [vc addVimInput:tabDrop];
1046     }
1048     // Pass ODB data
1049     if (filenames && [args objectForKey:@"remoteID"]) {
1050         [vc odbEdit:filenames
1051              server:[[args objectForKey:@"remoteID"] unsignedIntValue]
1052                path:[args objectForKey:@"remotePath"]
1053               token:[args objectForKey:@"remoteToken"]];
1054     }
1056     // Pass range of lines to select
1057     if ([args objectForKey:@"selectionRange"]) {
1058         NSRange selectionRange = NSRangeFromString(
1059                 [args objectForKey:@"selectionRange"]);
1060         [vc addVimInput:buildSelectRangeCommand(selectionRange)];
1061     }
1063     // Pass search text
1064     NSString *searchText = [args objectForKey:@"searchText"];
1065     if (searchText)
1066         [vc addVimInput:buildSearchTextCommand(searchText)];
1069 @end // MMAppController (Private)
1074 @implementation NSMenu (MMExtras)
1076 - (void)recurseSetAutoenablesItems:(BOOL)on
1078     [self setAutoenablesItems:on];
1080     int i, count = [self numberOfItems];
1081     for (i = 0; i < count; ++i) {
1082         NSMenuItem *item = [self itemAtIndex:i];
1083         [item setEnabled:YES];
1084         NSMenu *submenu = [item submenu];
1085         if (submenu) {
1086             [submenu recurseSetAutoenablesItems:on];
1087         }
1088     }
1091 @end  // NSMenu (MMExtras)
1096 @implementation NSNumber (MMExtras)
1097 - (int)tag
1099     return [self intValue];
1101 @end // NSNumber (MMExtras)