Preserve swap files after crash
[MacVim.git] / src / MacVim / MMAppController.m
blob18e106a6be6fc891b88c4a930137796a9bc72a72
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         [NSNumber numberWithBool:YES],  MMLoginShellKey,
88         nil];
90     [[NSUserDefaults standardUserDefaults] registerDefaults:dict];
92     NSArray *types = [NSArray arrayWithObject:NSStringPboardType];
93     [NSApp registerServicesMenuSendTypes:types returnTypes:types];
96 - (id)init
98     if ((self = [super init])) {
99         fontContainerRef = loadFonts();
101         vimControllers = [NSMutableArray new];
103         // NOTE!  If the name of the connection changes here it must also be
104         // updated in MMBackend.m.
105         NSConnection *connection = [NSConnection defaultConnection];
106         NSString *name = [NSString stringWithFormat:@"%@-connection",
107                  [[NSBundle mainBundle] bundleIdentifier]];
108         //NSLog(@"Registering connection with name '%@'", name);
109         if ([connection registerName:name]) {
110             [connection setRequestTimeout:MMRequestTimeout];
111             [connection setReplyTimeout:MMReplyTimeout];
112             [connection setRootObject:self];
114             // NOTE: When the user is resizing the window the AppKit puts the
115             // run loop in event tracking mode.  Unless the connection listens
116             // to request in this mode, live resizing won't work.
117             [connection addRequestMode:NSEventTrackingRunLoopMode];
118         } else {
119             NSLog(@"WARNING: Failed to register connection with name '%@'",
120                     name);
121         }
122     }
124     return self;
127 - (void)dealloc
129     //NSLog(@"MMAppController dealloc");
131     [vimControllers release];
132     [openSelectionString release];
134     [super dealloc];
137 - (void)applicationDidFinishLaunching:(NSNotification *)notification
139     [NSApp setServicesProvider:self];
142 - (BOOL)applicationShouldOpenUntitledFile:(NSApplication *)sender
144     // NOTE!  This way it possible to start the app with the command-line
145     // argument '-nowindow yes' and no window will be opened by default.
146     untitledWindowOpening =
147         ![[NSUserDefaults standardUserDefaults] boolForKey:MMNoWindowKey];
148     return untitledWindowOpening;
151 - (BOOL)applicationOpenUntitledFile:(NSApplication *)sender
153     //NSLog(@"%s NSapp=%@ theApp=%@", _cmd, NSApp, sender);
155     [self newWindow:self];
156     return YES;
159 - (void)application:(NSApplication *)sender openFiles:(NSArray *)filenames
161     filenames = [self filterOpenFilesAndRaiseFirst:filenames];
162     if ([filenames count]) {
163         MMVimController *vc;
164         BOOL openInTabs = [[NSUserDefaults standardUserDefaults]
165             boolForKey:MMOpenFilesInTabsKey];
167         if (openInTabs && (vc = [self topmostVimController])) {
168             [vc dropFiles:filenames];
169         } else {
170             NSMutableArray *args = [NSMutableArray arrayWithObject:@"-p"];
171             [args addObjectsFromArray:filenames];
172             [self launchVimProcessWithArguments:args];
173         }
174     }
176     [NSApp replyToOpenOrPrint:NSApplicationDelegateReplySuccess];
177     // NSApplicationDelegateReplySuccess = 0,
178     // NSApplicationDelegateReplyCancel = 1,
179     // NSApplicationDelegateReplyFailure = 2
182 - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender
184     return [[NSUserDefaults standardUserDefaults]
185             boolForKey:MMTerminateAfterLastWindowClosedKey];
188 - (NSApplicationTerminateReply)applicationShouldTerminate:
189     (NSApplication *)sender
191     // TODO: Follow Apple's guidelines for 'Graceful Application Termination'
192     // (in particular, allow user to review changes and save).
193     int reply = NSTerminateNow;
194     BOOL modifiedBuffers = NO;
196     // Go through windows, checking for modified buffers.  (Each Vim process
197     // tells MacVim when any buffer has been modified and MacVim sets the
198     // 'documentEdited' flag of the window correspondingly.)
199     NSEnumerator *e = [[NSApp windows] objectEnumerator];
200     id window;
201     while ((window = [e nextObject])) {
202         if ([window isDocumentEdited]) {
203             modifiedBuffers = YES;
204             break;
205         }
206     }
208     if (modifiedBuffers) {
209         NSAlert *alert = [[NSAlert alloc] init];
210         [alert addButtonWithTitle:@"Quit"];
211         [alert addButtonWithTitle:@"Cancel"];
212         [alert setMessageText:@"Quit without saving?"];
213         [alert setInformativeText:@"There are modified buffers, "
214             "if you quit now all changes will be lost.  Quit anyway?"];
215         [alert setAlertStyle:NSWarningAlertStyle];
217         if ([alert runModal] != NSAlertFirstButtonReturn)
218             reply = NSTerminateCancel;
220         [alert release];
221     }
223     // Tell all Vim processes to terminate now (otherwise they'll leave swap
224     // files behind).
225     if (NSTerminateNow == reply) {
226         e = [vimControllers objectEnumerator];
227         id vc;
228         while ((vc = [e nextObject]))
229             [vc sendMessage:TerminateNowMsgID data:nil];
230     }
232     return reply;
235 - (void)applicationWillTerminate:(NSNotification *)aNotification
237     // This will invalidate all connections (since they were spawned from the
238     // default connection).
239     [[NSConnection defaultConnection] invalidate];
241     // Send a SIGINT to all running Vim processes, so that they are sure to
242     // receive the connectionDidDie: notification (a process has to be checking
243     // the run-loop for this to happen).
244     unsigned i, count = [vimControllers count];
245     for (i = 0; i < count; ++i) {
246         MMVimController *controller = [vimControllers objectAtIndex:i];
247         int pid = [controller pid];
248         if (pid > 0)
249             kill(pid, SIGINT);
250     }
252     if (fontContainerRef) {
253         ATSFontDeactivate(fontContainerRef, NULL, kATSOptionFlagsDefault);
254         fontContainerRef = 0;
255     }
257     // TODO: Is this a correct way of releasing the MMAppController?
258     // (It doesn't seem like dealloc is ever called.)
259     [NSApp setDelegate:nil];
260     [self autorelease];
263 - (void)removeVimController:(id)controller
265     //NSLog(@"%s%@", _cmd, controller);
267     [[controller windowController] close];
269     [vimControllers removeObject:controller];
271     if (![vimControllers count]) {
272         // Turn on autoenabling of menus (because no Vim is open to handle it),
273         // but do not touch the MacVim menu.  Note that the menus must be
274         // enabled first otherwise autoenabling does not work.
275         NSMenu *mainMenu = [NSApp mainMenu];
276         int i, count = [mainMenu numberOfItems];
277         for (i = 1; i < count; ++i) {
278             NSMenuItem *item = [mainMenu itemAtIndex:i];
279             [item setEnabled:YES];
280             [[item submenu] recurseSetAutoenablesItems:YES];
281         }
282     }
285 - (void)windowControllerWillOpen:(MMWindowController *)windowController
287     NSPoint topLeft = NSZeroPoint;
288     NSWindow *keyWin = [NSApp keyWindow];
289     NSWindow *win = [windowController window];
291     if (!win) return;
293     // If there is a key window, cascade from it, otherwise use the autosaved
294     // window position (if any).
295     if (keyWin) {
296         NSRect frame = [keyWin frame];
297         topLeft = NSMakePoint(frame.origin.x, NSMaxY(frame));
298     } else {
299         NSString *topLeftString = [[NSUserDefaults standardUserDefaults]
300             stringForKey:MMTopLeftPointKey];
301         if (topLeftString)
302             topLeft = NSPointFromString(topLeftString);
303     }
305     if (!NSEqualPoints(topLeft, NSZeroPoint)) {
306         if (keyWin)
307             topLeft = [win cascadeTopLeftFromPoint:topLeft];
309         [win setFrameTopLeftPoint:topLeft];
310     }
312     if (openSelectionString) {
313         // There is some text to paste into this window as a result of the
314         // services menu "Open selection ..." being used.
315         [[windowController vimController] dropString:openSelectionString];
316         [openSelectionString release];
317         openSelectionString = nil;
318     }
321 - (IBAction)newWindow:(id)sender
323     [self launchVimProcessWithArguments:nil];
326 - (IBAction)selectNextWindow:(id)sender
328     unsigned i, count = [vimControllers count];
329     if (!count) return;
331     NSWindow *keyWindow = [NSApp keyWindow];
332     for (i = 0; i < count; ++i) {
333         MMVimController *vc = [vimControllers objectAtIndex:i];
334         if ([[[vc windowController] window] isEqual:keyWindow])
335             break;
336     }
338     if (i < count) {
339         if (++i >= count)
340             i = 0;
341         MMVimController *vc = [vimControllers objectAtIndex:i];
342         [[vc windowController] showWindow:self];
343     }
346 - (IBAction)selectPreviousWindow:(id)sender
348     unsigned i, count = [vimControllers count];
349     if (!count) return;
351     NSWindow *keyWindow = [NSApp keyWindow];
352     for (i = 0; i < count; ++i) {
353         MMVimController *vc = [vimControllers objectAtIndex:i];
354         if ([[[vc windowController] window] isEqual:keyWindow])
355             break;
356     }
358     if (i < count) {
359         if (i > 0) {
360             --i;
361         } else {
362             i = count - 1;
363         }
364         MMVimController *vc = [vimControllers objectAtIndex:i];
365         [[vc windowController] showWindow:self];
366     }
369 - (IBAction)fontSizeUp:(id)sender
371     [[NSFontManager sharedFontManager] modifyFont:
372             [NSNumber numberWithInt:NSSizeUpFontAction]];
375 - (IBAction)fontSizeDown:(id)sender
377     [[NSFontManager sharedFontManager] modifyFont:
378             [NSNumber numberWithInt:NSSizeDownFontAction]];
381 - (byref id <MMFrontendProtocol>)
382     connectBackend:(byref in id <MMBackendProtocol>)backend
383                pid:(int)pid
385     //NSLog(@"Frontend got connection request from backend...adding new "
386     //        "MMVimController");
388     [(NSDistantObject*)backend
389             setProtocolForProxy:@protocol(MMBackendProtocol)];
391     MMVimController *vc = [[[MMVimController alloc]
392             initWithBackend:backend pid:pid] autorelease];
394     if (![vimControllers count]) {
395         // The first window autosaves its position.  (The autosaving features
396         // of Cocoa are not used because we need more control over what is
397         // autosaved and when it is restored.)
398         [[vc windowController] setWindowAutosaveKey:MMTopLeftPointKey];
399     }
401     [vimControllers addObject:vc];
403     // HACK!  MacVim does not get activated if it is launched from the
404     // terminal, so we forcibly activate here unless it is an untitled window
405     // opening (i.e. MacVim was opened from the Finder).  Untitled windows are
406     // treated differently, else MacVim would steal the focus if another app
407     // was activated while the untitled window was loading.
408     if (!untitledWindowOpening)
409         [NSApp activateIgnoringOtherApps:YES];
411     untitledWindowOpening = NO;
413     return vc;
416 - (NSArray *)serverList
418     NSMutableArray *array = [NSMutableArray array];
420     unsigned i, count = [vimControllers count];
421     for (i = 0; i < count; ++i) {
422         MMVimController *controller = [vimControllers objectAtIndex:i];
423         if ([controller serverName])
424             [array addObject:[controller serverName]];
425     }
427     return array;
430 @end // MMAppController
435 @implementation MMAppController (MMServices)
437 - (void)openSelection:(NSPasteboard *)pboard userData:(NSString *)userData
438                 error:(NSString **)error
440     if (![[pboard types] containsObject:NSStringPboardType]) {
441         NSLog(@"WARNING: Pasteboard contains no object of type "
442                 "NSStringPboardType");
443         return;
444     }
446     MMVimController *vc = [self topmostVimController];
447     if (vc) {
448         // Open a new tab first, since dropString: does not do this.
449         [vc sendMessage:AddNewTabMsgID data:nil];
450         [vc dropString:[pboard stringForType:NSStringPboardType]];
451     } else {
452         // NOTE: There is no window to paste the selection into, so save the
453         // text, open a new window, and paste the text when the next window
454         // opens.  (If this is called several times in a row, then all but the
455         // last call might be ignored.)
456         if (openSelectionString) [openSelectionString release];
457         openSelectionString = [[pboard stringForType:NSStringPboardType] copy];
459         [self newWindow:self];
460     }
463 - (void)openFile:(NSPasteboard *)pboard userData:(NSString *)userData
464            error:(NSString **)error
466     if (![[pboard types] containsObject:NSStringPboardType]) {
467         NSLog(@"WARNING: Pasteboard contains no object of type "
468                 "NSStringPboardType");
469         return;
470     }
472     // TODO: Parse multiple filenames and create array with names.
473     NSString *string = [pboard stringForType:NSStringPboardType];
474     string = [string stringByTrimmingCharactersInSet:
475             [NSCharacterSet whitespaceAndNewlineCharacterSet]];
476     string = [string stringByStandardizingPath];
478     NSArray *filenames = [self filterFilesAndNotify:
479             [NSArray arrayWithObject:string]];
480     if ([filenames count] > 0) {
481         MMVimController *vc = nil;
482         if (userData && [userData isEqual:@"Tab"])
483             vc = [self topmostVimController];
485         if (vc) {
486             [vc dropFiles:filenames];
487         } else {
488             [self application:NSApp openFiles:filenames];
489         }
490     }
493 @end // MMAppController (MMServices)
498 @implementation MMAppController (Private)
500 - (MMVimController *)keyVimController
502     NSWindow *keyWindow = [NSApp keyWindow];
503     if (keyWindow) {
504         unsigned i, count = [vimControllers count];
505         for (i = 0; i < count; ++i) {
506             MMVimController *vc = [vimControllers objectAtIndex:i];
507             if ([[[vc windowController] window] isEqual:keyWindow])
508                 return vc;
509         }
510     }
512     return nil;
515 - (MMVimController *)topmostVimController
517     NSArray *windows = [NSApp orderedWindows];
518     if ([windows count] > 0) {
519         NSWindow *window = [windows objectAtIndex:0];
520         unsigned i, count = [vimControllers count];
521         for (i = 0; i < count; ++i) {
522             MMVimController *vc = [vimControllers objectAtIndex:i];
523             if ([[[vc windowController] window] isEqual:window])
524                 return vc;
525         }
526     }
528     return nil;
531 - (void)launchVimProcessWithArguments:(NSArray *)args
533     NSString *taskPath = nil;
534     NSArray *taskArgs = nil;
535     NSString *path = [[NSBundle mainBundle] pathForAuxiliaryExecutable:@"Vim"];
537     if (!path) {
538         NSLog(@"ERROR: Vim executable could not be found inside app bundle!");
539         return;
540     }
542     if ([[NSUserDefaults standardUserDefaults] boolForKey:MMLoginShellKey]) {
543         // Run process with a login shell
544         //   $SHELL -l -c "exec Vim args"
546         NSMutableString *execArg = [NSMutableString
547             stringWithFormat:@"exec \"%@\" -g", path];
548         if (args) {
549             // Append all arguments while making sure that arguments containing
550             // spaces are enclosed in quotes.
551             NSCharacterSet *space = [NSCharacterSet whitespaceCharacterSet];
552             unsigned i, count = [args count];
554             for (i = 0; i < count; ++i) {
555                 NSString *arg = [args objectAtIndex:i];
556                 if (NSNotFound != [arg rangeOfCharacterFromSet:space].location)
557                     [execArg appendFormat:@" \"%@\"", arg];
558                 else
559                     [execArg appendFormat:@" %@", arg];
560             }
561         }
563         // Launch the process with a login shell so that users environment
564         // settings get sourced.  This does not always happen when MacVim is
565         // started.
566         taskArgs = [NSArray arrayWithObjects:@"-l", @"-c", execArg, nil];
567         taskPath = [[[NSProcessInfo processInfo] environment]
568             objectForKey:@"SHELL"];
569         if (!taskPath)
570             taskPath = @"/bin/sh";
571     } else {
572         // Run process directly:
573         //   Vim args
574         taskPath = path;
575         taskArgs = [NSArray arrayWithObject:@"-g"];
576         if (args)
577             taskArgs = [taskArgs arrayByAddingObjectsFromArray:args];
578     }
580     //NSLog(@"Launching: %@  args: %@", taskPath, taskArgs);
581     [NSTask launchedTaskWithLaunchPath:taskPath arguments:taskArgs];
584 - (NSArray *)filterFilesAndNotify:(NSArray *)filenames
586     // Go trough 'filenames' array and make sure each file exists.  Present
587     // warning dialog if some file was missing.
589     NSString *firstMissingFile = nil;
590     NSMutableArray *files = [NSMutableArray array];
591     unsigned i, count = [filenames count];
593     for (i = 0; i < count; ++i) {
594         NSString *name = [filenames objectAtIndex:i];
595         if ([[NSFileManager defaultManager] fileExistsAtPath:name]) {
596             [files addObject:name];
597         } else if (!firstMissingFile) {
598             firstMissingFile = name;
599         }
600     }
602     if (firstMissingFile) {
603         NSAlert *alert = [[NSAlert alloc] init];
604         [alert addButtonWithTitle:@"OK"];
606         NSString *text;
607         if ([files count] >= count-1) {
608             [alert setMessageText:@"File not found"];
609             text = [NSString stringWithFormat:@"Could not open file with "
610                 "name %@.", firstMissingFile];
611         } else {
612             [alert setMessageText:@"Multiple files not found"];
613             text = [NSString stringWithFormat:@"Could not open file with "
614                 "name %@, and %d other files.", firstMissingFile,
615                 count-[files count]-1];
616         }
618         [alert setInformativeText:text];
619         [alert setAlertStyle:NSWarningAlertStyle];
621         [alert runModal];
622         [alert release];
624         [NSApp replyToOpenOrPrint:NSApplicationDelegateReplyFailure];
625     }
627     return files;
630 - (NSArray *)filterOpenFilesAndRaiseFirst:(NSArray *)filenames
632     // Check if any of the files in the 'filenames' array are open in any Vim
633     // process.  Remove the files that are open from the 'filenames' array and
634     // return it.  If all files were filtered out, then raise the first file in
635     // the Vim process it is open.
637     MMVimController *raiseController = nil;
638     NSString *raiseFile = nil;
639     NSMutableArray *files = [filenames mutableCopy];
640     NSString *expr = [NSString stringWithFormat:
641             @"map([\"%@\"],\"bufloaded(v:val)\")",
642             [files componentsJoinedByString:@"\",\""]];
643     unsigned i, count = [vimControllers count];
645     for (i = 0; i < count && [files count]; ++i) {
646         MMVimController *controller = [vimControllers objectAtIndex:i];
647         id proxy = [controller backendProxy];
649         @try {
650             NSString *eval = [proxy evaluateExpression:expr];
651             NSIndexSet *idxSet = [NSIndexSet indexSetWithVimList:eval];
652             if ([idxSet count]) {
653                 if (!raiseFile) {
654                     // Remember the file and which Vim that has it open so that
655                     // we can raise it later on.
656                     raiseController = controller;
657                     raiseFile = [files objectAtIndex:[idxSet firstIndex]];
658                     [[raiseFile retain] autorelease];
659                 }
661                 // Remove all the files that were open in this Vim process and
662                 // create a new expression to evaluate.
663                 [files removeObjectsAtIndexes:idxSet];
664                 expr = [NSString stringWithFormat:
665                         @"map([\"%@\"],\"bufloaded(v:val)\")",
666                         [files componentsJoinedByString:@"\",\""]];
667             }
668         }
669         @catch (NSException *e) {
670             // Do nothing ...
671         }
672     }
674     if (![files count] && raiseFile) {
675         // Raise the window containing the first file that was already open,
676         // and make sure that the tab containing that file is selected.  Only
677         // do this if there are no more files to open, otherwise sometimes the
678         // window with 'raiseFile' will be raised, other times it might be the
679         // window that will open with the files in the 'files' array.
680         raiseFile = [raiseFile stringByEscapingSpecialFilenameCharacters];
681         NSString *input = [NSString stringWithFormat:@"<C-\\><C-N>"
682             ":let oldswb=&swb|let &swb=\"useopen,usetab\"|"
683             "tab sb %@|let &swb=oldswb|unl oldswb|"
684             "cal foreground()|redr|f<CR>", raiseFile];
685         [raiseController addVimInput:input];
686     }
688     return files;
691 @end // MMAppController (Private)
696 @implementation NSMenu (MMExtras)
698 - (void)recurseSetAutoenablesItems:(BOOL)on
700     [self setAutoenablesItems:on];
702     int i, count = [self numberOfItems];
703     for (i = 0; i < count; ++i) {
704         NSMenuItem *item = [self itemAtIndex:i];
705         [item setEnabled:YES];
706         NSMenu *submenu = [item submenu];
707         if (submenu) {
708             [submenu recurseSetAutoenablesItems:on];
709         }
710     }
713 @end  // NSMenu (MMExtras)
718 @implementation NSNumber (MMExtras)
719 - (int)tag
721     return [self intValue];
723 @end // NSNumber (MMExtras)