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 * Coordinates input/output to/from backend. Each MMBackend communicates
14 * directly with a MMVimController.
16 * MMVimController does not deal with visual presentation. Essentially it
17 * should be able to run with no window present.
19 * Output from the backend is received in processInputQueue:. Input is sent
20 * to the backend via sendMessage:data: or addVimInput:. The latter allows
21 * execution of arbitrary strings in the Vim process, much like the Vim script
22 * function remote_send() does. The messages that may be passed between
23 * frontend and backend are defined in an enum in MacVim.h.
26 #import "MMAppController.h"
27 #import "MMAtsuiTextView.h"
28 #import "MMFindReplaceController.h"
29 #import "MMTextView.h"
30 #import "MMVimController.h"
32 #import "MMWindowController.h"
33 #import "Miscellaneous.h"
35 #ifdef MM_ENABLE_PLUGINS
36 #import "MMPlugInManager.h"
39 static NSString *MMDefaultToolbarImageName = @"Attention";
40 static int MMAlertTextFieldHeight = 22;
42 // NOTE: By default a message sent to the backend will be dropped if it cannot
43 // be delivered instantly; otherwise there is a possibility that MacVim will
44 // 'beachball' while waiting to deliver DO messages to an unresponsive Vim
45 // process. This means that you cannot rely on any message sent with
46 // sendMessage: to actually reach Vim.
47 static NSTimeInterval MMBackendProxyRequestTimeout = 0;
49 // Timeout used for setDialogReturn:.
50 static NSTimeInterval MMSetDialogReturnTimeout = 1.0;
52 static BOOL isUnsafeMessage(int msgid);
54 static unsigned identifierCounter = 1;
57 @interface MMAlert : NSAlert {
58 NSTextField *textField;
60 - (void)setTextFieldString:(NSString *)textFieldString;
61 - (NSTextField *)textField;
65 @interface MMVimController (Private)
66 - (void)doProcessInputQueue:(NSArray *)queue;
67 - (void)handleMessage:(int)msgid data:(NSData *)data;
68 - (void)savePanelDidEnd:(NSSavePanel *)panel code:(int)code
69 context:(void *)context;
70 - (void)alertDidEnd:(MMAlert *)alert code:(int)code context:(void *)context;
71 - (NSMenuItem *)menuItemForDescriptor:(NSArray *)desc;
72 - (NSMenu *)parentMenuForDescriptor:(NSArray *)desc;
73 - (NSMenu *)topLevelMenuForTitle:(NSString *)title;
74 - (void)addMenuWithDescriptor:(NSArray *)desc atIndex:(int)index;
75 - (void)addMenuItemWithDescriptor:(NSArray *)desc
79 keyEquivalent:(NSString *)keyEquivalent
80 modifierMask:(int)modifierMask
81 action:(NSString *)action
82 isAlternate:(BOOL)isAlternate;
83 - (void)removeMenuItemWithDescriptor:(NSArray *)desc;
84 - (void)enableMenuItemWithDescriptor:(NSArray *)desc state:(BOOL)on;
85 - (void)addToolbarItemToDictionaryWithLabel:(NSString *)title
86 toolTip:(NSString *)tip icon:(NSString *)icon;
87 - (void)addToolbarItemWithLabel:(NSString *)label
88 tip:(NSString *)tip icon:(NSString *)icon
90 - (void)popupMenuWithDescriptor:(NSArray *)desc
92 column:(NSNumber *)col;
93 - (void)popupMenuWithAttributes:(NSDictionary *)attrs;
94 - (void)connectionDidDie:(NSNotification *)notification;
95 - (void)scheduleClose;
96 - (void)handleBrowseForFile:(NSDictionary *)attr;
97 - (void)handleShowDialog:(NSDictionary *)attr;
103 @implementation MMVimController
105 - (id)initWithBackend:(id)backend pid:(int)processIdentifier
107 if (!(self = [super init]))
110 // TODO: Come up with a better way of creating an identifier.
111 identifier = identifierCounter++;
114 [[MMWindowController alloc] initWithVimController:self];
115 backendProxy = [backend retain];
116 popupMenuItems = [[NSMutableArray alloc] init];
117 toolbarItemDict = [[NSMutableDictionary alloc] init];
118 pid = processIdentifier;
119 creationDate = [[NSDate alloc] init];
121 NSConnection *connection = [backendProxy connectionForProxy];
123 // TODO: Check that this will not set the timeout for the root proxy
124 // (in MMAppController).
125 [connection setRequestTimeout:MMBackendProxyRequestTimeout];
127 [[NSNotificationCenter defaultCenter] addObserver:self
128 selector:@selector(connectionDidDie:)
129 name:NSConnectionDidDieNotification object:connection];
131 // Set up a main menu with only a "MacVim" menu (copied from a template
132 // which itself is set up in MainMenu.nib). The main menu is populated
134 mainMenu = [[NSMenu alloc] initWithTitle:@"MainMenu"];
135 NSMenuItem *appMenuItem = [[MMAppController sharedInstance]
136 appMenuItemTemplate];
137 appMenuItem = [[appMenuItem copy] autorelease];
139 // Note: If the title of the application menu is anything but what
140 // CFBundleName says then the application menu will not be typeset in
141 // boldface for some reason. (It should already be set when we copy
142 // from the default main menu, but this is not the case for some
144 NSString *appName = [[NSBundle mainBundle]
145 objectForInfoDictionaryKey:@"CFBundleName"];
146 [appMenuItem setTitle:appName];
148 [mainMenu addItem:appMenuItem];
150 #ifdef MM_ENABLE_PLUGINS
151 instanceMediator = [[MMPlugInInstanceMediator alloc]
152 initWithVimController:self];
166 #ifdef MM_ENABLE_PLUGINS
167 [instanceMediator release]; instanceMediator = nil;
170 [serverName release]; serverName = nil;
171 [backendProxy release]; backendProxy = nil;
173 [toolbarItemDict release]; toolbarItemDict = nil;
174 [toolbar release]; toolbar = nil;
175 [popupMenuItems release]; popupMenuItems = nil;
176 [windowController release]; windowController = nil;
178 [vimState release]; vimState = nil;
179 [mainMenu release]; mainMenu = nil;
180 [creationDate release]; creationDate = nil;
185 - (unsigned)identifier
190 - (MMWindowController *)windowController
192 return windowController;
195 #ifdef MM_ENABLE_PLUGINS
196 - (MMPlugInInstanceMediator *)instanceMediator
198 return instanceMediator;
202 - (NSDictionary *)vimState
207 - (id)objectForVimStateKey:(NSString *)key
209 return [vimState objectForKey:key];
222 - (void)setIsPreloading:(BOOL)yn
227 - (NSDate *)creationDate
232 - (void)setServerName:(NSString *)name
234 if (name != serverName) {
235 [serverName release];
236 serverName = [name copy];
240 - (NSString *)serverName
250 - (void)dropFiles:(NSArray *)filenames forceOpen:(BOOL)force
252 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
254 // Default to opening in tabs if layout is invalid or set to "windows".
255 int layout = [ud integerForKey:MMOpenLayoutKey];
256 if (layout < 0 || layout > MMLayoutTabs)
257 layout = MMLayoutTabs;
259 BOOL splitVert = [ud boolForKey:MMVerticalSplitKey];
260 if (splitVert && MMLayoutHorizontalSplit == layout)
261 layout = MMLayoutVerticalSplit;
263 NSDictionary *args = [NSDictionary dictionaryWithObjectsAndKeys:
264 [NSNumber numberWithInt:layout], @"layout",
265 filenames, @"filenames",
266 [NSNumber numberWithBool:force], @"forceOpen",
269 [self sendMessage:DropFilesMsgID data:[args dictionaryAsData]];
272 - (void)file:(NSString *)filename draggedToTabAtIndex:(NSUInteger)tabIndex
274 NSString *fnEsc = [filename stringByEscapingSpecialFilenameCharacters];
275 NSString *input = [NSString stringWithFormat:@"<C-\\><C-N>:silent "
277 "edit! %@<CR>", tabIndex + 1, fnEsc];
278 [self addVimInput:input];
281 - (void)filesDraggedToTabBar:(NSArray *)filenames
283 NSUInteger i, count = [filenames count];
284 NSMutableString *input = [NSMutableString stringWithString:@"<C-\\><C-N>"
285 ":silent! tabnext 9999"];
286 for (i = 0; i < count; i++) {
287 NSString *fn = [filenames objectAtIndex:i];
288 NSString *fnEsc = [fn stringByEscapingSpecialFilenameCharacters];
289 [input appendFormat:@"|tabedit %@", fnEsc];
291 [input appendString:@"<CR>"];
292 [self addVimInput:input];
295 - (void)dropString:(NSString *)string
297 int len = [string lengthOfBytesUsingEncoding:NSUTF8StringEncoding] + 1;
299 NSMutableData *data = [NSMutableData data];
301 [data appendBytes:&len length:sizeof(int)];
302 [data appendBytes:[string UTF8String] length:len];
304 [self sendMessage:DropStringMsgID data:data];
308 - (void)passArguments:(NSDictionary *)args
312 [self sendMessage:OpenWithArgumentsMsgID data:[args dictionaryAsData]];
314 // HACK! Fool findUnusedEditor into thinking that this controller is not
315 // unused anymore, in case it is called before the arguments have reached
316 // the Vim process. This should be a "safe" hack since the next time the
317 // Vim process flushes its output queue the state will be updated again (at
318 // which time the "unusedEditor" state will have been properly set).
319 NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithDictionary:
321 [dict setObject:[NSNumber numberWithBool:NO] forKey:@"unusedEditor"];
323 vimState = [dict copy];
326 - (void)sendMessage:(int)msgid data:(NSData *)data
328 //NSLog(@"sendMessage:%s (isInitialized=%d)",
329 // MessageStrings[msgid], isInitialized);
331 if (!isInitialized) return;
334 [backendProxy processInput:msgid data:data];
336 @catch (NSException *e) {
337 //NSLog(@"%@ %s Exception caught during DO call: %@",
338 // [self className], _cmd, e);
342 - (BOOL)sendMessageNow:(int)msgid data:(NSData *)data
343 timeout:(NSTimeInterval)timeout
345 // Send a message with a timeout. USE WITH EXTREME CAUTION! Sending
346 // messages in rapid succession with a timeout may cause MacVim to beach
347 // ball forever. In almost all circumstances sendMessage:data: should be
353 if (timeout < 0) timeout = 0;
356 NSConnection *conn = [backendProxy connectionForProxy];
357 NSTimeInterval oldTimeout = [conn requestTimeout];
359 [conn setRequestTimeout:timeout];
362 [backendProxy processInput:msgid data:data];
364 @catch (NSException *e) {
368 [conn setRequestTimeout:oldTimeout];
374 - (void)addVimInput:(NSString *)string
376 // This is a very general method of adding input to the Vim process. It is
377 // basically the same as calling remote_send() on the process (see
378 // ':h remote_send').
380 NSData *data = [string dataUsingEncoding:NSUTF8StringEncoding];
381 [self sendMessage:AddInputMsgID data:data];
385 - (NSString *)evaluateVimExpression:(NSString *)expr
387 NSString *eval = nil;
390 eval = [backendProxy evaluateExpression:expr];
392 @catch (NSException *ex) { /* do nothing */ }
397 - (id)evaluateVimExpressionCocoa:(NSString *)expr
398 errorString:(NSString **)errstr
403 eval = [backendProxy evaluateExpressionCocoa:expr
405 } @catch (NSException *ex) {
406 *errstr = [ex reason];
419 if (!isInitialized) return;
421 // Remove any delayed calls made on this object.
422 [NSObject cancelPreviousPerformRequestsWithTarget:self];
425 [toolbar setDelegate:nil];
426 [[NSNotificationCenter defaultCenter] removeObserver:self];
427 //[[backendProxy connectionForProxy] invalidate];
428 //[windowController close];
429 [windowController cleanup];
432 - (void)processInputQueue:(NSArray *)queue
434 if (!isInitialized) return;
436 [self doProcessInputQueue:queue];
437 [windowController processInputQueueDidFinish];
440 - (NSToolbarItem *)toolbar:(NSToolbar *)theToolbar
441 itemForItemIdentifier:(NSString *)itemId
442 willBeInsertedIntoToolbar:(BOOL)flag
444 NSToolbarItem *item = [toolbarItemDict objectForKey:itemId];
446 NSLog(@"WARNING: No toolbar item with id '%@'", itemId);
452 - (NSArray *)toolbarAllowedItemIdentifiers:(NSToolbar *)theToolbar
457 - (NSArray *)toolbarDefaultItemIdentifiers:(NSToolbar *)theToolbar
462 @end // MMVimController
466 @implementation MMVimController (Private)
468 - (void)doProcessInputQueue:(NSArray *)queue
470 NSMutableArray *delayQueue = nil;
473 unsigned i, count = [queue count];
475 NSLog(@"WARNING: Uneven number of components (%d) in command "
476 "queue. Skipping...", count);
480 //NSLog(@"======== %s BEGIN ========", _cmd);
481 for (i = 0; i < count; i += 2) {
482 NSData *value = [queue objectAtIndex:i];
483 NSData *data = [queue objectAtIndex:i+1];
485 int msgid = *((int*)[value bytes]);
486 //NSLog(@"%s%s", _cmd, MessageStrings[msgid]);
488 BOOL inDefaultMode = [[[NSRunLoop currentRunLoop] currentMode]
489 isEqual:NSDefaultRunLoopMode];
490 if (!inDefaultMode && isUnsafeMessage(msgid)) {
491 // NOTE: Because we may be listening to DO messages in "event
492 // tracking mode" we have to take extra care when doing things
493 // like releasing view items (and other Cocoa objects).
494 // Messages that may be potentially "unsafe" are delayed until
495 // the run loop is back to default mode at which time they are
496 // safe to call again.
497 // A problem with this approach is that it is hard to
498 // classify which messages are unsafe. As a rule of thumb, if
499 // a message may release an object used by the Cocoa framework
500 // (e.g. views) then the message should be considered unsafe.
501 // Delaying messages may have undesired side-effects since it
502 // means that messages may not be processed in the order Vim
503 // sent them, so beware.
505 delayQueue = [NSMutableArray array];
507 //NSLog(@"Adding unsafe message '%s' to delay queue (mode=%@)",
508 // MessageStrings[msgid],
509 // [[NSRunLoop currentRunLoop] currentMode]);
510 [delayQueue addObject:value];
511 [delayQueue addObject:data];
513 [self handleMessage:msgid data:data];
516 //NSLog(@"======== %s END ========", _cmd);
518 @catch (NSException *e) {
519 NSLog(@"Exception caught whilst processing command queue: %@", e);
523 //NSLog(@" Flushing delay queue (%d items)", [delayQueue count]/2);
524 [self performSelector:@selector(processInputQueue:)
525 withObject:delayQueue
530 - (void)handleMessage:(int)msgid data:(NSData *)data
532 //if (msgid != AddMenuMsgID && msgid != AddMenuItemMsgID &&
533 // msgid != EnableMenuItemMsgID)
534 // NSLog(@"%@ %s%s", [self className], _cmd, MessageStrings[msgid]);
536 if (OpenWindowMsgID == msgid) {
537 [windowController openWindow];
539 // If the vim controller is preloading then the window will be
540 // displayed when it is taken off the preload cache.
542 [windowController showWindow];
543 } else if (BatchDrawMsgID == msgid) {
544 [[[windowController vimView] textView] performBatchDrawWithData:data];
545 } else if (SelectTabMsgID == msgid) {
546 #if 0 // NOTE: Tab selection is done inside updateTabsWithData:.
547 const void *bytes = [data bytes];
548 int idx = *((int*)bytes);
549 //NSLog(@"Selecting tab with index %d", idx);
550 [windowController selectTabWithIndex:idx];
552 } else if (UpdateTabBarMsgID == msgid) {
553 [windowController updateTabsWithData:data];
554 } else if (ShowTabBarMsgID == msgid) {
555 [windowController showTabBar:YES];
556 } else if (HideTabBarMsgID == msgid) {
557 [windowController showTabBar:NO];
558 } else if (SetTextDimensionsMsgID == msgid || LiveResizeMsgID == msgid ||
559 SetTextDimensionsReplyMsgID == msgid) {
560 const void *bytes = [data bytes];
561 int rows = *((int*)bytes); bytes += sizeof(int);
562 int cols = *((int*)bytes); bytes += sizeof(int);
564 [windowController setTextDimensionsWithRows:rows
566 isLive:(LiveResizeMsgID==msgid)
567 isReply:(SetTextDimensionsReplyMsgID==msgid)];
568 } else if (SetWindowTitleMsgID == msgid) {
569 const void *bytes = [data bytes];
570 int len = *((int*)bytes); bytes += sizeof(int);
572 NSString *string = [[NSString alloc] initWithBytes:(void*)bytes
573 length:len encoding:NSUTF8StringEncoding];
575 // While in live resize the window title displays the dimensions of the
576 // window so don't clobber this with a spurious "set title" message
578 if (![[windowController vimView] inLiveResize])
579 [windowController setTitle:string];
582 } else if (SetDocumentFilenameMsgID == msgid) {
583 const void *bytes = [data bytes];
584 int len = *((int*)bytes); bytes += sizeof(int);
587 NSString *filename = [[NSString alloc] initWithBytes:(void*)bytes
588 length:len encoding:NSUTF8StringEncoding];
590 [windowController setDocumentFilename:filename];
594 [windowController setDocumentFilename:@""];
596 } else if (AddMenuMsgID == msgid) {
597 NSDictionary *attrs = [NSDictionary dictionaryWithData:data];
598 [self addMenuWithDescriptor:[attrs objectForKey:@"descriptor"]
599 atIndex:[[attrs objectForKey:@"index"] intValue]];
600 } else if (AddMenuItemMsgID == msgid) {
601 NSDictionary *attrs = [NSDictionary dictionaryWithData:data];
602 [self addMenuItemWithDescriptor:[attrs objectForKey:@"descriptor"]
603 atIndex:[[attrs objectForKey:@"index"] intValue]
604 tip:[attrs objectForKey:@"tip"]
605 icon:[attrs objectForKey:@"icon"]
606 keyEquivalent:[attrs objectForKey:@"keyEquivalent"]
607 modifierMask:[[attrs objectForKey:@"modifierMask"] intValue]
608 action:[attrs objectForKey:@"action"]
609 isAlternate:[[attrs objectForKey:@"isAlternate"] boolValue]];
610 } else if (RemoveMenuItemMsgID == msgid) {
611 NSDictionary *attrs = [NSDictionary dictionaryWithData:data];
612 [self removeMenuItemWithDescriptor:[attrs objectForKey:@"descriptor"]];
613 } else if (EnableMenuItemMsgID == msgid) {
614 NSDictionary *attrs = [NSDictionary dictionaryWithData:data];
615 [self enableMenuItemWithDescriptor:[attrs objectForKey:@"descriptor"]
616 state:[[attrs objectForKey:@"enable"] boolValue]];
617 } else if (ShowToolbarMsgID == msgid) {
618 const void *bytes = [data bytes];
619 int enable = *((int*)bytes); bytes += sizeof(int);
620 int flags = *((int*)bytes); bytes += sizeof(int);
622 int mode = NSToolbarDisplayModeDefault;
623 if (flags & ToolbarLabelFlag) {
624 mode = flags & ToolbarIconFlag ? NSToolbarDisplayModeIconAndLabel
625 : NSToolbarDisplayModeLabelOnly;
626 } else if (flags & ToolbarIconFlag) {
627 mode = NSToolbarDisplayModeIconOnly;
630 int size = flags & ToolbarSizeRegularFlag ? NSToolbarSizeModeRegular
631 : NSToolbarSizeModeSmall;
633 [windowController showToolbar:enable size:size mode:mode];
634 } else if (CreateScrollbarMsgID == msgid) {
635 const void *bytes = [data bytes];
636 long ident = *((long*)bytes); bytes += sizeof(long);
637 int type = *((int*)bytes); bytes += sizeof(int);
639 [windowController createScrollbarWithIdentifier:ident type:type];
640 } else if (DestroyScrollbarMsgID == msgid) {
641 const void *bytes = [data bytes];
642 long ident = *((long*)bytes); bytes += sizeof(long);
644 [windowController destroyScrollbarWithIdentifier:ident];
645 } else if (ShowScrollbarMsgID == msgid) {
646 const void *bytes = [data bytes];
647 long ident = *((long*)bytes); bytes += sizeof(long);
648 int visible = *((int*)bytes); bytes += sizeof(int);
650 [windowController showScrollbarWithIdentifier:ident state:visible];
651 } else if (SetScrollbarPositionMsgID == msgid) {
652 const void *bytes = [data bytes];
653 long ident = *((long*)bytes); bytes += sizeof(long);
654 int pos = *((int*)bytes); bytes += sizeof(int);
655 int len = *((int*)bytes); bytes += sizeof(int);
657 [windowController setScrollbarPosition:pos length:len
659 } else if (SetScrollbarThumbMsgID == msgid) {
660 const void *bytes = [data bytes];
661 long ident = *((long*)bytes); bytes += sizeof(long);
662 float val = *((float*)bytes); bytes += sizeof(float);
663 float prop = *((float*)bytes); bytes += sizeof(float);
665 [windowController setScrollbarThumbValue:val proportion:prop
667 } else if (SetFontMsgID == msgid) {
668 const void *bytes = [data bytes];
669 float size = *((float*)bytes); bytes += sizeof(float);
670 int len = *((int*)bytes); bytes += sizeof(int);
671 NSString *name = [[NSString alloc]
672 initWithBytes:(void*)bytes length:len
673 encoding:NSUTF8StringEncoding];
674 NSFont *font = [NSFont fontWithName:name size:size];
676 // This should only happen if the default font was not loaded in
677 // which case we fall back on using the Cocoa default fixed width
679 font = [NSFont userFixedPitchFontOfSize:size];
682 [windowController setFont:font];
684 } else if (SetWideFontMsgID == msgid) {
685 const void *bytes = [data bytes];
686 float size = *((float*)bytes); bytes += sizeof(float);
687 int len = *((int*)bytes); bytes += sizeof(int);
689 NSString *name = [[NSString alloc]
690 initWithBytes:(void*)bytes length:len
691 encoding:NSUTF8StringEncoding];
692 NSFont *font = [NSFont fontWithName:name size:size];
693 [windowController setWideFont:font];
697 [windowController setWideFont:nil];
699 } else if (SetDefaultColorsMsgID == msgid) {
700 const void *bytes = [data bytes];
701 unsigned bg = *((unsigned*)bytes); bytes += sizeof(unsigned);
702 unsigned fg = *((unsigned*)bytes); bytes += sizeof(unsigned);
703 NSColor *back = [NSColor colorWithArgbInt:bg];
704 NSColor *fore = [NSColor colorWithRgbInt:fg];
706 [windowController setDefaultColorsBackground:back foreground:fore];
707 } else if (ExecuteActionMsgID == msgid) {
708 const void *bytes = [data bytes];
709 int len = *((int*)bytes); bytes += sizeof(int);
710 NSString *actionName = [[NSString alloc]
711 initWithBytes:(void*)bytes length:len
712 encoding:NSUTF8StringEncoding];
714 SEL sel = NSSelectorFromString(actionName);
715 [NSApp sendAction:sel to:nil from:self];
717 [actionName release];
718 } else if (ShowPopupMenuMsgID == msgid) {
719 NSDictionary *attrs = [NSDictionary dictionaryWithData:data];
721 // The popup menu enters a modal loop so delay this call so that we
722 // don't block inside processInputQueue:.
723 [self performSelector:@selector(popupMenuWithAttributes:)
726 } else if (SetMouseShapeMsgID == msgid) {
727 const void *bytes = [data bytes];
728 int shape = *((int*)bytes); bytes += sizeof(int);
730 [windowController setMouseShape:shape];
731 } else if (AdjustLinespaceMsgID == msgid) {
732 const void *bytes = [data bytes];
733 int linespace = *((int*)bytes); bytes += sizeof(int);
735 [windowController adjustLinespace:linespace];
736 } else if (ActivateMsgID == msgid) {
737 //NSLog(@"ActivateMsgID");
738 [NSApp activateIgnoringOtherApps:YES];
739 [[windowController window] makeKeyAndOrderFront:self];
740 } else if (SetServerNameMsgID == msgid) {
741 NSString *name = [[NSString alloc] initWithData:data
742 encoding:NSUTF8StringEncoding];
743 [self setServerName:name];
745 } else if (EnterFullscreenMsgID == msgid) {
746 const void *bytes = [data bytes];
747 int fuoptions = *((int*)bytes); bytes += sizeof(int);
748 int bg = *((int*)bytes);
749 NSColor *back = [NSColor colorWithArgbInt:bg];
751 [windowController enterFullscreen:fuoptions backgroundColor:back];
752 } else if (LeaveFullscreenMsgID == msgid) {
753 [windowController leaveFullscreen];
754 } else if (BuffersNotModifiedMsgID == msgid) {
755 [windowController setBuffersModified:NO];
756 } else if (BuffersModifiedMsgID == msgid) {
757 [windowController setBuffersModified:YES];
758 } else if (SetPreEditPositionMsgID == msgid) {
759 const int *dim = (const int*)[data bytes];
760 [[[windowController vimView] textView] setPreEditRow:dim[0]
762 } else if (EnableAntialiasMsgID == msgid) {
763 [[[windowController vimView] textView] setAntialias:YES];
764 } else if (DisableAntialiasMsgID == msgid) {
765 [[[windowController vimView] textView] setAntialias:NO];
766 } else if (SetVimStateMsgID == msgid) {
767 NSDictionary *dict = [NSDictionary dictionaryWithData:data];
770 vimState = [dict retain];
772 } else if (CloseWindowMsgID == msgid) {
773 [self scheduleClose];
774 } else if (SetFullscreenColorMsgID == msgid) {
775 const int *bg = (const int*)[data bytes];
776 NSColor *color = [NSColor colorWithRgbInt:*bg];
778 [windowController setFullscreenBackgroundColor:color];
779 } else if (ShowFindReplaceDialogMsgID == msgid) {
780 NSDictionary *dict = [NSDictionary dictionaryWithData:data];
782 [[MMFindReplaceController sharedInstance]
783 showWithText:[dict objectForKey:@"text"]
784 flags:[[dict objectForKey:@"flags"] intValue]];
786 } else if (ActivateKeyScriptID == msgid) {
787 KeyScript(smKeySysScript);
788 } else if (DeactivateKeyScriptID == msgid) {
789 KeyScript(smKeyRoman);
790 } else if (BrowseForFileMsgID == msgid) {
791 NSDictionary *dict = [NSDictionary dictionaryWithData:data];
793 [self handleBrowseForFile:dict];
794 } else if (ShowDialogMsgID == msgid) {
795 NSDictionary *dict = [NSDictionary dictionaryWithData:data];
797 [self handleShowDialog:dict];
798 // IMPORTANT: When adding a new message, make sure to update
799 // isUnsafeMessage() if necessary!
801 NSLog(@"WARNING: Unknown message received (msgid=%d)", msgid);
805 - (void)savePanelDidEnd:(NSSavePanel *)panel code:(int)code
806 context:(void *)context
808 NSString *path = (code == NSOKButton) ? [panel filename] : nil;
810 // NOTE! setDialogReturn: is a synchronous call so set a proper timeout to
811 // avoid waiting forever for it to finish. We make this a synchronous call
812 // so that we can be fairly certain that Vim doesn't think the dialog box
813 // is still showing when MacVim has in fact already dismissed it.
814 NSConnection *conn = [backendProxy connectionForProxy];
815 NSTimeInterval oldTimeout = [conn requestTimeout];
816 [conn setRequestTimeout:MMSetDialogReturnTimeout];
819 [backendProxy setDialogReturn:path];
821 // Add file to the "Recent Files" menu (this ensures that files that
822 // are opened/saved from a :browse command are added to this menu).
824 [[NSDocumentController sharedDocumentController]
825 noteNewRecentFilePath:path];
827 @catch (NSException *e) {
828 NSLog(@"Exception caught in %s %@", _cmd, e);
831 [conn setRequestTimeout:oldTimeout];
835 - (void)alertDidEnd:(MMAlert *)alert code:(int)code context:(void *)context
839 code = code - NSAlertFirstButtonReturn + 1;
841 if ([alert isKindOfClass:[MMAlert class]] && [alert textField]) {
842 ret = [NSArray arrayWithObjects:[NSNumber numberWithInt:code],
843 [[alert textField] stringValue], nil];
845 ret = [NSArray arrayWithObject:[NSNumber numberWithInt:code]];
849 [backendProxy setDialogReturn:ret];
851 @catch (NSException *e) {
852 NSLog(@"Exception caught in %s %@", _cmd, e);
856 - (NSMenuItem *)menuItemForDescriptor:(NSArray *)desc
858 if (!(desc && [desc count] > 0)) return nil;
860 NSString *rootName = [desc objectAtIndex:0];
861 NSArray *rootItems = [rootName hasPrefix:@"PopUp"] ? popupMenuItems
862 : [mainMenu itemArray];
864 NSMenuItem *item = nil;
865 int i, count = [rootItems count];
866 for (i = 0; i < count; ++i) {
867 item = [rootItems objectAtIndex:i];
868 if ([[item title] isEqual:rootName])
872 if (i == count) return nil;
874 count = [desc count];
875 for (i = 1; i < count; ++i) {
876 item = [[item submenu] itemWithTitle:[desc objectAtIndex:i]];
877 if (!item) return nil;
883 - (NSMenu *)parentMenuForDescriptor:(NSArray *)desc
885 if (!(desc && [desc count] > 0)) return nil;
887 NSString *rootName = [desc objectAtIndex:0];
888 NSArray *rootItems = [rootName hasPrefix:@"PopUp"] ? popupMenuItems
889 : [mainMenu itemArray];
892 int i, count = [rootItems count];
893 for (i = 0; i < count; ++i) {
894 NSMenuItem *item = [rootItems objectAtIndex:i];
895 if ([[item title] isEqual:rootName]) {
896 menu = [item submenu];
901 if (!menu) return nil;
903 count = [desc count] - 1;
904 for (i = 1; i < count; ++i) {
905 NSMenuItem *item = [menu itemWithTitle:[desc objectAtIndex:i]];
906 menu = [item submenu];
907 if (!menu) return nil;
913 - (NSMenu *)topLevelMenuForTitle:(NSString *)title
915 // Search only the top-level menus.
917 unsigned i, count = [popupMenuItems count];
918 for (i = 0; i < count; ++i) {
919 NSMenuItem *item = [popupMenuItems objectAtIndex:i];
920 if ([title isEqual:[item title]])
921 return [item submenu];
924 count = [mainMenu numberOfItems];
925 for (i = 0; i < count; ++i) {
926 NSMenuItem *item = [mainMenu itemAtIndex:i];
927 if ([title isEqual:[item title]])
928 return [item submenu];
934 - (void)addMenuWithDescriptor:(NSArray *)desc atIndex:(int)idx
936 if (!(desc && [desc count] > 0 && idx >= 0)) return;
938 NSString *rootName = [desc objectAtIndex:0];
939 if ([rootName isEqual:@"ToolBar"]) {
940 // The toolbar only has one menu, we take this as a hint to create a
941 // toolbar, then we return.
943 // NOTE! Each toolbar must have a unique identifier, else each
944 // window will have the same toolbar.
945 NSString *ident = [NSString stringWithFormat:@"%d", (int)self];
946 toolbar = [[NSToolbar alloc] initWithIdentifier:ident];
948 [toolbar setShowsBaselineSeparator:NO];
949 [toolbar setDelegate:self];
950 [toolbar setDisplayMode:NSToolbarDisplayModeIconOnly];
951 [toolbar setSizeMode:NSToolbarSizeModeSmall];
953 [windowController setToolbar:toolbar];
959 // This is either a main menu item or a popup menu item.
960 NSString *title = [desc lastObject];
961 NSMenuItem *item = [[NSMenuItem alloc] init];
962 NSMenu *menu = [[NSMenu alloc] initWithTitle:title];
964 [item setTitle:title];
965 [item setSubmenu:menu];
967 NSMenu *parent = [self parentMenuForDescriptor:desc];
968 if (!parent && [rootName hasPrefix:@"PopUp"]) {
969 if ([popupMenuItems count] <= idx) {
970 [popupMenuItems addObject:item];
972 [popupMenuItems insertObject:item atIndex:idx];
975 // If descriptor has no parent and its not a popup (or toolbar) menu,
976 // then it must belong to main menu.
977 if (!parent) parent = mainMenu;
979 if ([parent numberOfItems] <= idx) {
980 [parent addItem:item];
982 [parent insertItem:item atIndex:idx];
990 - (void)addMenuItemWithDescriptor:(NSArray *)desc
993 icon:(NSString *)icon
994 keyEquivalent:(NSString *)keyEquivalent
995 modifierMask:(int)modifierMask
996 action:(NSString *)action
997 isAlternate:(BOOL)isAlternate
999 if (!(desc && [desc count] > 1 && idx >= 0)) return;
1001 NSString *title = [desc lastObject];
1002 NSString *rootName = [desc objectAtIndex:0];
1004 if ([rootName isEqual:@"ToolBar"]) {
1005 if (toolbar && [desc count] == 2)
1006 [self addToolbarItemWithLabel:title tip:tip icon:icon atIndex:idx];
1010 NSMenu *parent = [self parentMenuForDescriptor:desc];
1012 NSLog(@"WARNING: Menu item '%@' has no parent",
1013 [desc componentsJoinedByString:@"->"]);
1017 NSMenuItem *item = nil;
1018 if (0 == [title length]
1019 || ([title hasPrefix:@"-"] && [title hasSuffix:@"-"])) {
1020 item = [NSMenuItem separatorItem];
1021 [item setTitle:title];
1023 item = [[[NSMenuItem alloc] init] autorelease];
1024 [item setTitle:title];
1026 // Note: It is possible to set the action to a message that "doesn't
1027 // exist" without problems. We take advantage of this when adding
1028 // "dummy items" e.g. when dealing with the "Recent Files" menu (in
1029 // which case a recentFilesDummy: action is set, although it is never
1031 if ([action length] > 0)
1032 [item setAction:NSSelectorFromString(action)];
1034 [item setAction:@selector(vimMenuItemAction:)];
1035 if ([tip length] > 0) [item setToolTip:tip];
1036 if ([keyEquivalent length] > 0) {
1037 [item setKeyEquivalent:keyEquivalent];
1038 [item setKeyEquivalentModifierMask:modifierMask];
1040 [item setAlternate:isAlternate];
1042 // The tag is used to indicate whether Vim thinks a menu item should be
1043 // enabled or disabled. By default Vim thinks menu items are enabled.
1047 if ([parent numberOfItems] <= idx) {
1048 [parent addItem:item];
1050 [parent insertItem:item atIndex:idx];
1054 - (void)removeMenuItemWithDescriptor:(NSArray *)desc
1056 if (!(desc && [desc count] > 0)) return;
1058 NSString *title = [desc lastObject];
1059 NSString *rootName = [desc objectAtIndex:0];
1060 if ([rootName isEqual:@"ToolBar"]) {
1062 // Only remove toolbar items, never actually remove the toolbar
1063 // itself or strange things may happen.
1064 if ([desc count] == 2) {
1065 int idx = [toolbar indexOfItemWithItemIdentifier:title];
1066 if (idx != NSNotFound)
1067 [toolbar removeItemAtIndex:idx];
1073 NSMenuItem *item = [self menuItemForDescriptor:desc];
1075 NSLog(@"Failed to remove menu item, descriptor not found: %@",
1076 [desc componentsJoinedByString:@"->"]);
1082 if ([item menu] == [NSApp mainMenu] || ![item menu]) {
1083 // NOTE: To be on the safe side we try to remove the item from
1084 // both arrays (it is ok to call removeObject: even if an array
1085 // does not contain the object to remove).
1086 [popupMenuItems removeObject:item];
1090 [[item menu] removeItem:item];
1095 - (void)enableMenuItemWithDescriptor:(NSArray *)desc state:(BOOL)on
1097 if (!(desc && [desc count] > 0)) return;
1099 /*NSLog(@"%sable item %@", on ? "En" : "Dis",
1100 [desc componentsJoinedByString:@"->"]);*/
1102 NSString *rootName = [desc objectAtIndex:0];
1103 if ([rootName isEqual:@"ToolBar"]) {
1104 if (toolbar && [desc count] == 2) {
1105 NSString *title = [desc lastObject];
1106 [[toolbar itemWithItemIdentifier:title] setEnabled:on];
1109 // Use tag to set whether item is enabled or disabled instead of
1110 // calling setEnabled:. This way the menus can autoenable themselves
1111 // but at the same time Vim can set if a menu is enabled whenever it
1113 [[self menuItemForDescriptor:desc] setTag:on];
1117 - (void)addToolbarItemToDictionaryWithLabel:(NSString *)title
1118 toolTip:(NSString *)tip
1119 icon:(NSString *)icon
1121 // If the item corresponds to a separator then do nothing, since it is
1122 // already defined by Cocoa.
1123 if (!title || [title isEqual:NSToolbarSeparatorItemIdentifier]
1124 || [title isEqual:NSToolbarSpaceItemIdentifier]
1125 || [title isEqual:NSToolbarFlexibleSpaceItemIdentifier])
1128 NSToolbarItem *item = [[NSToolbarItem alloc] initWithItemIdentifier:title];
1129 [item setLabel:title];
1130 [item setToolTip:tip];
1131 [item setAction:@selector(vimToolbarItemAction:)];
1132 [item setAutovalidates:NO];
1134 NSImage *img = [NSImage imageNamed:icon];
1136 img = [[[NSImage alloc] initByReferencingFile:icon] autorelease];
1137 if (!(img && [img isValid]))
1141 NSLog(@"WARNING: Could not find image with name '%@' to use as toolbar"
1142 " image for identifier '%@';"
1143 " using default toolbar icon '%@' instead.",
1144 icon, title, MMDefaultToolbarImageName);
1146 img = [NSImage imageNamed:MMDefaultToolbarImageName];
1149 [item setImage:img];
1151 [toolbarItemDict setObject:item forKey:title];
1156 - (void)addToolbarItemWithLabel:(NSString *)label
1158 icon:(NSString *)icon
1161 if (!toolbar) return;
1163 // Check for separator items.
1165 label = NSToolbarSeparatorItemIdentifier;
1166 } else if ([label length] >= 2 && [label hasPrefix:@"-"]
1167 && [label hasSuffix:@"-"]) {
1168 // The label begins and ends with '-'; decided which kind of separator
1169 // item it is by looking at the prefix.
1170 if ([label hasPrefix:@"-space"]) {
1171 label = NSToolbarSpaceItemIdentifier;
1172 } else if ([label hasPrefix:@"-flexspace"]) {
1173 label = NSToolbarFlexibleSpaceItemIdentifier;
1175 label = NSToolbarSeparatorItemIdentifier;
1179 [self addToolbarItemToDictionaryWithLabel:label toolTip:tip icon:icon];
1181 int maxIdx = [[toolbar items] count];
1182 if (maxIdx < idx) idx = maxIdx;
1184 [toolbar insertItemWithItemIdentifier:label atIndex:idx];
1187 - (void)popupMenuWithDescriptor:(NSArray *)desc
1188 atRow:(NSNumber *)row
1189 column:(NSNumber *)col
1191 NSMenu *menu = [[self menuItemForDescriptor:desc] submenu];
1194 id textView = [[windowController vimView] textView];
1197 // TODO: Let textView convert (row,col) to NSPoint.
1198 int r = [row intValue];
1199 int c = [col intValue];
1200 NSSize cellSize = [textView cellSize];
1201 pt = NSMakePoint((c+1)*cellSize.width, (r+1)*cellSize.height);
1202 pt = [textView convertPoint:pt toView:nil];
1204 pt = [[windowController window] mouseLocationOutsideOfEventStream];
1207 NSEvent *event = [NSEvent mouseEventWithType:NSRightMouseDown
1211 windowNumber:[[windowController window] windowNumber]
1217 [NSMenu popUpContextMenu:menu withEvent:event forView:textView];
1220 - (void)popupMenuWithAttributes:(NSDictionary *)attrs
1224 [self popupMenuWithDescriptor:[attrs objectForKey:@"descriptor"]
1225 atRow:[attrs objectForKey:@"row"]
1226 column:[attrs objectForKey:@"column"]];
1229 - (void)connectionDidDie:(NSNotification *)notification
1231 //NSLog(@"%@ %s%@", [self className], _cmd, notification);
1232 [self scheduleClose];
1235 - (void)scheduleClose
1237 // NOTE! This message can arrive at pretty much anytime, e.g. while
1238 // the run loop is the 'event tracking' mode. This means that Cocoa may
1239 // well be in the middle of processing some message while this message is
1240 // received. If we were to remove the vim controller straight away we may
1241 // free objects that Cocoa is currently using (e.g. view objects). The
1242 // following call ensures that the vim controller is not released until the
1243 // run loop is back in the 'default' mode.
1244 [[MMAppController sharedInstance]
1245 performSelector:@selector(removeVimController:)
1250 - (void)handleBrowseForFile:(NSDictionary *)attr
1252 if (!isInitialized) return;
1254 BOOL inDefaultMode = [[[NSRunLoop currentRunLoop] currentMode]
1255 isEqual:NSDefaultRunLoopMode];
1256 if (!inDefaultMode) {
1257 // Delay call until run loop is in default mode.
1258 [self performSelector:@selector(handleBrowseForFile:)
1264 NSString *dir = [attr objectForKey:@"dir"];
1265 BOOL saving = [[attr objectForKey:@"saving"] boolValue];
1268 // 'dir == nil' means: set dir to the pwd of the Vim process, or let
1269 // open dialog decide (depending on the below user default).
1270 BOOL trackPwd = [[NSUserDefaults standardUserDefaults]
1271 boolForKey:MMDialogsTrackPwdKey];
1273 dir = [vimState objectForKey:@"pwd"];
1277 [[NSSavePanel savePanel] beginSheetForDirectory:dir file:nil
1278 modalForWindow:[windowController window]
1280 didEndSelector:@selector(savePanelDidEnd:code:context:)
1283 NSOpenPanel *panel = [NSOpenPanel openPanel];
1284 [panel setAllowsMultipleSelection:NO];
1285 [panel setAccessoryView:openPanelAccessoryView()];
1287 [panel beginSheetForDirectory:dir file:nil types:nil
1288 modalForWindow:[windowController window]
1290 didEndSelector:@selector(savePanelDidEnd:code:context:)
1295 - (void)handleShowDialog:(NSDictionary *)attr
1297 if (!isInitialized) return;
1299 BOOL inDefaultMode = [[[NSRunLoop currentRunLoop] currentMode]
1300 isEqual:NSDefaultRunLoopMode];
1301 if (!inDefaultMode) {
1302 // Delay call until run loop is in default mode.
1303 [self performSelector:@selector(handleShowDialog:)
1309 NSArray *buttonTitles = [attr objectForKey:@"buttonTitles"];
1310 if (!(buttonTitles && [buttonTitles count])) return;
1312 int style = [[attr objectForKey:@"alertStyle"] intValue];
1313 NSString *message = [attr objectForKey:@"messageText"];
1314 NSString *text = [attr objectForKey:@"informativeText"];
1315 NSString *textFieldString = [attr objectForKey:@"textFieldString"];
1316 MMAlert *alert = [[MMAlert alloc] init];
1318 // NOTE! This has to be done before setting the informative text.
1319 if (textFieldString)
1320 [alert setTextFieldString:textFieldString];
1322 [alert setAlertStyle:style];
1325 [alert setMessageText:message];
1327 // If no message text is specified 'Alert' is used, which we don't
1328 // want, so set an empty string as message text.
1329 [alert setMessageText:@""];
1333 [alert setInformativeText:text];
1334 } else if (textFieldString) {
1335 // Make sure there is always room for the input text field.
1336 [alert setInformativeText:@""];
1339 unsigned i, count = [buttonTitles count];
1340 for (i = 0; i < count; ++i) {
1341 NSString *title = [buttonTitles objectAtIndex:i];
1342 // NOTE: The title of the button may contain the character '&' to
1343 // indicate that the following letter should be the key equivalent
1344 // associated with the button. Extract this letter and lowercase it.
1345 NSString *keyEquivalent = nil;
1346 NSRange hotkeyRange = [title rangeOfString:@"&"];
1347 if (NSNotFound != hotkeyRange.location) {
1348 if ([title length] > NSMaxRange(hotkeyRange)) {
1349 NSRange keyEquivRange = NSMakeRange(hotkeyRange.location+1, 1);
1350 keyEquivalent = [[title substringWithRange:keyEquivRange]
1354 NSMutableString *string = [NSMutableString stringWithString:title];
1355 [string deleteCharactersInRange:hotkeyRange];
1359 [alert addButtonWithTitle:title];
1361 // Set key equivalent for the button, but only if NSAlert hasn't
1362 // already done so. (Check the documentation for
1363 // - [NSAlert addButtonWithTitle:] to see what key equivalents are
1364 // automatically assigned.)
1365 NSButton *btn = [[alert buttons] lastObject];
1366 if ([[btn keyEquivalent] length] == 0 && keyEquivalent) {
1367 [btn setKeyEquivalent:keyEquivalent];
1371 [alert beginSheetModalForWindow:[windowController window]
1373 didEndSelector:@selector(alertDidEnd:code:context:)
1380 @end // MMVimController (Private)
1385 @implementation MMAlert
1388 [textField release]; textField = nil;
1392 - (void)setTextFieldString:(NSString *)textFieldString
1394 [textField release];
1395 textField = [[NSTextField alloc] init];
1396 [textField setStringValue:textFieldString];
1399 - (NSTextField *)textField
1404 - (void)setInformativeText:(NSString *)text
1407 // HACK! Add some space for the text field.
1408 [super setInformativeText:[text stringByAppendingString:@"\n\n\n"]];
1410 [super setInformativeText:text];
1414 - (void)beginSheetModalForWindow:(NSWindow *)window
1415 modalDelegate:(id)delegate
1416 didEndSelector:(SEL)didEndSelector
1417 contextInfo:(void *)contextInfo
1419 [super beginSheetModalForWindow:window
1420 modalDelegate:delegate
1421 didEndSelector:didEndSelector
1422 contextInfo:contextInfo];
1424 // HACK! Place the input text field at the bottom of the informative text
1425 // (which has been made a bit larger by adding newline characters).
1426 NSView *contentView = [[self window] contentView];
1427 NSRect rect = [contentView frame];
1428 rect.origin.y = rect.size.height;
1430 NSArray *subviews = [contentView subviews];
1431 unsigned i, count = [subviews count];
1432 for (i = 0; i < count; ++i) {
1433 NSView *view = [subviews objectAtIndex:i];
1434 if ([view isKindOfClass:[NSTextField class]]
1435 && [view frame].origin.y < rect.origin.y) {
1436 // NOTE: The informative text field is the lowest NSTextField in
1437 // the alert dialog.
1438 rect = [view frame];
1442 rect.size.height = MMAlertTextFieldHeight;
1443 [textField setFrame:rect];
1444 [contentView addSubview:textField];
1445 [textField becomeFirstResponder];
1454 isUnsafeMessage(int msgid)
1456 // Messages that may release Cocoa objects must be added to this list. For
1457 // example, UpdateTabBarMsgID may delete NSTabViewItem objects so it goes
1459 static int unsafeMessages[] = { // REASON MESSAGE IS ON THIS LIST:
1460 //OpenWindowMsgID, // Changes lots of state
1461 UpdateTabBarMsgID, // May delete NSTabViewItem
1462 RemoveMenuItemMsgID, // Deletes NSMenuItem
1463 DestroyScrollbarMsgID, // Deletes NSScroller
1464 ExecuteActionMsgID, // Impossible to predict
1465 ShowPopupMenuMsgID, // Enters modal loop
1467 EnterFullscreenMsgID, // Modifies delegate of window controller
1468 LeaveFullscreenMsgID, // Modifies delegate of window controller
1469 CloseWindowMsgID, // See note below
1472 // NOTE about CloseWindowMsgID: If this arrives at the same time as say
1473 // ExecuteActionMsgID, then the "execute" message will be lost due to it
1474 // being queued and handled after the "close" message has caused the
1475 // controller to cleanup...UNLESS we add CloseWindowMsgID to the list of
1476 // unsafe messages. This is the _only_ reason it is on this list (since
1477 // all that happens in response to it is that we schedule another message
1478 // for later handling).
1480 int i, count = sizeof(unsafeMessages)/sizeof(unsafeMessages[0]);
1481 for (i = 0; i < count; ++i)
1482 if (msgid == unsafeMessages[i])