1 /* vi:set ts=8 sts=4 sw=4 ft=objc:
3 * VIM - Vi IMproved by Bram Moolenaar
4 * MacVim GUI port by Bjorn Winckler
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.
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.
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
24 * A Vim process started from the command line connects directly by sending the
25 * connectBackend:pid: message (launchVimProcessWithArguments: is never called
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;
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;
57 @interface NSMenu (MMExtras)
58 - (void)recurseSetAutoenablesItems:(BOOL)on;
61 @interface NSNumber (MMExtras)
67 @implementation MMAppController
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,
90 [[NSUserDefaults standardUserDefaults] registerDefaults:dict];
92 NSArray *types = [NSArray arrayWithObject:NSStringPboardType];
93 [NSApp registerServicesMenuSendTypes:types returnTypes:types];
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];
119 NSLog(@"WARNING: Failed to register connection with name '%@'",
129 //NSLog(@"MMAppController dealloc");
131 [vimControllers release];
132 [openSelectionString release];
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];
159 - (void)application:(NSApplication *)sender openFiles:(NSArray *)filenames
161 filenames = [self filterOpenFilesAndRaiseFirst:filenames];
162 if ([filenames count]) {
164 BOOL openInTabs = [[NSUserDefaults standardUserDefaults]
165 boolForKey:MMOpenFilesInTabsKey];
167 if (openInTabs && (vc = [self topmostVimController])) {
168 [vc dropFiles:filenames];
170 NSMutableArray *args = [NSMutableArray arrayWithObject:@"-p"];
171 [args addObjectsFromArray:filenames];
172 [self launchVimProcessWithArguments:args];
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];
201 while (window = [e nextObject]) {
202 if ([window isDocumentEdited]) {
203 modifiedBuffers = YES;
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;
226 - (void)applicationWillTerminate:(NSNotification *)aNotification
228 // This will invalidate all connections (since they were spawned from the
229 // default connection).
230 [[NSConnection defaultConnection] invalidate];
232 // Send a SIGINT to all running Vim processes, so that they are sure to
233 // receive the connectionDidDie: notification (a process has to be checking
234 // the run-loop for this to happen).
235 unsigned i, count = [vimControllers count];
236 for (i = 0; i < count; ++i) {
237 MMVimController *controller = [vimControllers objectAtIndex:i];
238 int pid = [controller pid];
243 if (fontContainerRef) {
244 ATSFontDeactivate(fontContainerRef, NULL, kATSOptionFlagsDefault);
245 fontContainerRef = 0;
248 // TODO: Is this a correct way of releasing the MMAppController?
249 // (It doesn't seem like dealloc is ever called.)
250 [NSApp setDelegate:nil];
254 - (void)removeVimController:(id)controller
256 //NSLog(@"%s%@", _cmd, controller);
258 [[controller windowController] close];
260 [vimControllers removeObject:controller];
262 if (![vimControllers count]) {
263 // Turn on autoenabling of menus (because no Vim is open to handle it),
264 // but do not touch the MacVim menu. Note that the menus must be
265 // enabled first otherwise autoenabling does not work.
266 NSMenu *mainMenu = [NSApp mainMenu];
267 int i, count = [mainMenu numberOfItems];
268 for (i = 1; i < count; ++i) {
269 NSMenuItem *item = [mainMenu itemAtIndex:i];
270 [item setEnabled:YES];
271 [[item submenu] recurseSetAutoenablesItems:YES];
276 - (void)windowControllerWillOpen:(MMWindowController *)windowController
278 NSPoint topLeft = NSZeroPoint;
279 NSWindow *keyWin = [NSApp keyWindow];
280 NSWindow *win = [windowController window];
284 // If there is a key window, cascade from it, otherwise use the autosaved
285 // window position (if any).
287 NSRect frame = [keyWin frame];
288 topLeft = NSMakePoint(frame.origin.x, NSMaxY(frame));
290 NSString *topLeftString = [[NSUserDefaults standardUserDefaults]
291 stringForKey:MMTopLeftPointKey];
293 topLeft = NSPointFromString(topLeftString);
296 if (!NSEqualPoints(topLeft, NSZeroPoint)) {
298 topLeft = [win cascadeTopLeftFromPoint:topLeft];
300 [win setFrameTopLeftPoint:topLeft];
303 if (openSelectionString) {
304 // There is some text to paste into this window as a result of the
305 // services menu "Open selection ..." being used.
306 [[windowController vimController] dropString:openSelectionString];
307 [openSelectionString release];
308 openSelectionString = nil;
312 - (IBAction)newWindow:(id)sender
314 [self launchVimProcessWithArguments:nil];
317 - (IBAction)selectNextWindow:(id)sender
319 unsigned i, count = [vimControllers count];
322 NSWindow *keyWindow = [NSApp keyWindow];
323 for (i = 0; i < count; ++i) {
324 MMVimController *vc = [vimControllers objectAtIndex:i];
325 if ([[[vc windowController] window] isEqual:keyWindow])
332 MMVimController *vc = [vimControllers objectAtIndex:i];
333 [[vc windowController] showWindow:self];
337 - (IBAction)selectPreviousWindow:(id)sender
339 unsigned i, count = [vimControllers count];
342 NSWindow *keyWindow = [NSApp keyWindow];
343 for (i = 0; i < count; ++i) {
344 MMVimController *vc = [vimControllers objectAtIndex:i];
345 if ([[[vc windowController] window] isEqual:keyWindow])
355 MMVimController *vc = [vimControllers objectAtIndex:i];
356 [[vc windowController] showWindow:self];
360 - (IBAction)fontSizeUp:(id)sender
362 [[NSFontManager sharedFontManager] modifyFont:
363 [NSNumber numberWithInt:NSSizeUpFontAction]];
366 - (IBAction)fontSizeDown:(id)sender
368 [[NSFontManager sharedFontManager] modifyFont:
369 [NSNumber numberWithInt:NSSizeDownFontAction]];
372 - (byref id <MMFrontendProtocol>)
373 connectBackend:(byref in id <MMBackendProtocol>)backend
376 //NSLog(@"Frontend got connection request from backend...adding new "
377 // "MMVimController");
379 [(NSDistantObject*)backend
380 setProtocolForProxy:@protocol(MMBackendProtocol)];
382 MMVimController *vc = [[[MMVimController alloc]
383 initWithBackend:backend pid:pid] autorelease];
385 if (![vimControllers count]) {
386 // The first window autosaves its position. (The autosaving features
387 // of Cocoa are not used because we need more control over what is
388 // autosaved and when it is restored.)
389 [[vc windowController] setWindowAutosaveKey:MMTopLeftPointKey];
392 [vimControllers addObject:vc];
394 // HACK! MacVim does not get activated if it is launched from the
395 // terminal, so we forcibly activate here unless it is an untitled window
396 // opening (i.e. MacVim was opened from the Finder). Untitled windows are
397 // treated differently, else MacVim would steal the focus if another app
398 // was activated while the untitled window was loading.
399 if (!untitledWindowOpening)
400 [NSApp activateIgnoringOtherApps:YES];
402 untitledWindowOpening = NO;
404 NSLog(@"connectBackend:pid: done.");
408 - (NSArray *)serverList
410 NSMutableArray *array = [NSMutableArray array];
412 unsigned i, count = [vimControllers count];
413 for (i = 0; i < count; ++i) {
414 MMVimController *controller = [vimControllers objectAtIndex:i];
415 if ([controller serverName])
416 [array addObject:[controller serverName]];
422 @end // MMAppController
427 @implementation MMAppController (MMServices)
429 - (void)openSelection:(NSPasteboard *)pboard userData:(NSString *)userData
430 error:(NSString **)error
432 if (![[pboard types] containsObject:NSStringPboardType]) {
433 NSLog(@"WARNING: Pasteboard contains no object of type "
434 "NSStringPboardType");
438 MMVimController *vc = [self topmostVimController];
440 // Open a new tab first, since dropString: does not do this.
441 [vc sendMessage:AddNewTabMsgID data:nil];
442 [vc dropString:[pboard stringForType:NSStringPboardType]];
444 // NOTE: There is no window to paste the selection into, so save the
445 // text, open a new window, and paste the text when the next window
446 // opens. (If this is called several times in a row, then all but the
447 // last call might be ignored.)
448 if (openSelectionString) [openSelectionString release];
449 openSelectionString = [[pboard stringForType:NSStringPboardType] copy];
451 [self newWindow:self];
455 - (void)openFile:(NSPasteboard *)pboard userData:(NSString *)userData
456 error:(NSString **)error
458 if (![[pboard types] containsObject:NSStringPboardType]) {
459 NSLog(@"WARNING: Pasteboard contains no object of type "
460 "NSStringPboardType");
464 // TODO: Parse multiple filenames and create array with names.
465 NSString *string = [pboard stringForType:NSStringPboardType];
466 string = [string stringByTrimmingCharactersInSet:
467 [NSCharacterSet whitespaceAndNewlineCharacterSet]];
468 string = [string stringByStandardizingPath];
470 NSArray *filenames = [self filterFilesAndNotify:
471 [NSArray arrayWithObject:string]];
472 if ([filenames count] > 0) {
473 MMVimController *vc = nil;
474 if (userData && [userData isEqual:@"Tab"])
475 vc = [self topmostVimController];
478 [vc dropFiles:filenames];
480 [self application:NSApp openFiles:filenames];
485 @end // MMAppController (MMServices)
490 @implementation MMAppController (Private)
492 - (MMVimController *)keyVimController
494 NSWindow *keyWindow = [NSApp keyWindow];
496 unsigned i, count = [vimControllers count];
497 for (i = 0; i < count; ++i) {
498 MMVimController *vc = [vimControllers objectAtIndex:i];
499 if ([[[vc windowController] window] isEqual:keyWindow])
507 - (MMVimController *)topmostVimController
509 NSArray *windows = [NSApp orderedWindows];
510 if ([windows count] > 0) {
511 NSWindow *window = [windows objectAtIndex:0];
512 unsigned i, count = [vimControllers count];
513 for (i = 0; i < count; ++i) {
514 MMVimController *vc = [vimControllers objectAtIndex:i];
515 if ([[[vc windowController] window] isEqual:window])
523 - (void)launchVimProcessWithArguments:(NSArray *)args
525 NSString *taskPath = nil;
526 NSArray *taskArgs = nil;
527 NSString *path = [[NSBundle mainBundle] pathForAuxiliaryExecutable:@"Vim"];
530 NSLog(@"ERROR: Vim executable could not be found inside app bundle!");
534 if ([[NSUserDefaults standardUserDefaults] boolForKey:MMLoginShellKey]) {
535 // Run process with a login shell
536 // $SHELL -l -c "exec Vim args"
538 NSMutableString *execArg = [NSMutableString
539 stringWithFormat:@"exec \"%@\" -g", path];
541 // Append all arguments while making sure that arguments containing
542 // spaces are enclosed in quotes.
543 NSCharacterSet *space = [NSCharacterSet whitespaceCharacterSet];
544 unsigned i, count = [args count];
546 for (i = 0; i < count; ++i) {
547 NSString *arg = [args objectAtIndex:i];
548 if (NSNotFound != [arg rangeOfCharacterFromSet:space].location)
549 [execArg appendFormat:@" \"%@\"", arg];
551 [execArg appendFormat:@" %@", arg];
555 // Launch the process with a login shell so that users environment
556 // settings get sourced. This does not always happen when MacVim is
558 taskArgs = [NSArray arrayWithObjects:@"-l", @"-c", execArg, nil];
559 taskPath = [[[NSProcessInfo processInfo] environment]
560 objectForKey:@"SHELL"];
562 taskPath = @"/bin/sh";
564 // Run process directly:
567 taskArgs = [NSArray arrayWithObject:@"-g"];
569 taskArgs = [taskArgs arrayByAddingObjectsFromArray:args];
572 //NSLog(@"Launching: %@ args: %@", taskPath, taskArgs);
573 [NSTask launchedTaskWithLaunchPath:taskPath arguments:taskArgs];
576 - (NSArray *)filterFilesAndNotify:(NSArray *)filenames
578 // Go trough 'filenames' array and make sure each file exists. Present
579 // warning dialog if some file was missing.
581 NSString *firstMissingFile = nil;
582 NSMutableArray *files = [NSMutableArray array];
583 unsigned i, count = [filenames count];
585 for (i = 0; i < count; ++i) {
586 NSString *name = [filenames objectAtIndex:i];
587 if ([[NSFileManager defaultManager] fileExistsAtPath:name]) {
588 [files addObject:name];
589 } else if (!firstMissingFile) {
590 firstMissingFile = name;
594 if (firstMissingFile) {
595 NSAlert *alert = [[NSAlert alloc] init];
596 [alert addButtonWithTitle:@"OK"];
599 if ([files count] >= count-1) {
600 [alert setMessageText:@"File not found"];
601 text = [NSString stringWithFormat:@"Could not open file with "
602 "name %@.", firstMissingFile];
604 [alert setMessageText:@"Multiple files not found"];
605 text = [NSString stringWithFormat:@"Could not open file with "
606 "name %@, and %d other files.", firstMissingFile,
607 count-[files count]-1];
610 [alert setInformativeText:text];
611 [alert setAlertStyle:NSWarningAlertStyle];
616 [NSApp replyToOpenOrPrint:NSApplicationDelegateReplyFailure];
622 - (NSArray *)filterOpenFilesAndRaiseFirst:(NSArray *)filenames
624 // Check if any of the files in the 'filenames' array are open in any Vim
625 // process. Remove the files that are open from the 'filenames' array and
626 // return it. If all files were filtered out, then raise the first file in
627 // the Vim process it is open.
629 MMVimController *raiseController = nil;
630 NSString *raiseFile = nil;
631 NSMutableArray *files = [filenames mutableCopy];
632 NSString *expr = [NSString stringWithFormat:
633 @"map([\"%@\"],\"bufloaded(v:val)\")",
634 [files componentsJoinedByString:@"\",\""]];
635 unsigned i, count = [vimControllers count];
637 for (i = 0; i < count && [files count]; ++i) {
638 MMVimController *controller = [vimControllers objectAtIndex:i];
639 id proxy = [controller backendProxy];
642 NSString *eval = [proxy evaluateExpression:expr];
643 NSIndexSet *idxSet = [NSIndexSet indexSetWithVimList:eval];
644 if ([idxSet count]) {
646 // Remember the file and which Vim that has it open so that
647 // we can raise it later on.
648 raiseController = controller;
649 raiseFile = [files objectAtIndex:[idxSet firstIndex]];
650 [[raiseFile retain] autorelease];
653 // Remove all the files that were open in this Vim process and
654 // create a new expression to evaluate.
655 [files removeObjectsAtIndexes:idxSet];
656 expr = [NSString stringWithFormat:
657 @"map([\"%@\"],\"bufloaded(v:val)\")",
658 [files componentsJoinedByString:@"\",\""]];
661 @catch (NSException *e) {
666 if (![files count] && raiseFile) {
667 // Raise the window containing the first file that was already open,
668 // and make sure that the tab containing that file is selected. Only
669 // do this if there are no more files to open, otherwise sometimes the
670 // window with 'raiseFile' will be raised, other times it might be the
671 // window that will open with the files in the 'files' array.
672 raiseFile = [raiseFile stringByEscapingSpecialFilenameCharacters];
673 NSString *input = [NSString stringWithFormat:@"<C-\\><C-N>"
674 ":let oldswb=&swb|let &swb=\"useopen,usetab\"|"
675 "tab sb %@|let &swb=oldswb|unl oldswb|"
676 "cal foreground()|redr|f<CR>", raiseFile];
677 [raiseController addVimInput:input];
683 @end // MMAppController (Private)
688 @implementation NSMenu (MMExtras)
690 - (void)recurseSetAutoenablesItems:(BOOL)on
692 [self setAutoenablesItems:on];
694 int i, count = [self numberOfItems];
695 for (i = 0; i < count; ++i) {
696 NSMenuItem *item = [self itemAtIndex:i];
697 [item setEnabled:YES];
698 NSMenu *submenu = [item submenu];
700 [submenu recurseSetAutoenablesItems:on];
705 @end // NSMenu (MMExtras)
710 @implementation NSNumber (MMExtras)
713 return [self intValue];
715 @end // NSNumber (MMExtras)