Use Launch Services, user default for untitled windows
[MacVim.git] / src / MacVim / MMAppController.m
blob6dd5b5969e3e7741f11d3b183f104d0dd9f03ebe
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         nil];
117     [[NSUserDefaults standardUserDefaults] registerDefaults:dict];
119     NSArray *types = [NSArray arrayWithObject:NSStringPboardType];
120     [NSApp registerServicesMenuSendTypes:types returnTypes:types];
123 - (id)init
125     if ((self = [super init])) {
126         fontContainerRef = loadFonts();
128         vimControllers = [NSMutableArray new];
129         pidArguments = [NSMutableDictionary new];
131         // NOTE!  If the name of the connection changes here it must also be
132         // updated in MMBackend.m.
133         NSConnection *connection = [NSConnection defaultConnection];
134         NSString *name = [NSString stringWithFormat:@"%@-connection",
135                  [[NSBundle mainBundle] bundleIdentifier]];
136         //NSLog(@"Registering connection with name '%@'", name);
137         if ([connection registerName:name]) {
138             [connection setRequestTimeout:MMRequestTimeout];
139             [connection setReplyTimeout:MMReplyTimeout];
140             [connection setRootObject:self];
142             // NOTE: When the user is resizing the window the AppKit puts the
143             // run loop in event tracking mode.  Unless the connection listens
144             // to request in this mode, live resizing won't work.
145             [connection addRequestMode:NSEventTrackingRunLoopMode];
146         } else {
147             NSLog(@"WARNING: Failed to register connection with name '%@'",
148                     name);
149         }
150     }
152     return self;
155 - (void)dealloc
157     //NSLog(@"MMAppController dealloc");
159     [pidArguments release];  pidArguments = nil;
160     [vimControllers release];  vimControllers = nil;
161     [openSelectionString release];  openSelectionString = nil;
163     [super dealloc];
166 #if MM_HANDLE_XCODE_MOD_EVENT
167 - (void)applicationWillFinishLaunching:(NSNotification *)notification
169     [[NSAppleEventManager sharedAppleEventManager]
170             setEventHandler:self
171                 andSelector:@selector(handleXcodeModEvent:replyEvent:)
172               forEventClass:'KAHL'
173                  andEventID:'MOD '];
175 #endif
177 - (void)applicationDidFinishLaunching:(NSNotification *)notification
179     [NSApp setServicesProvider:self];
182 - (BOOL)applicationShouldOpenUntitledFile:(NSApplication *)sender
184     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
185     NSAppleEventManager *aem = [NSAppleEventManager sharedAppleEventManager];
186     NSAppleEventDescriptor *desc = [aem currentAppleEvent];
188     // The user default MMUntitledWindow can be set to control whether an
189     // untitled window should open on 'Open' and 'Reopen' events.
190     int untitledWindowFlag = [ud integerForKey:MMUntitledWindowKey];
191     if ([desc eventID] == kAEOpenApplication
192             && (untitledWindowFlag & MMUntitledWindowOnOpen) == 0)
193         return NO;
194     else if ([desc eventID] == kAEReopenApplication
195             && (untitledWindowFlag & MMUntitledWindowOnReopen) == 0)
196         return NO;
198     // When a process is started from the command line, the 'Open' event will
199     // contain a parameter to surpress the opening of an untitled window.
200     desc = [desc paramDescriptorForKeyword:keyAEPropData];
201     desc = [desc paramDescriptorForKeyword:keyMMUntitledWindow];
202     if (desc && ![desc booleanValue])
203         return NO;
205     // Never open an untitled window if there is at least one open window or if
206     // there are processes that are currently launching.
207     if ([vimControllers count] > 0 || [pidArguments count] > 0)
208         return NO;
210     // NOTE!  This way it possible to start the app with the command-line
211     // argument '-nowindow yes' and no window will be opened by default.
212     return ![ud boolForKey:MMNoWindowKey];
215 - (BOOL)applicationOpenUntitledFile:(NSApplication *)sender
217     [self newWindow:self];
218     return YES;
221 - (void)application:(NSApplication *)sender openFiles:(NSArray *)filenames
223     // Opening files works like this:
224     //  a) extract ODB and/or Xcode parameters from the current Apple event
225     //  b) filter out any already open files (see filterOpenFiles:::::)
226     //  c) open any remaining files
227     //
228     // A file is opened in an untitled window if there is one (it may be
229     // currently launching, or it may already be visible), otherwise a new
230     // window is opened.
231     //
232     // Each launching Vim process has a dictionary of arguments that are passed
233     // to the process when in checks in (via connectBackend:pid:).  The
234     // arguments for each launching process can be looked up by its PID (in the
235     // pidArguments dictionary).
237     OSType remoteID = 0;
238     NSString *remotePath = nil;
239     NSAppleEventManager *aem = nil;
240     NSAppleEventDescriptor *remoteToken = nil;
241     NSAppleEventDescriptor *odbdesc = nil;
242     NSAppleEventDescriptor *xcodedesc = nil;
243     NSRange selectionRange = { NSNotFound, 0 };
245     // 1. Extract ODB parameters (if any)
246     aem = [NSAppleEventManager sharedAppleEventManager];
247     odbdesc = [aem currentAppleEvent];
248     if (![odbdesc paramDescriptorForKeyword:keyFileSender]) {
249         // The ODB paramaters may hide inside the 'keyAEPropData' descriptor.
250         odbdesc = [odbdesc paramDescriptorForKeyword:keyAEPropData];
251         if (![odbdesc paramDescriptorForKeyword:keyFileSender])
252             odbdesc = nil;
253     }
255     if (odbdesc) {
256         remoteID = [[odbdesc paramDescriptorForKeyword:keyFileSender]
257                 typeCodeValue];
258         remotePath = [[odbdesc paramDescriptorForKeyword:keyFileCustomPath]
259                 stringValue];
260         remoteToken = [[odbdesc paramDescriptorForKeyword:keyFileSenderToken]
261                 copy];
262     }
264     // 2. Extract Xcode parameters (if any)
265     xcodedesc = [[aem currentAppleEvent]
266             paramDescriptorForKeyword:keyAEPosition];
267     if (xcodedesc) {
268         MMSelectionRange *sr = (MMSelectionRange*)[[xcodedesc data] bytes];
269         if (sr->lineNum < 0) {
270             // Should select a range of lines.
271             selectionRange.location = sr->startRange + 1;
272             selectionRange.length = sr->endRange - sr->startRange + 1;
273         } else {
274             // Should only move cursor to a line.
275             selectionRange.location = sr->lineNum + 1;
276         }
277     }
279     // 3. Filter out files that are already open
280     filenames = [self filterOpenFiles:filenames remote:remoteID path:remotePath
281                                 token:remoteToken
282                        selectionRange:selectionRange];
284     // 4. Open any files that remain
285     if ([filenames count]) {
286         MMVimController *vc;
287         BOOL openInTabs = [[NSUserDefaults standardUserDefaults]
288             boolForKey:MMOpenFilesInTabsKey];
290         if ((openInTabs && (vc = [self topmostVimController]))
291                || (vc = [self findUntitledWindow])) {
293             // 5-1 Open files in an already open window (either due to the fact
294             // that MMMOpenFilesInTabs was set or because there was already an
295             // untitled Vim process open).
297             [vc dropFiles:filenames forceOpen:YES];
298             if (odbdesc)
299                 [vc odbEdit:filenames server:remoteID path:remotePath
300                       token:remoteToken];
301             if (selectionRange.location != NSNotFound)
302                 [vc addVimInput:buildSelectRangeCommand(selectionRange)];
303         } else {
305             // 5-2. Open files
306             //   a) in a launching Vim that has no arguments, if there is one
307             // else
308             //   b) launch a new Vim process and open files there.
310             NSMutableDictionary *argDict = [NSMutableDictionary dictionary];
312             int pid = [self findLaunchingProcessWithoutArguments];
313             if (pid) {
314                 // The filenames are passed as arguments in connectBackend::.
315                 [argDict setObject:filenames forKey:@"filenames"];
316             } else {
317                 // Pass the filenames to the process straight away.
318                 //
319                 // TODO: It would be nicer if all arguments were passed to the
320                 // Vim process in connectBackend::, but if we don't pass the
321                 // filename arguments here, the window 'flashes' once when it
322                 // opens.  This is due to the 'welcome' screen first being
323                 // displayed, then quickly thereafter the files are opened.
324                 NSArray *fileArgs = [NSArray arrayWithObject:@"-p"];
325                 fileArgs = [fileArgs arrayByAddingObjectsFromArray:filenames];
327                 pid = [self launchVimProcessWithArguments:fileArgs];
328             }
330             // TODO: If the Vim process fails to start, or if it changes PID,
331             // then the memory allocated for these parameters will leak.
332             // Ensure that this cannot happen or somehow detect it.
334             if (odbdesc) {
335                 [argDict setObject:filenames forKey:@"remoteFiles"];
337                 // The remote token can be arbitrary data so it is cannot
338                 // (without encoding it as text) be passed on the command line.
339                 [argDict setObject:[NSNumber numberWithUnsignedInt:remoteID]
340                             forKey:@"remoteID"];
341                 if (remotePath)
342                     [argDict setObject:remotePath forKey:@"remotePath"];
343                 if (remoteToken)
344                     [argDict setObject:remoteToken forKey:@"remoteToken"];
345             }
347             if (selectionRange.location != NSNotFound)
348                 [argDict setObject:NSStringFromRange(selectionRange)
349                             forKey:@"selectionRange"];
351             if ([argDict count] > 0)
352                 [pidArguments setObject:argDict
353                                  forKey:[NSNumber numberWithInt:pid]];
354         }
355     }
357     [NSApp replyToOpenOrPrint:NSApplicationDelegateReplySuccess];
358     // NSApplicationDelegateReplySuccess = 0,
359     // NSApplicationDelegateReplyCancel = 1,
360     // NSApplicationDelegateReplyFailure = 2
363 - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender
365     return [[NSUserDefaults standardUserDefaults]
366             boolForKey:MMTerminateAfterLastWindowClosedKey];
369 - (NSApplicationTerminateReply)applicationShouldTerminate:
370     (NSApplication *)sender
372     // TODO: Follow Apple's guidelines for 'Graceful Application Termination'
373     // (in particular, allow user to review changes and save).
374     int reply = NSTerminateNow;
375     BOOL modifiedBuffers = NO;
377     // Go through windows, checking for modified buffers.  (Each Vim process
378     // tells MacVim when any buffer has been modified and MacVim sets the
379     // 'documentEdited' flag of the window correspondingly.)
380     NSEnumerator *e = [[NSApp windows] objectEnumerator];
381     id window;
382     while ((window = [e nextObject])) {
383         if ([window isDocumentEdited]) {
384             modifiedBuffers = YES;
385             break;
386         }
387     }
389     if (modifiedBuffers) {
390         NSAlert *alert = [[NSAlert alloc] init];
391         [alert addButtonWithTitle:@"Quit"];
392         [alert addButtonWithTitle:@"Cancel"];
393         [alert setMessageText:@"Quit without saving?"];
394         [alert setInformativeText:@"There are modified buffers, "
395             "if you quit now all changes will be lost.  Quit anyway?"];
396         [alert setAlertStyle:NSWarningAlertStyle];
398         if ([alert runModal] != NSAlertFirstButtonReturn)
399             reply = NSTerminateCancel;
401         [alert release];
402     }
404     // Tell all Vim processes to terminate now (otherwise they'll leave swap
405     // files behind).
406     if (NSTerminateNow == reply) {
407         e = [vimControllers objectEnumerator];
408         id vc;
409         while ((vc = [e nextObject]))
410             [vc sendMessage:TerminateNowMsgID data:nil];
411     }
413     return reply;
416 - (void)applicationWillTerminate:(NSNotification *)notification
418 #if MM_HANDLE_XCODE_MOD_EVENT
419     [[NSAppleEventManager sharedAppleEventManager]
420             removeEventHandlerForEventClass:'KAHL'
421                                  andEventID:'MOD '];
422 #endif
424     // This will invalidate all connections (since they were spawned from the
425     // default connection).
426     [[NSConnection defaultConnection] invalidate];
428     // Send a SIGINT to all running Vim processes, so that they are sure to
429     // receive the connectionDidDie: notification (a process has to be checking
430     // the run-loop for this to happen).
431     unsigned i, count = [vimControllers count];
432     for (i = 0; i < count; ++i) {
433         MMVimController *controller = [vimControllers objectAtIndex:i];
434         int pid = [controller pid];
435         if (pid > 0)
436             kill(pid, SIGINT);
437     }
439     if (fontContainerRef) {
440         ATSFontDeactivate(fontContainerRef, NULL, kATSOptionFlagsDefault);
441         fontContainerRef = 0;
442     }
444     // TODO: Is this a correct way of releasing the MMAppController?
445     // (It doesn't seem like dealloc is ever called.)
446     [NSApp setDelegate:nil];
447     [self autorelease];
450 - (void)removeVimController:(id)controller
452     //NSLog(@"%s%@", _cmd, controller);
454     [[controller windowController] close];
456     [vimControllers removeObject:controller];
458     if (![vimControllers count]) {
459         // Turn on autoenabling of menus (because no Vim is open to handle it),
460         // but do not touch the MacVim menu.  Note that the menus must be
461         // enabled first otherwise autoenabling does not work.
462         NSMenu *mainMenu = [NSApp mainMenu];
463         int i, count = [mainMenu numberOfItems];
464         for (i = 1; i < count; ++i) {
465             NSMenuItem *item = [mainMenu itemAtIndex:i];
466             [item setEnabled:YES];
467             [[item submenu] recurseSetAutoenablesItems:YES];
468         }
469     }
472 - (void)windowControllerWillOpen:(MMWindowController *)windowController
474     NSPoint topLeft = NSZeroPoint;
475     NSWindow *keyWin = [NSApp keyWindow];
476     NSWindow *win = [windowController window];
478     if (!win) return;
480     // If there is a key window, cascade from it, otherwise use the autosaved
481     // window position (if any).
482     if (keyWin) {
483         NSRect frame = [keyWin frame];
484         topLeft = NSMakePoint(frame.origin.x, NSMaxY(frame));
485     } else {
486         NSString *topLeftString = [[NSUserDefaults standardUserDefaults]
487             stringForKey:MMTopLeftPointKey];
488         if (topLeftString)
489             topLeft = NSPointFromString(topLeftString);
490     }
492     if (!NSEqualPoints(topLeft, NSZeroPoint)) {
493         if (keyWin)
494             topLeft = [win cascadeTopLeftFromPoint:topLeft];
496         [win setFrameTopLeftPoint:topLeft];
497     }
499     if (openSelectionString) {
500         // TODO: Pass this as a parameter instead!  Get rid of
501         // 'openSelectionString' etc.
502         //
503         // There is some text to paste into this window as a result of the
504         // services menu "Open selection ..." being used.
505         [[windowController vimController] dropString:openSelectionString];
506         [openSelectionString release];
507         openSelectionString = nil;
508     }
511 - (IBAction)newWindow:(id)sender
513     [self launchVimProcessWithArguments:nil];
516 - (IBAction)fileOpen:(id)sender
518     NSOpenPanel *panel = [NSOpenPanel openPanel];
519     [panel setAllowsMultipleSelection:YES];
521     int result = [panel runModalForTypes:nil];
522     if (NSOKButton == result)
523         [self application:NSApp openFiles:[panel filenames]];
526 - (IBAction)selectNextWindow:(id)sender
528     unsigned i, count = [vimControllers count];
529     if (!count) return;
531     NSWindow *keyWindow = [NSApp keyWindow];
532     for (i = 0; i < count; ++i) {
533         MMVimController *vc = [vimControllers objectAtIndex:i];
534         if ([[[vc windowController] window] isEqual:keyWindow])
535             break;
536     }
538     if (i < count) {
539         if (++i >= count)
540             i = 0;
541         MMVimController *vc = [vimControllers objectAtIndex:i];
542         [[vc windowController] showWindow:self];
543     }
546 - (IBAction)selectPreviousWindow:(id)sender
548     unsigned i, count = [vimControllers count];
549     if (!count) return;
551     NSWindow *keyWindow = [NSApp keyWindow];
552     for (i = 0; i < count; ++i) {
553         MMVimController *vc = [vimControllers objectAtIndex:i];
554         if ([[[vc windowController] window] isEqual:keyWindow])
555             break;
556     }
558     if (i < count) {
559         if (i > 0) {
560             --i;
561         } else {
562             i = count - 1;
563         }
564         MMVimController *vc = [vimControllers objectAtIndex:i];
565         [[vc windowController] showWindow:self];
566     }
569 - (IBAction)fontSizeUp:(id)sender
571     [[NSFontManager sharedFontManager] modifyFont:
572             [NSNumber numberWithInt:NSSizeUpFontAction]];
575 - (IBAction)fontSizeDown:(id)sender
577     [[NSFontManager sharedFontManager] modifyFont:
578             [NSNumber numberWithInt:NSSizeDownFontAction]];
581 - (byref id <MMFrontendProtocol>)
582     connectBackend:(byref in id <MMBackendProtocol>)backend
583                pid:(int)pid
585     //NSLog(@"Connect backend (pid=%d)", pid);
586     NSNumber *pidKey = [NSNumber numberWithInt:pid];
587     MMVimController *vc = nil;
589     @try {
590         [(NSDistantObject*)backend
591                 setProtocolForProxy:@protocol(MMBackendProtocol)];
593         vc = [[[MMVimController alloc]
594                 initWithBackend:backend pid:pid] autorelease];
596         if (![vimControllers count]) {
597             // The first window autosaves its position.  (The autosaving
598             // features of Cocoa are not used because we need more control over
599             // what is autosaved and when it is restored.)
600             [[vc windowController] setWindowAutosaveKey:MMTopLeftPointKey];
601         }
603         [vimControllers addObject:vc];
605         // Pass arguments to the Vim process.
606         id args = [pidArguments objectForKey:pidKey];
607         if (args && args != [NSNull null]) {
608             // Pass filenames to open
609             NSArray *filenames = [args objectForKey:@"filenames"];
610             if (filenames) {
611                 NSString *tabDrop = buildTabDropCommand(filenames);
612                 [vc addVimInput:tabDrop];
613             }
615             // Pass ODB data
616             if ([args objectForKey:@"remoteFiles"]
617                     && [args objectForKey:@"remoteID"]) {
618                 [vc odbEdit:[args objectForKey:@"remoteFiles"]
619                      server:[[args objectForKey:@"remoteID"] unsignedIntValue]
620                        path:[args objectForKey:@"remotePath"]
621                       token:[args objectForKey:@"remoteToken"]];
622             }
624             // Pass range of lines to select
625             if ([args objectForKey:@"selectionRange"]) {
626                 NSRange selectionRange = NSRangeFromString(
627                         [args objectForKey:@"selectionRange"]);
628                 [vc addVimInput:buildSelectRangeCommand(selectionRange)];
629             }
630         }
632         // HACK!  MacVim does not get activated if it is launched from the
633         // terminal, so we forcibly activate here unless it is an untitled
634         // window opening.  Untitled windows are treated differently, else
635         // MacVim would steal the focus if another app was activated while the
636         // untitled window was loading.
637         if (!args || args != [NSNull null])
638             [NSApp activateIgnoringOtherApps:YES];
640         if (args)
641             [pidArguments removeObjectForKey:pidKey];
643         return vc;
644     }
646     @catch (NSException *e) {
647         NSLog(@"Exception caught in %s: \"%@\"", _cmd, e);
649         if (vc)
650             [vimControllers removeObject:vc];
652         [pidArguments removeObjectForKey:pidKey];
653     }
655     return nil;
658 - (NSArray *)serverList
660     NSMutableArray *array = [NSMutableArray array];
662     unsigned i, count = [vimControllers count];
663     for (i = 0; i < count; ++i) {
664         MMVimController *controller = [vimControllers objectAtIndex:i];
665         if ([controller serverName])
666             [array addObject:[controller serverName]];
667     }
669     return array;
672 @end // MMAppController
677 @implementation MMAppController (MMServices)
679 - (void)openSelection:(NSPasteboard *)pboard userData:(NSString *)userData
680                 error:(NSString **)error
682     if (![[pboard types] containsObject:NSStringPboardType]) {
683         NSLog(@"WARNING: Pasteboard contains no object of type "
684                 "NSStringPboardType");
685         return;
686     }
688     MMVimController *vc = [self topmostVimController];
689     if (vc) {
690         // Open a new tab first, since dropString: does not do this.
691         [vc sendMessage:AddNewTabMsgID data:nil];
692         [vc dropString:[pboard stringForType:NSStringPboardType]];
693     } else {
694         // NOTE: There is no window to paste the selection into, so save the
695         // text, open a new window, and paste the text when the next window
696         // opens.  (If this is called several times in a row, then all but the
697         // last call might be ignored.)
698         if (openSelectionString) [openSelectionString release];
699         openSelectionString = [[pboard stringForType:NSStringPboardType] copy];
701         [self newWindow:self];
702     }
705 - (void)openFile:(NSPasteboard *)pboard userData:(NSString *)userData
706            error:(NSString **)error
708     if (![[pboard types] containsObject:NSStringPboardType]) {
709         NSLog(@"WARNING: Pasteboard contains no object of type "
710                 "NSStringPboardType");
711         return;
712     }
714     // TODO: Parse multiple filenames and create array with names.
715     NSString *string = [pboard stringForType:NSStringPboardType];
716     string = [string stringByTrimmingCharactersInSet:
717             [NSCharacterSet whitespaceAndNewlineCharacterSet]];
718     string = [string stringByStandardizingPath];
720     NSArray *filenames = [self filterFilesAndNotify:
721             [NSArray arrayWithObject:string]];
722     if ([filenames count] > 0) {
723         MMVimController *vc = nil;
724         if (userData && [userData isEqual:@"Tab"])
725             vc = [self topmostVimController];
727         if (vc) {
728             [vc dropFiles:filenames forceOpen:YES];
729         } else {
730             [self application:NSApp openFiles:filenames];
731         }
732     }
735 @end // MMAppController (MMServices)
740 @implementation MMAppController (Private)
742 - (MMVimController *)keyVimController
744     NSWindow *keyWindow = [NSApp keyWindow];
745     if (keyWindow) {
746         unsigned i, count = [vimControllers count];
747         for (i = 0; i < count; ++i) {
748             MMVimController *vc = [vimControllers objectAtIndex:i];
749             if ([[[vc windowController] window] isEqual:keyWindow])
750                 return vc;
751         }
752     }
754     return nil;
757 - (MMVimController *)topmostVimController
759     NSArray *windows = [NSApp orderedWindows];
760     if ([windows count] > 0) {
761         NSWindow *window = [windows objectAtIndex:0];
762         unsigned i, count = [vimControllers count];
763         for (i = 0; i < count; ++i) {
764             MMVimController *vc = [vimControllers objectAtIndex:i];
765             if ([[[vc windowController] window] isEqual:window])
766                 return vc;
767         }
768     }
770     return nil;
773 - (int)launchVimProcessWithArguments:(NSArray *)args
775     NSString *taskPath = nil;
776     NSArray *taskArgs = nil;
777     NSString *path = [[NSBundle mainBundle] pathForAuxiliaryExecutable:@"Vim"];
779     if (!path) {
780         NSLog(@"ERROR: Vim executable could not be found inside app bundle!");
781         return 0;
782     }
784     if ([[NSUserDefaults standardUserDefaults] boolForKey:MMLoginShellKey]) {
785         // Run process with a login shell
786         //   $SHELL -l -c "exec Vim -g -f args"
787         // (-g for GUI, -f for foreground, i.e. don't fork)
789         NSMutableString *execArg = [NSMutableString
790             stringWithFormat:@"exec \"%@\" -g -f", path];
791         if (args) {
792             // Append all arguments while making sure that arguments containing
793             // spaces are enclosed in quotes.
794             NSCharacterSet *space = [NSCharacterSet whitespaceCharacterSet];
795             unsigned i, count = [args count];
797             for (i = 0; i < count; ++i) {
798                 NSString *arg = [args objectAtIndex:i];
799                 if (NSNotFound != [arg rangeOfCharacterFromSet:space].location)
800                     [execArg appendFormat:@" \"%@\"", arg];
801                 else
802                     [execArg appendFormat:@" %@", arg];
803             }
804         }
806         // Launch the process with a login shell so that users environment
807         // settings get sourced.  This does not always happen when MacVim is
808         // started.
809         taskArgs = [NSArray arrayWithObjects:@"-l", @"-c", execArg, nil];
810         taskPath = [[[NSProcessInfo processInfo] environment]
811             objectForKey:@"SHELL"];
812         if (!taskPath)
813             taskPath = @"/bin/sh";
814     } else {
815         // Run process directly:
816         //   Vim -g -f args
817         // (-g for GUI, -f for foreground, i.e. don't fork)
818         taskPath = path;
819         taskArgs = [NSArray arrayWithObjects:@"-g", @"-f", nil];
820         if (args)
821             taskArgs = [taskArgs arrayByAddingObjectsFromArray:args];
822     }
824     NSTask *task =[NSTask launchedTaskWithLaunchPath:taskPath
825                                            arguments:taskArgs];
826     //NSLog(@"launch %@ with args=%@ (pid=%d)", taskPath, taskArgs,
827     //        [task processIdentifier]);
829     int pid = [task processIdentifier];
831     // If the process has no arguments, then add a null argument to the
832     // pidArguments dictionary.  This is later used to detect that a process
833     // without arguments is being launched.
834     if (!args)
835         [pidArguments setObject:[NSNull null]
836                          forKey:[NSNumber numberWithInt:pid]];
838     return pid;
841 - (NSArray *)filterFilesAndNotify:(NSArray *)filenames
843     // Go trough 'filenames' array and make sure each file exists.  Present
844     // warning dialog if some file was missing.
846     NSString *firstMissingFile = nil;
847     NSMutableArray *files = [NSMutableArray array];
848     unsigned i, count = [filenames count];
850     for (i = 0; i < count; ++i) {
851         NSString *name = [filenames objectAtIndex:i];
852         if ([[NSFileManager defaultManager] fileExistsAtPath:name]) {
853             [files addObject:name];
854         } else if (!firstMissingFile) {
855             firstMissingFile = name;
856         }
857     }
859     if (firstMissingFile) {
860         NSAlert *alert = [[NSAlert alloc] init];
861         [alert addButtonWithTitle:@"OK"];
863         NSString *text;
864         if ([files count] >= count-1) {
865             [alert setMessageText:@"File not found"];
866             text = [NSString stringWithFormat:@"Could not open file with "
867                 "name %@.", firstMissingFile];
868         } else {
869             [alert setMessageText:@"Multiple files not found"];
870             text = [NSString stringWithFormat:@"Could not open file with "
871                 "name %@, and %d other files.", firstMissingFile,
872                 count-[files count]-1];
873         }
875         [alert setInformativeText:text];
876         [alert setAlertStyle:NSWarningAlertStyle];
878         [alert runModal];
879         [alert release];
881         [NSApp replyToOpenOrPrint:NSApplicationDelegateReplyFailure];
882     }
884     return files;
887 - (NSArray *)filterOpenFiles:(NSArray *)filenames remote:(OSType)theID
888                         path:(NSString *)path
889                        token:(NSAppleEventDescriptor *)token
890               selectionRange:(NSRange)selectionRange
892     // Check if any of the files in the 'filenames' array are open in any Vim
893     // process.  Remove the files that are open from the 'filenames' array and
894     // return it.  If all files were filtered out, then raise the first file in
895     // the Vim process it is open.  Files that are filtered are sent an odb
896     // open event in case theID is not zero.
898     MMVimController *raiseController = nil;
899     NSString *raiseFile = nil;
900     NSMutableArray *files = [filenames mutableCopy];
901     NSString *expr = [NSString stringWithFormat:
902             @"map([\"%@\"],\"bufloaded(v:val)\")",
903             [files componentsJoinedByString:@"\",\""]];
904     unsigned i, count = [vimControllers count];
906     for (i = 0; i < count && [files count]; ++i) {
907         MMVimController *controller = [vimControllers objectAtIndex:i];
908         id proxy = [controller backendProxy];
910         @try {
911             NSString *eval = [proxy evaluateExpression:expr];
912             NSIndexSet *idxSet = [NSIndexSet indexSetWithVimList:eval];
913             if ([idxSet count]) {
914                 if (!raiseFile) {
915                     // Remember the file and which Vim that has it open so that
916                     // we can raise it later on.
917                     raiseController = controller;
918                     raiseFile = [files objectAtIndex:[idxSet firstIndex]];
919                     [[raiseFile retain] autorelease];
920                 }
922                 // Send an odb open event to the Vim process.
923                 if (theID != 0)
924                     [controller odbEdit:[files objectsAtIndexes:idxSet]
925                                  server:theID path:path token:token];
927                 // Remove all the files that were open in this Vim process and
928                 // create a new expression to evaluate.
929                 [files removeObjectsAtIndexes:idxSet];
930                 expr = [NSString stringWithFormat:
931                         @"map([\"%@\"],\"bufloaded(v:val)\")",
932                         [files componentsJoinedByString:@"\",\""]];
933             }
934         }
935         @catch (NSException *e) {
936             // Do nothing ...
937         }
938     }
940     if (![files count] && raiseFile) {
941         // Raise the window containing the first file that was already open,
942         // and make sure that the tab containing that file is selected.  Only
943         // do this if there are no more files to open, otherwise sometimes the
944         // window with 'raiseFile' will be raised, other times it might be the
945         // window that will open with the files in the 'files' array.
946         raiseFile = [raiseFile stringByEscapingSpecialFilenameCharacters];
947         NSString *input = [NSString stringWithFormat:@"<C-\\><C-N>"
948             ":let oldswb=&swb|let &swb=\"useopen,usetab\"|"
949             "tab sb %@|let &swb=oldswb|unl oldswb|"
950             "cal foreground()|redr|f<CR>", raiseFile];
952         if (selectionRange.location != NSNotFound)
953             input = [input stringByAppendingString:
954                     buildSelectRangeCommand(selectionRange)];
956         [raiseController addVimInput:input];
957     }
959     return files;
962 #if MM_HANDLE_XCODE_MOD_EVENT
963 - (void)handleXcodeModEvent:(NSAppleEventDescriptor *)event
964                  replyEvent:(NSAppleEventDescriptor *)reply
966 #if 0
967     // Xcode sends this event to query MacVim which open files have been
968     // modified.
969     NSLog(@"reply:%@", reply);
970     NSLog(@"event:%@", event);
972     NSEnumerator *e = [vimControllers objectEnumerator];
973     id vc;
974     while ((vc = [e nextObject])) {
975         DescType type = [reply descriptorType];
976         unsigned len = [[type data] length];
977         NSMutableData *data = [NSMutableData data];
979         [data appendBytes:&type length:sizeof(DescType)];
980         [data appendBytes:&len length:sizeof(unsigned)];
981         [data appendBytes:[reply data] length:len];
983         [vc sendMessage:XcodeModMsgID data:data];
984     }
985 #endif
987 #endif
989 - (int)findLaunchingProcessWithoutArguments
991     NSArray *keys = [pidArguments allKeysForObject:[NSNull null]];
992     if ([keys count] > 0) {
993         //NSLog(@"found launching process without arguments");
994         return [[keys objectAtIndex:0] intValue];
995     }
997     return 0;
1000 - (MMVimController *)findUntitledWindow
1002     NSEnumerator *e = [vimControllers objectEnumerator];
1003     id vc;
1004     while ((vc = [e nextObject])) {
1005         // TODO: This is a moronic test...should query the Vim process if there
1006         // are any open buffers or something like that instead.
1007         NSString *title = [[[vc windowController] window] title];
1008         if ([title hasPrefix:@"[No Name]"]) {
1009             //NSLog(@"found untitled window");
1010             return vc;
1011         }
1012     }
1014     return nil;
1017 @end // MMAppController (Private)
1022 @implementation NSMenu (MMExtras)
1024 - (void)recurseSetAutoenablesItems:(BOOL)on
1026     [self setAutoenablesItems:on];
1028     int i, count = [self numberOfItems];
1029     for (i = 0; i < count; ++i) {
1030         NSMenuItem *item = [self itemAtIndex:i];
1031         [item setEnabled:YES];
1032         NSMenu *submenu = [item submenu];
1033         if (submenu) {
1034             [submenu recurseSetAutoenablesItems:on];
1035         }
1036     }
1039 @end  // NSMenu (MMExtras)
1044 @implementation NSNumber (MMExtras)
1045 - (int)tag
1047     return [self intValue];
1049 @end // NSNumber (MMExtras)