Add eval & add input methods to MacVim, apply to file open
[MacVim.git] / src / MacVim / MMAppController.m
blobf6815a791959d68fff0154c7d3d945c6e851ebcb
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 #import "MMAppController.h"
12 #import "MMVimController.h"
13 #import "MMWindowController.h"
17 // Default timeout intervals on all connections.
18 static NSTimeInterval MMRequestTimeout = 5;
19 static NSTimeInterval MMReplyTimeout = 5;
23 @interface MMAppController (MMServices)
24 - (void)openSelection:(NSPasteboard *)pboard userData:(NSString *)userData
25                 error:(NSString **)error;
26 - (void)openFile:(NSPasteboard *)pboard userData:(NSString *)userData
27            error:(NSString **)error;
28 @end
31 @interface MMAppController (Private)
32 - (MMVimController *)keyVimController;
33 - (MMVimController *)topmostVimController;
34 - (void)launchVimProcessWithArguments:(NSArray *)args;
35 @end
37 @interface NSMenu (MMExtras)
38 - (void)recurseSetAutoenablesItems:(BOOL)on;
39 @end
41 @interface NSNumber (MMExtras)
42 - (int)tag;
43 @end
47 @implementation MMAppController
49 + (void)initialize
51     NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:
52         [NSNumber numberWithBool:NO],   MMNoWindowKey,
53         [NSNumber numberWithInt:64],    MMTabMinWidthKey,
54         [NSNumber numberWithInt:6*64],  MMTabMaxWidthKey,
55         [NSNumber numberWithInt:132],   MMTabOptimumWidthKey,
56         [NSNumber numberWithInt:2],     MMTextInsetLeftKey,
57         [NSNumber numberWithInt:1],     MMTextInsetRightKey,
58         [NSNumber numberWithInt:1],     MMTextInsetTopKey,
59         [NSNumber numberWithInt:1],     MMTextInsetBottomKey,
60         [NSNumber numberWithBool:NO],   MMTerminateAfterLastWindowClosedKey,
61         @"MMTypesetter",                MMTypesetterKey,
62         [NSNumber numberWithFloat:1],   MMCellWidthMultiplierKey,
63         [NSNumber numberWithFloat:-1],  MMBaselineOffsetKey,
64         [NSNumber numberWithBool:YES],  MMTranslateCtrlClickKey,
65         [NSNumber numberWithBool:NO],   MMOpenFilesInTabsKey,
66         nil];
68     [[NSUserDefaults standardUserDefaults] registerDefaults:dict];
70     NSArray *types = [NSArray arrayWithObject:NSStringPboardType];
71     [NSApp registerServicesMenuSendTypes:types returnTypes:types];
74 - (id)init
76     if ((self = [super init])) {
77         fontContainerRef = loadFonts();
79         vimControllers = [NSMutableArray new];
81         // NOTE!  If the name of the connection changes here it must also be
82         // updated in MMBackend.m.
83         NSConnection *connection = [NSConnection defaultConnection];
84         NSString *name = [NSString stringWithFormat:@"%@-connection",
85                  [[NSBundle mainBundle] bundleIdentifier]];
86         //NSLog(@"Registering connection with name '%@'", name);
87         if ([connection registerName:name]) {
88             [connection setRequestTimeout:MMRequestTimeout];
89             [connection setReplyTimeout:MMReplyTimeout];
90             [connection setRootObject:self];
92             // NOTE: When the user is resizing the window the AppKit puts the
93             // run loop in event tracking mode.  Unless the connection listens
94             // to request in this mode, live resizing won't work.
95             [connection addRequestMode:NSEventTrackingRunLoopMode];
96         } else {
97             NSLog(@"WARNING: Failed to register connection with name '%@'",
98                     name);
99         }
100     }
102     return self;
105 - (void)dealloc
107     //NSLog(@"MMAppController dealloc");
109     [vimControllers release];
110     [openSelectionString release];
112     [super dealloc];
115 - (void)applicationDidFinishLaunching:(NSNotification *)notification
117     [NSApp setServicesProvider:self];
120 - (BOOL)applicationShouldOpenUntitledFile:(NSApplication *)sender
122     // NOTE!  This way it possible to start the app with the command-line
123     // argument '-nowindow yes' and no window will be opened by default.
124     untitledWindowOpening =
125         ![[NSUserDefaults standardUserDefaults] boolForKey:MMNoWindowKey];
126     return untitledWindowOpening;
129 - (BOOL)applicationOpenUntitledFile:(NSApplication *)sender
131     //NSLog(@"%s NSapp=%@ theApp=%@", _cmd, NSApp, sender);
133     [self newWindow:self];
134     return YES;
137 - (void)application:(NSApplication *)sender openFiles:(NSArray *)filenames
139     // Go trough 'filenames' array and make sure each file exists.
140     NSString *firstMissingFile = nil;
141     NSMutableArray *files = [NSMutableArray array];
142     int i, count = [filenames count];
143     for (i = 0; i < count; ++i) {
144         NSString *name = [filenames objectAtIndex:i];
145         if ([[NSFileManager defaultManager] fileExistsAtPath:name]) {
146             [files addObject:name];
147         } else if (!firstMissingFile) {
148             firstMissingFile = name;
149         }
150     }
152     if (firstMissingFile) {
153         NSAlert *alert = [[NSAlert alloc] init];
154         [alert addButtonWithTitle:@"OK"];
156         NSString *text;
157         if ([files count] >= count-1) {
158             [alert setMessageText:@"File not found"];
159             text = [NSString stringWithFormat:@"Could not open file with "
160                 "name %@.", firstMissingFile];
161         } else {
162             [alert setMessageText:@"Multiple files not found"];
163             text = [NSString stringWithFormat:@"Could not open file with "
164                 "name %@, and %d other files.", firstMissingFile,
165                 count-[files count]-1];
166         }
168         [alert setInformativeText:text];
169         [alert setAlertStyle:NSWarningAlertStyle];
171         [alert runModal];
172         [alert release];
174         [NSApp replyToOpenOrPrint:NSApplicationDelegateReplyFailure];
175         return;
176     }
178     if ([files count] == 1) {
179         // Check if the file is already open...if so raise that window.
180         i, count = [vimControllers count];
181         for (i = 0; i < count; ++i) {
182             MMVimController *controller = [vimControllers objectAtIndex:i];
183             id proxy = [controller backendProxy];
185             @try {
186                 NSString *expr = [NSString stringWithFormat:
187                         @"bufloaded(\"%@\")", [files objectAtIndex:0]];
188                 NSString *eval = [proxy evaluateExpression:expr];
189                 if ([eval isEqual:@"1"]) {
190                     // TODO: Select the tab with 'file' open.
191                     [controller addVimInput:
192                                 @"<C-\\><C-N>:cal foreground()<CR>"];
193                     return;
194                 }
195             }
196             @catch (NSException *e) {
197                 // Do nothing ...
198             }
199         }
200     }
202     MMVimController *vc;
203     BOOL openInTabs = [[NSUserDefaults standardUserDefaults]
204         boolForKey:MMOpenFilesInTabsKey];
206     if (openInTabs && (vc = [self topmostVimController])) {
207         [vc dropFiles:files];
208     } else {
209         NSMutableArray *args = [NSMutableArray arrayWithObject:@"-p"];
210         [args addObjectsFromArray:files];
211         [self launchVimProcessWithArguments:args];
212     }
214     [NSApp replyToOpenOrPrint:NSApplicationDelegateReplySuccess];
215     // NSApplicationDelegateReplySuccess = 0,
216     // NSApplicationDelegateReplyCancel = 1,
217     // NSApplicationDelegateReplyFailure = 2
220 - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender
222     return [[NSUserDefaults standardUserDefaults]
223             boolForKey:MMTerminateAfterLastWindowClosedKey];
226 - (NSApplicationTerminateReply)applicationShouldTerminate:
227     (NSApplication *)sender
229     // TODO: Follow Apple's guidelines for 'Graceful Application Termination'
230     // (in particular, allow user to review changes and save).
231     int reply = NSTerminateNow;
232     BOOL modifiedBuffers = NO;
234     // Go through windows, checking for modified buffers.  (Each Vim process
235     // tells MacVim when any buffer has been modified and MacVim sets the
236     // 'documentEdited' flag of the window correspondingly.)
237     NSEnumerator *e = [[NSApp windows] objectEnumerator];
238     id window;
239     while (window = [e nextObject]) {
240         if ([window isDocumentEdited]) {
241             modifiedBuffers = YES;
242             break;
243         }
244     }
246     if (modifiedBuffers) {
247         NSAlert *alert = [[NSAlert alloc] init];
248         [alert addButtonWithTitle:@"Quit"];
249         [alert addButtonWithTitle:@"Cancel"];
250         [alert setMessageText:@"Quit without saving?"];
251         [alert setInformativeText:@"There are modified buffers, "
252             "if you quit now all changes will be lost.  Quit anyway?"];
253         [alert setAlertStyle:NSWarningAlertStyle];
255         if ([alert runModal] != NSAlertFirstButtonReturn)
256             reply = NSTerminateCancel;
258         [alert release];
259     }
261     return reply;
264 - (void)applicationWillTerminate:(NSNotification *)aNotification
266     // This will invalidate all connections (since they were spawned from the
267     // default connection).
268     [[NSConnection defaultConnection] invalidate];
270     // Send a SIGINT to all running Vim processes, so that they are sure to
271     // receive the connectionDidDie: notification (a process has to be checking
272     // the run-loop for this to happen).
273     unsigned i, count = [vimControllers count];
274     for (i = 0; i < count; ++i) {
275         MMVimController *controller = [vimControllers objectAtIndex:i];
276         int pid = [controller pid];
277         if (pid > 0)
278             kill(pid, SIGINT);
279     }
281     if (fontContainerRef) {
282         ATSFontDeactivate(fontContainerRef, NULL, kATSOptionFlagsDefault);
283         fontContainerRef = 0;
284     }
286     // TODO: Is this a correct way of releasing the MMAppController?
287     // (It doesn't seem like dealloc is ever called.)
288     [NSApp setDelegate:nil];
289     [self autorelease];
292 - (void)removeVimController:(id)controller
294     //NSLog(@"%s%@", _cmd, controller);
296     [[controller windowController] close];
298     [vimControllers removeObject:controller];
300     if (![vimControllers count]) {
301         // Turn on autoenabling of menus (because no Vim is open to handle it),
302         // but do not touch the MacVim menu.  Note that the menus must be
303         // enabled first otherwise autoenabling does not work.
304         NSMenu *mainMenu = [NSApp mainMenu];
305         int i, count = [mainMenu numberOfItems];
306         for (i = 1; i < count; ++i) {
307             NSMenuItem *item = [mainMenu itemAtIndex:i];
308             [item setEnabled:YES];
309             [[item submenu] recurseSetAutoenablesItems:YES];
310         }
311     }
314 - (void)windowControllerWillOpen:(MMWindowController *)windowController
316     NSPoint topLeft = NSZeroPoint;
317     NSWindow *keyWin = [NSApp keyWindow];
318     NSWindow *win = [windowController window];
320     if (!win) return;
322     // If there is a key window, cascade from it, otherwise use the autosaved
323     // window position (if any).
324     if (keyWin) {
325         NSRect frame = [keyWin frame];
326         topLeft = NSMakePoint(frame.origin.x, NSMaxY(frame));
327     } else {
328         NSString *topLeftString = [[NSUserDefaults standardUserDefaults]
329             stringForKey:MMTopLeftPointKey];
330         if (topLeftString)
331             topLeft = NSPointFromString(topLeftString);
332     }
334     if (!NSEqualPoints(topLeft, NSZeroPoint)) {
335         if (keyWin)
336             topLeft = [win cascadeTopLeftFromPoint:topLeft];
338         [win setFrameTopLeftPoint:topLeft];
339     }
341     if (openSelectionString) {
342         // There is some text to paste into this window as a result of the
343         // services menu "Open selection ..." being used.
344         [[windowController vimController] dropString:openSelectionString];
345         [openSelectionString release];
346         openSelectionString = nil;
347     }
350 - (IBAction)newWindow:(id)sender
352     [self launchVimProcessWithArguments:nil];
355 - (IBAction)selectNextWindow:(id)sender
357     unsigned i, count = [vimControllers count];
358     if (!count) return;
360     NSWindow *keyWindow = [NSApp keyWindow];
361     for (i = 0; i < count; ++i) {
362         MMVimController *vc = [vimControllers objectAtIndex:i];
363         if ([[[vc windowController] window] isEqual:keyWindow])
364             break;
365     }
367     if (i < count) {
368         if (++i >= count)
369             i = 0;
370         MMVimController *vc = [vimControllers objectAtIndex:i];
371         [[vc windowController] showWindow:self];
372     }
375 - (IBAction)selectPreviousWindow:(id)sender
377     unsigned i, count = [vimControllers count];
378     if (!count) return;
380     NSWindow *keyWindow = [NSApp keyWindow];
381     for (i = 0; i < count; ++i) {
382         MMVimController *vc = [vimControllers objectAtIndex:i];
383         if ([[[vc windowController] window] isEqual:keyWindow])
384             break;
385     }
387     if (i < count) {
388         if (i > 0) {
389             --i;
390         } else {
391             i = count - 1;
392         }
393         MMVimController *vc = [vimControllers objectAtIndex:i];
394         [[vc windowController] showWindow:self];
395     }
398 - (IBAction)fontSizeUp:(id)sender
400     [[NSFontManager sharedFontManager] modifyFont:
401             [NSNumber numberWithInt:NSSizeUpFontAction]];
404 - (IBAction)fontSizeDown:(id)sender
406     [[NSFontManager sharedFontManager] modifyFont:
407             [NSNumber numberWithInt:NSSizeDownFontAction]];
410 - (byref id <MMFrontendProtocol>)
411     connectBackend:(byref in id <MMBackendProtocol>)backend
412                pid:(int)pid
414     //NSLog(@"Frontend got connection request from backend...adding new "
415     //        "MMVimController");
417     [(NSDistantObject*)backend
418             setProtocolForProxy:@protocol(MMBackendProtocol)];
420     MMVimController *vc = [[[MMVimController alloc]
421             initWithBackend:backend pid:pid] autorelease];
423     if (![vimControllers count]) {
424         // The first window autosaves its position.  (The autosaving features
425         // of Cocoa are not used because we need more control over what is
426         // autosaved and when it is restored.)
427         [[vc windowController] setWindowAutosaveKey:MMTopLeftPointKey];
428     }
430     [vimControllers addObject:vc];
432     // HACK!  MacVim does not get activated if it is launched from the
433     // terminal, so we forcibly activate here unless it is an untitled window
434     // opening (i.e. MacVim was opened from the Finder).  Untitled windows are
435     // treated differently, else MacVim would steal the focus if another app
436     // was activated while the untitled window was loading.
437     if (!untitledWindowOpening)
438         [NSApp activateIgnoringOtherApps:YES];
440     untitledWindowOpening = NO;
442     return vc;
445 - (NSArray *)serverList
447     NSMutableArray *array = [NSMutableArray array];
449     unsigned i, count = [vimControllers count];
450     for (i = 0; i < count; ++i) {
451         MMVimController *controller = [vimControllers objectAtIndex:i];
452         if ([controller serverName])
453             [array addObject:[controller serverName]];
454     }
456     return array;
459 @end // MMAppController
464 @implementation MMAppController (MMServices)
466 - (void)openSelection:(NSPasteboard *)pboard userData:(NSString *)userData
467                 error:(NSString **)error
469     if (![[pboard types] containsObject:NSStringPboardType]) {
470         NSLog(@"WARNING: Pasteboard contains no object of type "
471                 "NSStringPboardType");
472         return;
473     }
475     MMVimController *vc = [self topmostVimController];
476     if (vc) {
477         // Open a new tab first, since dropString: does not do this.
478         [vc sendMessage:AddNewTabMsgID data:nil];
479         [vc dropString:[pboard stringForType:NSStringPboardType]];
480     } else {
481         // NOTE: There is no window to paste the selection into, so save the
482         // text, open a new window, and paste the text when the next window
483         // opens.  (If this is called several times in a row, then all but the
484         // last call might be ignored.)
485         if (openSelectionString) [openSelectionString release];
486         openSelectionString = [[pboard stringForType:NSStringPboardType] copy];
488         [self newWindow:self];
489     }
492 - (void)openFile:(NSPasteboard *)pboard userData:(NSString *)userData
493            error:(NSString **)error
495     if (![[pboard types] containsObject:NSStringPboardType]) {
496         NSLog(@"WARNING: Pasteboard contains no object of type "
497                 "NSStringPboardType");
498         return;
499     }
501     // TODO: Parse multiple filenames and create array with names.
502     NSString *string = [pboard stringForType:NSStringPboardType];
503     string = [string stringByTrimmingCharactersInSet:
504             [NSCharacterSet whitespaceAndNewlineCharacterSet]];
505     string = [string stringByStandardizingPath];
507     MMVimController *vc = nil;
508     if (userData && [userData isEqual:@"Tab"])
509         vc = [self topmostVimController];
511     if (vc) {
512         [vc dropFiles:[NSArray arrayWithObject:string]];
513     } else {
514         [self application:NSApp openFiles:[NSArray arrayWithObject:string]];
515     }
518 @end // MMAppController (MMServices)
523 @implementation MMAppController (Private)
525 - (MMVimController *)keyVimController
527     NSWindow *keyWindow = [NSApp keyWindow];
528     if (keyWindow) {
529         unsigned i, count = [vimControllers count];
530         for (i = 0; i < count; ++i) {
531             MMVimController *vc = [vimControllers objectAtIndex:i];
532             if ([[[vc windowController] window] isEqual:keyWindow])
533                 return vc;
534         }
535     }
537     return nil;
540 - (MMVimController *)topmostVimController
542     NSArray *windows = [NSApp orderedWindows];
543     if ([windows count] > 0) {
544         NSWindow *window = [windows objectAtIndex:0];
545         unsigned i, count = [vimControllers count];
546         for (i = 0; i < count; ++i) {
547             MMVimController *vc = [vimControllers objectAtIndex:i];
548             if ([[[vc windowController] window] isEqual:window])
549                 return vc;
550         }
551     }
553     return nil;
556 - (void)launchVimProcessWithArguments:(NSArray *)args
558     NSString *path = [[NSBundle mainBundle] pathForAuxiliaryExecutable:@"Vim"];
559     if (!path) {
560         NSLog(@"ERROR: Vim executable could not be found inside app bundle!");
561         return;
562     }
564     NSMutableString *execArg = [NSMutableString
565         stringWithFormat:@"exec \"%@\" -g", path];
566     if (args) {
567         // Append all arguments while making sure that arguments containing
568         // spaces are enclosed in quotes.
569         NSCharacterSet *space = [NSCharacterSet whitespaceCharacterSet];
570         unsigned i, count = [args count];
572         for (i = 0; i < count; ++i) {
573             NSString *arg = [args objectAtIndex:i];
574             if (NSNotFound != [arg rangeOfCharacterFromSet:space].location)
575                 [execArg appendFormat:@" \"%@\"", arg];
576             else
577                 [execArg appendFormat:@" %@", arg];
578         }
579     }
581     // Launch the process with a login shell so that users environment settings
582     // get sourced.  This does not always happen when MacVim is started.
583     NSArray *shellArgs = [NSArray arrayWithObjects:@"-l", @"-c", execArg, nil];
584     NSString *shell = [[[NSProcessInfo processInfo] environment]
585         objectForKey:@"SHELL"];
586     if (!shell)
587         shell = @"/bin/sh";
589     //NSLog(@"Launching: %@  args: %@", shell, shellArgs);
590     [NSTask launchedTaskWithLaunchPath:shell arguments:shellArgs];
593 @end // MMAppController (Private)
598 @implementation NSMenu (MMExtras)
600 - (void)recurseSetAutoenablesItems:(BOOL)on
602     [self setAutoenablesItems:on];
604     int i, count = [self numberOfItems];
605     for (i = 0; i < count; ++i) {
606         NSMenuItem *item = [self itemAtIndex:i];
607         [item setEnabled:YES];
608         NSMenu *submenu = [item submenu];
609         if (submenu) {
610             [submenu recurseSetAutoenablesItems:on];
611         }
612     }
615 @end  // NSMenu (MMExtras)
620 @implementation NSNumber (MMExtras)
621 - (int)tag
623     return [self intValue];
625 @end // NSNumber (MMExtras)