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,
89 [[NSUserDefaults standardUserDefaults] registerDefaults:dict];
91 NSArray *types = [NSArray arrayWithObject:NSStringPboardType];
92 [NSApp registerServicesMenuSendTypes:types returnTypes:types];
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];
118 NSLog(@"WARNING: Failed to register connection with name '%@'",
128 //NSLog(@"MMAppController dealloc");
130 [vimControllers release];
131 [openSelectionString release];
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];
158 - (void)application:(NSApplication *)sender openFiles:(NSArray *)filenames
160 filenames = [self filterOpenFilesAndRaiseFirst:filenames];
161 if ([filenames count]) {
163 BOOL openInTabs = [[NSUserDefaults standardUserDefaults]
164 boolForKey:MMOpenFilesInTabsKey];
166 if (openInTabs && (vc = [self topmostVimController])) {
167 [vc dropFiles:filenames];
169 NSMutableArray *args = [NSMutableArray arrayWithObject:@"-p"];
170 [args addObjectsFromArray:filenames];
171 [self launchVimProcessWithArguments:args];
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];
200 while (window = [e nextObject]) {
201 if ([window isDocumentEdited]) {
202 modifiedBuffers = YES;
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;
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];
242 if (fontContainerRef) {
243 ATSFontDeactivate(fontContainerRef, NULL, kATSOptionFlagsDefault);
244 fontContainerRef = 0;
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];
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];
275 - (void)windowControllerWillOpen:(MMWindowController *)windowController
277 NSPoint topLeft = NSZeroPoint;
278 NSWindow *keyWin = [NSApp keyWindow];
279 NSWindow *win = [windowController window];
283 // If there is a key window, cascade from it, otherwise use the autosaved
284 // window position (if any).
286 NSRect frame = [keyWin frame];
287 topLeft = NSMakePoint(frame.origin.x, NSMaxY(frame));
289 NSString *topLeftString = [[NSUserDefaults standardUserDefaults]
290 stringForKey:MMTopLeftPointKey];
292 topLeft = NSPointFromString(topLeftString);
295 if (!NSEqualPoints(topLeft, NSZeroPoint)) {
297 topLeft = [win cascadeTopLeftFromPoint:topLeft];
299 [win setFrameTopLeftPoint:topLeft];
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;
311 - (IBAction)newWindow:(id)sender
313 [self launchVimProcessWithArguments:nil];
316 - (IBAction)selectNextWindow:(id)sender
318 unsigned i, count = [vimControllers count];
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])
331 MMVimController *vc = [vimControllers objectAtIndex:i];
332 [[vc windowController] showWindow:self];
336 - (IBAction)selectPreviousWindow:(id)sender
338 unsigned i, count = [vimControllers count];
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])
354 MMVimController *vc = [vimControllers objectAtIndex:i];
355 [[vc windowController] showWindow:self];
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
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];
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;
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]];
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");
436 MMVimController *vc = [self topmostVimController];
438 // Open a new tab first, since dropString: does not do this.
439 [vc sendMessage:AddNewTabMsgID data:nil];
440 [vc dropString:[pboard stringForType:NSStringPboardType]];
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];
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");
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];
476 [vc dropFiles:filenames];
478 [self application:NSApp openFiles:filenames];
483 @end // MMAppController (MMServices)
488 @implementation MMAppController (Private)
490 - (MMVimController *)keyVimController
492 NSWindow *keyWindow = [NSApp 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])
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])
521 - (void)launchVimProcessWithArguments:(NSArray *)args
523 NSString *path = [[NSBundle mainBundle] pathForAuxiliaryExecutable:@"Vim"];
525 NSLog(@"ERROR: Vim executable could not be found inside app bundle!");
529 NSMutableString *execArg = [NSMutableString
530 stringWithFormat:@"exec \"%@\" -g", path];
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];
542 [execArg appendFormat:@" %@", arg];
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"];
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;
576 if (firstMissingFile) {
577 NSAlert *alert = [[NSAlert alloc] init];
578 [alert addButtonWithTitle:@"OK"];
581 if ([files count] >= count-1) {
582 [alert setMessageText:@"File not found"];
583 text = [NSString stringWithFormat:@"Could not open file with "
584 "name %@.", firstMissingFile];
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];
592 [alert setInformativeText:text];
593 [alert setAlertStyle:NSWarningAlertStyle];
598 [NSApp replyToOpenOrPrint:NSApplicationDelegateReplyFailure];
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];
624 NSString *eval = [proxy evaluateExpression:expr];
625 NSIndexSet *idxSet = [NSIndexSet indexSetWithVimList:eval];
626 if ([idxSet count]) {
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];
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:@"\",\""]];
643 @catch (NSException *e) {
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];
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];
682 [submenu recurseSetAutoenablesItems:on];
687 @end // NSMenu (MMExtras)
692 @implementation NSNumber (MMExtras)
695 return [self intValue];
697 @end // NSNumber (MMExtras)