Support file-open check for multiple files
[MacVim.git] / src / MacVim / MMAppController.m
blob4a15f451d18671789109c9698c38cb67111ee36a
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"
35 // Default timeout intervals on all connections.
36 static NSTimeInterval MMRequestTimeout = 5;
37 static NSTimeInterval MMReplyTimeout = 5;
41 @interface MMAppController (MMServices)
42 - (void)openSelection:(NSPasteboard *)pboard userData:(NSString *)userData
43                 error:(NSString **)error;
44 - (void)openFile:(NSPasteboard *)pboard userData:(NSString *)userData
45            error:(NSString **)error;
46 @end
49 @interface MMAppController (Private)
50 - (MMVimController *)keyVimController;
51 - (MMVimController *)topmostVimController;
52 - (void)launchVimProcessWithArguments:(NSArray *)args;
53 - (NSArray *)filterFilesAndNotify:(NSArray *)files;
54 - (NSArray *)filterOpenFilesAndRaiseFirst:(NSArray *)filenames;
55 @end
57 @interface NSMenu (MMExtras)
58 - (void)recurseSetAutoenablesItems:(BOOL)on;
59 @end
61 @interface NSNumber (MMExtras)
62 - (int)tag;
63 @end
67 @implementation MMAppController
69 + (void)initialize
71     NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:
72         [NSNumber numberWithBool:NO],   MMNoWindowKey,
73         [NSNumber numberWithInt:64],    MMTabMinWidthKey,
74         [NSNumber numberWithInt:6*64],  MMTabMaxWidthKey,
75         [NSNumber numberWithInt:132],   MMTabOptimumWidthKey,
76         [NSNumber numberWithInt:2],     MMTextInsetLeftKey,
77         [NSNumber numberWithInt:1],     MMTextInsetRightKey,
78         [NSNumber numberWithInt:1],     MMTextInsetTopKey,
79         [NSNumber numberWithInt:1],     MMTextInsetBottomKey,
80         [NSNumber numberWithBool:NO],   MMTerminateAfterLastWindowClosedKey,
81         @"MMTypesetter",                MMTypesetterKey,
82         [NSNumber numberWithFloat:1],   MMCellWidthMultiplierKey,
83         [NSNumber numberWithFloat:-1],  MMBaselineOffsetKey,
84         [NSNumber numberWithBool:YES],  MMTranslateCtrlClickKey,
85         [NSNumber numberWithBool:NO],   MMOpenFilesInTabsKey,
86         [NSNumber numberWithBool:NO],   MMNoFontSubstitutionKey,
87         nil];
89     [[NSUserDefaults standardUserDefaults] registerDefaults:dict];
91     NSArray *types = [NSArray arrayWithObject:NSStringPboardType];
92     [NSApp registerServicesMenuSendTypes:types returnTypes:types];
95 - (id)init
97     if ((self = [super init])) {
98         fontContainerRef = loadFonts();
100         vimControllers = [NSMutableArray new];
102         // NOTE!  If the name of the connection changes here it must also be
103         // updated in MMBackend.m.
104         NSConnection *connection = [NSConnection defaultConnection];
105         NSString *name = [NSString stringWithFormat:@"%@-connection",
106                  [[NSBundle mainBundle] bundleIdentifier]];
107         //NSLog(@"Registering connection with name '%@'", name);
108         if ([connection registerName:name]) {
109             [connection setRequestTimeout:MMRequestTimeout];
110             [connection setReplyTimeout:MMReplyTimeout];
111             [connection setRootObject:self];
113             // NOTE: When the user is resizing the window the AppKit puts the
114             // run loop in event tracking mode.  Unless the connection listens
115             // to request in this mode, live resizing won't work.
116             [connection addRequestMode:NSEventTrackingRunLoopMode];
117         } else {
118             NSLog(@"WARNING: Failed to register connection with name '%@'",
119                     name);
120         }
121     }
123     return self;
126 - (void)dealloc
128     //NSLog(@"MMAppController dealloc");
130     [vimControllers release];
131     [openSelectionString release];
133     [super dealloc];
136 - (void)applicationDidFinishLaunching:(NSNotification *)notification
138     [NSApp setServicesProvider:self];
141 - (BOOL)applicationShouldOpenUntitledFile:(NSApplication *)sender
143     // NOTE!  This way it possible to start the app with the command-line
144     // argument '-nowindow yes' and no window will be opened by default.
145     untitledWindowOpening =
146         ![[NSUserDefaults standardUserDefaults] boolForKey:MMNoWindowKey];
147     return untitledWindowOpening;
150 - (BOOL)applicationOpenUntitledFile:(NSApplication *)sender
152     //NSLog(@"%s NSapp=%@ theApp=%@", _cmd, NSApp, sender);
154     [self newWindow:self];
155     return YES;
158 - (void)application:(NSApplication *)sender openFiles:(NSArray *)filenames
160     filenames = [self filterOpenFilesAndRaiseFirst:filenames];
161     if ([filenames count]) {
162         MMVimController *vc;
163         BOOL openInTabs = [[NSUserDefaults standardUserDefaults]
164             boolForKey:MMOpenFilesInTabsKey];
166         if (openInTabs && (vc = [self topmostVimController])) {
167             [vc dropFiles:filenames];
168         } else {
169             NSMutableArray *args = [NSMutableArray arrayWithObject:@"-p"];
170             [args addObjectsFromArray:filenames];
171             [self launchVimProcessWithArguments:args];
172         }
173     }
175     [NSApp replyToOpenOrPrint:NSApplicationDelegateReplySuccess];
176     // NSApplicationDelegateReplySuccess = 0,
177     // NSApplicationDelegateReplyCancel = 1,
178     // NSApplicationDelegateReplyFailure = 2
181 - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender
183     return [[NSUserDefaults standardUserDefaults]
184             boolForKey:MMTerminateAfterLastWindowClosedKey];
187 - (NSApplicationTerminateReply)applicationShouldTerminate:
188     (NSApplication *)sender
190     // TODO: Follow Apple's guidelines for 'Graceful Application Termination'
191     // (in particular, allow user to review changes and save).
192     int reply = NSTerminateNow;
193     BOOL modifiedBuffers = NO;
195     // Go through windows, checking for modified buffers.  (Each Vim process
196     // tells MacVim when any buffer has been modified and MacVim sets the
197     // 'documentEdited' flag of the window correspondingly.)
198     NSEnumerator *e = [[NSApp windows] objectEnumerator];
199     id window;
200     while (window = [e nextObject]) {
201         if ([window isDocumentEdited]) {
202             modifiedBuffers = YES;
203             break;
204         }
205     }
207     if (modifiedBuffers) {
208         NSAlert *alert = [[NSAlert alloc] init];
209         [alert addButtonWithTitle:@"Quit"];
210         [alert addButtonWithTitle:@"Cancel"];
211         [alert setMessageText:@"Quit without saving?"];
212         [alert setInformativeText:@"There are modified buffers, "
213             "if you quit now all changes will be lost.  Quit anyway?"];
214         [alert setAlertStyle:NSWarningAlertStyle];
216         if ([alert runModal] != NSAlertFirstButtonReturn)
217             reply = NSTerminateCancel;
219         [alert release];
220     }
222     return reply;
225 - (void)applicationWillTerminate:(NSNotification *)aNotification
227     // This will invalidate all connections (since they were spawned from the
228     // default connection).
229     [[NSConnection defaultConnection] invalidate];
231     // Send a SIGINT to all running Vim processes, so that they are sure to
232     // receive the connectionDidDie: notification (a process has to be checking
233     // the run-loop for this to happen).
234     unsigned i, count = [vimControllers count];
235     for (i = 0; i < count; ++i) {
236         MMVimController *controller = [vimControllers objectAtIndex:i];
237         int pid = [controller pid];
238         if (pid > 0)
239             kill(pid, SIGINT);
240     }
242     if (fontContainerRef) {
243         ATSFontDeactivate(fontContainerRef, NULL, kATSOptionFlagsDefault);
244         fontContainerRef = 0;
245     }
247     // TODO: Is this a correct way of releasing the MMAppController?
248     // (It doesn't seem like dealloc is ever called.)
249     [NSApp setDelegate:nil];
250     [self autorelease];
253 - (void)removeVimController:(id)controller
255     //NSLog(@"%s%@", _cmd, controller);
257     [[controller windowController] close];
259     [vimControllers removeObject:controller];
261     if (![vimControllers count]) {
262         // Turn on autoenabling of menus (because no Vim is open to handle it),
263         // but do not touch the MacVim menu.  Note that the menus must be
264         // enabled first otherwise autoenabling does not work.
265         NSMenu *mainMenu = [NSApp mainMenu];
266         int i, count = [mainMenu numberOfItems];
267         for (i = 1; i < count; ++i) {
268             NSMenuItem *item = [mainMenu itemAtIndex:i];
269             [item setEnabled:YES];
270             [[item submenu] recurseSetAutoenablesItems:YES];
271         }
272     }
275 - (void)windowControllerWillOpen:(MMWindowController *)windowController
277     NSPoint topLeft = NSZeroPoint;
278     NSWindow *keyWin = [NSApp keyWindow];
279     NSWindow *win = [windowController window];
281     if (!win) return;
283     // If there is a key window, cascade from it, otherwise use the autosaved
284     // window position (if any).
285     if (keyWin) {
286         NSRect frame = [keyWin frame];
287         topLeft = NSMakePoint(frame.origin.x, NSMaxY(frame));
288     } else {
289         NSString *topLeftString = [[NSUserDefaults standardUserDefaults]
290             stringForKey:MMTopLeftPointKey];
291         if (topLeftString)
292             topLeft = NSPointFromString(topLeftString);
293     }
295     if (!NSEqualPoints(topLeft, NSZeroPoint)) {
296         if (keyWin)
297             topLeft = [win cascadeTopLeftFromPoint:topLeft];
299         [win setFrameTopLeftPoint:topLeft];
300     }
302     if (openSelectionString) {
303         // There is some text to paste into this window as a result of the
304         // services menu "Open selection ..." being used.
305         [[windowController vimController] dropString:openSelectionString];
306         [openSelectionString release];
307         openSelectionString = nil;
308     }
311 - (IBAction)newWindow:(id)sender
313     [self launchVimProcessWithArguments:nil];
316 - (IBAction)selectNextWindow:(id)sender
318     unsigned i, count = [vimControllers count];
319     if (!count) return;
321     NSWindow *keyWindow = [NSApp keyWindow];
322     for (i = 0; i < count; ++i) {
323         MMVimController *vc = [vimControllers objectAtIndex:i];
324         if ([[[vc windowController] window] isEqual:keyWindow])
325             break;
326     }
328     if (i < count) {
329         if (++i >= count)
330             i = 0;
331         MMVimController *vc = [vimControllers objectAtIndex:i];
332         [[vc windowController] showWindow:self];
333     }
336 - (IBAction)selectPreviousWindow:(id)sender
338     unsigned i, count = [vimControllers count];
339     if (!count) return;
341     NSWindow *keyWindow = [NSApp keyWindow];
342     for (i = 0; i < count; ++i) {
343         MMVimController *vc = [vimControllers objectAtIndex:i];
344         if ([[[vc windowController] window] isEqual:keyWindow])
345             break;
346     }
348     if (i < count) {
349         if (i > 0) {
350             --i;
351         } else {
352             i = count - 1;
353         }
354         MMVimController *vc = [vimControllers objectAtIndex:i];
355         [[vc windowController] showWindow:self];
356     }
359 - (IBAction)fontSizeUp:(id)sender
361     [[NSFontManager sharedFontManager] modifyFont:
362             [NSNumber numberWithInt:NSSizeUpFontAction]];
365 - (IBAction)fontSizeDown:(id)sender
367     [[NSFontManager sharedFontManager] modifyFont:
368             [NSNumber numberWithInt:NSSizeDownFontAction]];
371 - (byref id <MMFrontendProtocol>)
372     connectBackend:(byref in id <MMBackendProtocol>)backend
373                pid:(int)pid
375     //NSLog(@"Frontend got connection request from backend...adding new "
376     //        "MMVimController");
378     [(NSDistantObject*)backend
379             setProtocolForProxy:@protocol(MMBackendProtocol)];
381     MMVimController *vc = [[[MMVimController alloc]
382             initWithBackend:backend pid:pid] autorelease];
384     if (![vimControllers count]) {
385         // The first window autosaves its position.  (The autosaving features
386         // of Cocoa are not used because we need more control over what is
387         // autosaved and when it is restored.)
388         [[vc windowController] setWindowAutosaveKey:MMTopLeftPointKey];
389     }
391     [vimControllers addObject:vc];
393     // HACK!  MacVim does not get activated if it is launched from the
394     // terminal, so we forcibly activate here unless it is an untitled window
395     // opening (i.e. MacVim was opened from the Finder).  Untitled windows are
396     // treated differently, else MacVim would steal the focus if another app
397     // was activated while the untitled window was loading.
398     if (!untitledWindowOpening)
399         [NSApp activateIgnoringOtherApps:YES];
401     untitledWindowOpening = NO;
403     return vc;
406 - (NSArray *)serverList
408     NSMutableArray *array = [NSMutableArray array];
410     unsigned i, count = [vimControllers count];
411     for (i = 0; i < count; ++i) {
412         MMVimController *controller = [vimControllers objectAtIndex:i];
413         if ([controller serverName])
414             [array addObject:[controller serverName]];
415     }
417     return array;
420 @end // MMAppController
425 @implementation MMAppController (MMServices)
427 - (void)openSelection:(NSPasteboard *)pboard userData:(NSString *)userData
428                 error:(NSString **)error
430     if (![[pboard types] containsObject:NSStringPboardType]) {
431         NSLog(@"WARNING: Pasteboard contains no object of type "
432                 "NSStringPboardType");
433         return;
434     }
436     MMVimController *vc = [self topmostVimController];
437     if (vc) {
438         // Open a new tab first, since dropString: does not do this.
439         [vc sendMessage:AddNewTabMsgID data:nil];
440         [vc dropString:[pboard stringForType:NSStringPboardType]];
441     } else {
442         // NOTE: There is no window to paste the selection into, so save the
443         // text, open a new window, and paste the text when the next window
444         // opens.  (If this is called several times in a row, then all but the
445         // last call might be ignored.)
446         if (openSelectionString) [openSelectionString release];
447         openSelectionString = [[pboard stringForType:NSStringPboardType] copy];
449         [self newWindow:self];
450     }
453 - (void)openFile:(NSPasteboard *)pboard userData:(NSString *)userData
454            error:(NSString **)error
456     if (![[pboard types] containsObject:NSStringPboardType]) {
457         NSLog(@"WARNING: Pasteboard contains no object of type "
458                 "NSStringPboardType");
459         return;
460     }
462     // TODO: Parse multiple filenames and create array with names.
463     NSString *string = [pboard stringForType:NSStringPboardType];
464     string = [string stringByTrimmingCharactersInSet:
465             [NSCharacterSet whitespaceAndNewlineCharacterSet]];
466     string = [string stringByStandardizingPath];
468     NSArray *filenames = [self filterFilesAndNotify:
469             [NSArray arrayWithObject:string]];
470     if ([filenames count] > 0) {
471         MMVimController *vc = nil;
472         if (userData && [userData isEqual:@"Tab"])
473             vc = [self topmostVimController];
475         if (vc) {
476             [vc dropFiles:filenames];
477         } else {
478             [self application:NSApp openFiles:filenames];
479         }
480     }
483 @end // MMAppController (MMServices)
488 @implementation MMAppController (Private)
490 - (MMVimController *)keyVimController
492     NSWindow *keyWindow = [NSApp keyWindow];
493     if (keyWindow) {
494         unsigned i, count = [vimControllers count];
495         for (i = 0; i < count; ++i) {
496             MMVimController *vc = [vimControllers objectAtIndex:i];
497             if ([[[vc windowController] window] isEqual:keyWindow])
498                 return vc;
499         }
500     }
502     return nil;
505 - (MMVimController *)topmostVimController
507     NSArray *windows = [NSApp orderedWindows];
508     if ([windows count] > 0) {
509         NSWindow *window = [windows objectAtIndex:0];
510         unsigned i, count = [vimControllers count];
511         for (i = 0; i < count; ++i) {
512             MMVimController *vc = [vimControllers objectAtIndex:i];
513             if ([[[vc windowController] window] isEqual:window])
514                 return vc;
515         }
516     }
518     return nil;
521 - (void)launchVimProcessWithArguments:(NSArray *)args
523     NSString *path = [[NSBundle mainBundle] pathForAuxiliaryExecutable:@"Vim"];
524     if (!path) {
525         NSLog(@"ERROR: Vim executable could not be found inside app bundle!");
526         return;
527     }
529     NSMutableString *execArg = [NSMutableString
530         stringWithFormat:@"exec \"%@\" -g", path];
531     if (args) {
532         // Append all arguments while making sure that arguments containing
533         // spaces are enclosed in quotes.
534         NSCharacterSet *space = [NSCharacterSet whitespaceCharacterSet];
535         unsigned i, count = [args count];
537         for (i = 0; i < count; ++i) {
538             NSString *arg = [args objectAtIndex:i];
539             if (NSNotFound != [arg rangeOfCharacterFromSet:space].location)
540                 [execArg appendFormat:@" \"%@\"", arg];
541             else
542                 [execArg appendFormat:@" %@", arg];
543         }
544     }
546     // Launch the process with a login shell so that users environment settings
547     // get sourced.  This does not always happen when MacVim is started.
548     NSArray *shellArgs = [NSArray arrayWithObjects:@"-l", @"-c", execArg, nil];
549     NSString *shell = [[[NSProcessInfo processInfo] environment]
550         objectForKey:@"SHELL"];
551     if (!shell)
552         shell = @"/bin/sh";
554     //NSLog(@"Launching: %@  args: %@", shell, shellArgs);
555     [NSTask launchedTaskWithLaunchPath:shell arguments:shellArgs];
558 - (NSArray *)filterFilesAndNotify:(NSArray *)filenames
560     // Go trough 'filenames' array and make sure each file exists.  Present
561     // warning dialog if some file was missing.
563     NSString *firstMissingFile = nil;
564     NSMutableArray *files = [NSMutableArray array];
565     unsigned i, count = [filenames count];
567     for (i = 0; i < count; ++i) {
568         NSString *name = [filenames objectAtIndex:i];
569         if ([[NSFileManager defaultManager] fileExistsAtPath:name]) {
570             [files addObject:name];
571         } else if (!firstMissingFile) {
572             firstMissingFile = name;
573         }
574     }
576     if (firstMissingFile) {
577         NSAlert *alert = [[NSAlert alloc] init];
578         [alert addButtonWithTitle:@"OK"];
580         NSString *text;
581         if ([files count] >= count-1) {
582             [alert setMessageText:@"File not found"];
583             text = [NSString stringWithFormat:@"Could not open file with "
584                 "name %@.", firstMissingFile];
585         } else {
586             [alert setMessageText:@"Multiple files not found"];
587             text = [NSString stringWithFormat:@"Could not open file with "
588                 "name %@, and %d other files.", firstMissingFile,
589                 count-[files count]-1];
590         }
592         [alert setInformativeText:text];
593         [alert setAlertStyle:NSWarningAlertStyle];
595         [alert runModal];
596         [alert release];
598         [NSApp replyToOpenOrPrint:NSApplicationDelegateReplyFailure];
599     }
601     return files;
604 - (NSArray *)filterOpenFilesAndRaiseFirst:(NSArray *)filenames
606     // Check if any of the files in the 'filenames' array are open in any Vim
607     // process.  Remove the files that are open from the 'filenames' array and
608     // return it.  If all files were filtered out, then raise the first file in
609     // the Vim process it is open.
611     MMVimController *raiseController = nil;
612     NSString *raiseFile = nil;
613     NSMutableArray *files = [filenames mutableCopy];
614     NSString *expr = [NSString stringWithFormat:
615             @"map([\"%@\"],\"bufloaded(v:val)\")",
616             [files componentsJoinedByString:@"\",\""]];
617     unsigned i, count = [vimControllers count];
619     for (i = 0; i < count && [files count]; ++i) {
620         MMVimController *controller = [vimControllers objectAtIndex:i];
621         id proxy = [controller backendProxy];
623         @try {
624             NSString *eval = [proxy evaluateExpression:expr];
625             NSIndexSet *idxSet = [NSIndexSet indexSetWithVimList:eval];
626             if ([idxSet count]) {
627                 if (!raiseFile) {
628                     // Remember the file and which Vim that has it open so that
629                     // we can raise it later on.
630                     raiseController = controller;
631                     raiseFile = [files objectAtIndex:[idxSet firstIndex]];
632                     [[raiseFile retain] autorelease];
633                 }
635                 // Remove all the files that were open in this Vim process and
636                 // create a new expression to evaluate.
637                 [files removeObjectsAtIndexes:idxSet];
638                 expr = [NSString stringWithFormat:
639                         @"map([\"%@\"],\"bufloaded(v:val)\")",
640                         [files componentsJoinedByString:@"\",\""]];
641             }
642         }
643         @catch (NSException *e) {
644             // Do nothing ...
645         }
646     }
648     if (![files count] && raiseFile) {
649         // Raise the window containing the first file that was already open,
650         // and make sure that the tab containing that file is selected.  Only
651         // do this if there are no more files to open, otherwise sometimes the
652         // window with 'raiseFile' will be raised, other times it might be the
653         // window that will open with the files in the 'files' array.
654         raiseFile = [raiseFile stringByEscapingSpecialFilenameCharacters];
655         NSString *input = [NSString stringWithFormat:@"<C-\\><C-N>"
656             ":let oldswb=&swb|let &swb=\"useopen,usetab\"|"
657             "tab sb %@|let &swb=oldswb|unl oldswb|"
658             "cal foreground()|redr|f<CR>", raiseFile];
659         [raiseController addVimInput:input];
660     }
662     return files;
665 @end // MMAppController (Private)
670 @implementation NSMenu (MMExtras)
672 - (void)recurseSetAutoenablesItems:(BOOL)on
674     [self setAutoenablesItems:on];
676     int i, count = [self numberOfItems];
677     for (i = 0; i < count; ++i) {
678         NSMenuItem *item = [self itemAtIndex:i];
679         [item setEnabled:YES];
680         NSMenu *submenu = [item submenu];
681         if (submenu) {
682             [submenu recurseSetAutoenablesItems:on];
683         }
684     }
687 @end  // NSMenu (MMExtras)
692 @implementation NSNumber (MMExtras)
693 - (int)tag
695     return [self intValue];
697 @end // NSNumber (MMExtras)