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