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;
223 // Tell all Vim processes to terminate now (otherwise they'll leave swap
225 if (NSTerminateNow == reply) {
226 e = [vimControllers objectEnumerator];
228 while ((vc = [e nextObject]))
229 [vc sendMessage:TerminateNowMsgID data:nil];
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];
252 if (fontContainerRef) {
253 ATSFontDeactivate(fontContainerRef, NULL, kATSOptionFlagsDefault);
254 fontContainerRef = 0;
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];
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];
285 - (void)windowControllerWillOpen:(MMWindowController *)windowController
287 NSPoint topLeft = NSZeroPoint;
288 NSWindow *keyWin = [NSApp keyWindow];
289 NSWindow *win = [windowController window];
293 // If there is a key window, cascade from it, otherwise use the autosaved
294 // window position (if any).
296 NSRect frame = [keyWin frame];
297 topLeft = NSMakePoint(frame.origin.x, NSMaxY(frame));
299 NSString *topLeftString = [[NSUserDefaults standardUserDefaults]
300 stringForKey:MMTopLeftPointKey];
302 topLeft = NSPointFromString(topLeftString);
305 if (!NSEqualPoints(topLeft, NSZeroPoint)) {
307 topLeft = [win cascadeTopLeftFromPoint:topLeft];
309 [win setFrameTopLeftPoint:topLeft];
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;
321 - (IBAction)newWindow:(id)sender
323 [self launchVimProcessWithArguments:nil];
326 - (IBAction)selectNextWindow:(id)sender
328 unsigned i, count = [vimControllers count];
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])
341 MMVimController *vc = [vimControllers objectAtIndex:i];
342 [[vc windowController] showWindow:self];
346 - (IBAction)selectPreviousWindow:(id)sender
348 unsigned i, count = [vimControllers count];
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])
364 MMVimController *vc = [vimControllers objectAtIndex:i];
365 [[vc windowController] showWindow:self];
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
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];
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;
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]];
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");
446 MMVimController *vc = [self topmostVimController];
448 // Open a new tab first, since dropString: does not do this.
449 [vc sendMessage:AddNewTabMsgID data:nil];
450 [vc dropString:[pboard stringForType:NSStringPboardType]];
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];
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");
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];
486 [vc dropFiles:filenames];
488 [self application:NSApp openFiles:filenames];
493 @end // MMAppController (MMServices)
498 @implementation MMAppController (Private)
500 - (MMVimController *)keyVimController
502 NSWindow *keyWindow = [NSApp 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])
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])
531 - (void)launchVimProcessWithArguments:(NSArray *)args
533 NSString *taskPath = nil;
534 NSArray *taskArgs = nil;
535 NSString *path = [[NSBundle mainBundle] pathForAuxiliaryExecutable:@"Vim"];
538 NSLog(@"ERROR: Vim executable could not be found inside app bundle!");
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];
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];
559 [execArg appendFormat:@" %@", arg];
563 // Launch the process with a login shell so that users environment
564 // settings get sourced. This does not always happen when MacVim is
566 taskArgs = [NSArray arrayWithObjects:@"-l", @"-c", execArg, nil];
567 taskPath = [[[NSProcessInfo processInfo] environment]
568 objectForKey:@"SHELL"];
570 taskPath = @"/bin/sh";
572 // Run process directly:
575 taskArgs = [NSArray arrayWithObject:@"-g"];
577 taskArgs = [taskArgs arrayByAddingObjectsFromArray:args];
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;
602 if (firstMissingFile) {
603 NSAlert *alert = [[NSAlert alloc] init];
604 [alert addButtonWithTitle:@"OK"];
607 if ([files count] >= count-1) {
608 [alert setMessageText:@"File not found"];
609 text = [NSString stringWithFormat:@"Could not open file with "
610 "name %@.", firstMissingFile];
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];
618 [alert setInformativeText:text];
619 [alert setAlertStyle:NSWarningAlertStyle];
624 [NSApp replyToOpenOrPrint:NSApplicationDelegateReplyFailure];
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];
650 NSString *eval = [proxy evaluateExpression:expr];
651 NSIndexSet *idxSet = [NSIndexSet indexSetWithVimList:eval];
652 if ([idxSet count]) {
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];
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:@"\",\""]];
669 @catch (NSException *e) {
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];
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];
708 [submenu recurseSetAutoenablesItems:on];
713 @end // NSMenu (MMExtras)
718 @implementation NSNumber (MMExtras)
721 return [self intValue];
723 @end // NSNumber (MMExtras)