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 processCommandQueue:. Input is sent
20 * to the backend via sendMessage:data: or addVimInput:. The latter allows
21 * execution of arbitrary stings 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 "MMVimController.h"
27 #import "MMWindowController.h"
28 #import "MMAppController.h"
30 #import "MMTextView.h"
31 #import "MMAtsuiTextView.h"
34 static NSString *MMDefaultToolbarImageName = @"Attention";
35 static int MMAlertTextFieldHeight = 22;
37 // NOTE: By default a message sent to the backend will be dropped if it cannot
38 // be delivered instantly; otherwise there is a possibility that MacVim will
39 // 'beachball' while waiting to deliver DO messages to an unresponsive Vim
40 // process. This means that you cannot rely on any message sent with
41 // sendMessage: to actually reach Vim.
42 static NSTimeInterval MMBackendProxyRequestTimeout = 0;
44 // Timeout used for setDialogReturn:.
45 static NSTimeInterval MMSetDialogReturnTimeout = 1.0;
47 // Maximum number of items in the receiveQueue. (It is hard to predict what
48 // consequences changing this number will have.)
49 static int MMReceiveQueueCap = 100;
51 static BOOL isUnsafeMessage(int msgid);
54 @interface MMAlert : NSAlert {
55 NSTextField *textField;
57 - (void)setTextFieldString:(NSString *)textFieldString;
58 - (NSTextField *)textField;
62 @interface MMVimController (Private)
63 - (void)doProcessCommandQueue:(NSArray *)queue;
64 - (void)handleMessage:(int)msgid data:(NSData *)data;
65 - (void)savePanelDidEnd:(NSSavePanel *)panel code:(int)code
66 context:(void *)context;
67 - (void)alertDidEnd:(MMAlert *)alert code:(int)code context:(void *)context;
68 - (NSMenuItem *)menuItemForDescriptor:(NSArray *)desc;
69 - (NSMenu *)parentMenuForDescriptor:(NSArray *)desc;
70 - (NSMenu *)topLevelMenuForTitle:(NSString *)title;
71 - (void)addMenuWithDescriptor:(NSArray *)desc atIndex:(int)index;
72 - (void)addMenuItemWithDescriptor:(NSArray *)desc
76 keyEquivalent:(NSString *)keyEquivalent
77 modifierMask:(int)modifierMask
78 action:(NSString *)action
79 isAlternate:(BOOL)isAlternate;
80 - (void)removeMenuItemWithDescriptor:(NSArray *)desc;
81 - (void)enableMenuItemWithDescriptor:(NSArray *)desc state:(BOOL)on;
82 - (void)addToolbarItemToDictionaryWithLabel:(NSString *)title
83 toolTip:(NSString *)tip icon:(NSString *)icon;
84 - (void)addToolbarItemWithLabel:(NSString *)label
85 tip:(NSString *)tip icon:(NSString *)icon
87 - (void)popupMenuWithDescriptor:(NSArray *)desc
89 column:(NSNumber *)col;
90 - (void)connectionDidDie:(NSNotification *)notification;
94 @interface NSToolbar (MMExtras)
95 - (int)indexOfItemWithItemIdentifier:(NSString *)identifier;
96 - (NSToolbarItem *)itemAtIndex:(int)idx;
97 - (NSToolbarItem *)itemWithItemIdentifier:(NSString *)identifier;
103 @implementation MMVimController
105 - (id)initWithBackend:(id)backend pid:(int)processIdentifier
107 if ((self = [super init])) {
109 [[MMWindowController alloc] initWithVimController:self];
110 backendProxy = [backend retain];
111 sendQueue = [NSMutableArray new];
112 receiveQueue = [NSMutableArray new];
113 popupMenuItems = [[NSMutableArray alloc] init];
114 toolbarItemDict = [[NSMutableDictionary alloc] init];
115 pid = processIdentifier;
117 NSConnection *connection = [backendProxy connectionForProxy];
119 // TODO: Check that this will not set the timeout for the root proxy
120 // (in MMAppController).
121 [connection setRequestTimeout:MMBackendProxyRequestTimeout];
123 [[NSNotificationCenter defaultCenter] addObserver:self
124 selector:@selector(connectionDidDie:)
125 name:NSConnectionDidDieNotification object:connection];
127 // Set up a main menu with only a "MacVim" menu (copied from a template
128 // which itself is set up in MainMenu.nib). The main menu is populated
130 mainMenu = [[NSMenu alloc] initWithTitle:@"MainMenu"];
131 NSMenuItem *appMenuItem = [[MMAppController sharedInstance]
132 appMenuItemTemplate];
133 appMenuItem = [[appMenuItem copy] autorelease];
135 // Note: If the title of the application menu is anything but what
136 // CFBundleName says then the application menu will not be typeset in
137 // boldface for some reason. (It should already be set when we copy
138 // from the default main menu, but this is not the case for some
140 NSString *appName = [[NSBundle mainBundle]
141 objectForInfoDictionaryKey:@"CFBundleName"];
142 [appMenuItem setTitle:appName];
144 [mainMenu addItem:appMenuItem];
154 //NSLog(@"%@ %s", [self className], _cmd);
157 [serverName release]; serverName = nil;
158 [backendProxy release]; backendProxy = nil;
159 [sendQueue release]; sendQueue = nil;
160 [receiveQueue release]; receiveQueue = nil;
162 [toolbarItemDict release]; toolbarItemDict = nil;
163 [toolbar release]; toolbar = nil;
164 [popupMenuItems release]; popupMenuItems = nil;
165 [windowController release]; windowController = nil;
167 [vimState release]; vimState = nil;
168 [mainMenu release]; mainMenu = nil;
173 - (MMWindowController *)windowController
175 return windowController;
178 - (NSDictionary *)vimState
188 - (void)setServerName:(NSString *)name
190 if (name != serverName) {
191 [serverName release];
192 serverName = [name copy];
196 - (NSString *)serverName
206 - (void)dropFiles:(NSArray *)filenames forceOpen:(BOOL)force
208 unsigned i, numberOfFiles = [filenames count];
209 NSMutableData *data = [NSMutableData data];
211 [data appendBytes:&force length:sizeof(BOOL)];
212 [data appendBytes:&numberOfFiles length:sizeof(int)];
214 for (i = 0; i < numberOfFiles; ++i) {
215 NSString *file = [filenames objectAtIndex:i];
216 int len = [file lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
219 ++len; // include NUL as well
220 [data appendBytes:&len length:sizeof(int)];
221 [data appendBytes:[file UTF8String] length:len];
225 [self sendMessage:DropFilesMsgID data:data];
228 - (void)dropString:(NSString *)string
230 int len = [string lengthOfBytesUsingEncoding:NSUTF8StringEncoding] + 1;
232 NSMutableData *data = [NSMutableData data];
234 [data appendBytes:&len length:sizeof(int)];
235 [data appendBytes:[string UTF8String] length:len];
237 [self sendMessage:DropStringMsgID data:data];
241 - (void)odbEdit:(NSArray *)filenames server:(OSType)theID path:(NSString *)path
242 token:(NSAppleEventDescriptor *)token
245 unsigned i, numberOfFiles = [filenames count];
246 NSMutableData *data = [NSMutableData data];
248 if (0 == numberOfFiles || 0 == theID)
251 [data appendBytes:&theID length:sizeof(theID)];
253 if (path && [path length] > 0) {
254 len = [path lengthOfBytesUsingEncoding:NSUTF8StringEncoding] + 1;
255 [data appendBytes:&len length:sizeof(int)];
256 [data appendBytes:[path UTF8String] length:len];
259 [data appendBytes:&len length:sizeof(int)];
263 DescType tokenType = [token descriptorType];
264 NSData *tokenData = [token data];
265 len = [tokenData length];
267 [data appendBytes:&tokenType length:sizeof(tokenType)];
268 [data appendBytes:&len length:sizeof(int)];
270 [data appendBytes:[tokenData bytes] length:len];
272 DescType tokenType = 0;
274 [data appendBytes:&tokenType length:sizeof(tokenType)];
275 [data appendBytes:&len length:sizeof(int)];
278 [data appendBytes:&numberOfFiles length:sizeof(int)];
280 for (i = 0; i < numberOfFiles; ++i) {
281 NSString *file = [filenames objectAtIndex:i];
282 len = [file lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
285 ++len; // include NUL as well
286 [data appendBytes:&len length:sizeof(unsigned)];
287 [data appendBytes:[file UTF8String] length:len];
291 [self sendMessage:ODBEditMsgID data:data];
294 - (void)sendMessage:(int)msgid data:(NSData *)data
296 //NSLog(@"sendMessage:%s (isInitialized=%d inProcessCommandQueue=%d)",
297 // MessageStrings[msgid], isInitialized, inProcessCommandQueue);
299 if (!isInitialized) return;
301 if (inProcessCommandQueue) {
302 //NSLog(@"In process command queue; delaying message send.");
303 [sendQueue addObject:[NSNumber numberWithInt:msgid]];
305 [sendQueue addObject:data];
307 [sendQueue addObject:[NSNull null]];
312 [backendProxy processInput:msgid data:data];
314 @catch (NSException *e) {
315 //NSLog(@"%@ %s Exception caught during DO call: %@",
316 // [self className], _cmd, e);
320 - (BOOL)sendMessageNow:(int)msgid data:(NSData *)data
321 timeout:(NSTimeInterval)timeout
323 // Send a message with a timeout. USE WITH EXTREME CAUTION! Sending
324 // messages in rapid succession with a timeout may cause MacVim to beach
325 // ball forever. In almost all circumstances sendMessage:data: should be
328 if (!isInitialized || inProcessCommandQueue)
331 if (timeout < 0) timeout = 0;
334 NSConnection *conn = [backendProxy connectionForProxy];
335 NSTimeInterval oldTimeout = [conn requestTimeout];
337 [conn setRequestTimeout:timeout];
340 [backendProxy processInput:msgid data:data];
342 @catch (NSException *e) {
346 [conn setRequestTimeout:oldTimeout];
352 - (void)addVimInput:(NSString *)string
354 // This is a very general method of adding input to the Vim process. It is
355 // basically the same as calling remote_send() on the process (see
356 // ':h remote_send').
358 NSData *data = [string dataUsingEncoding:NSUTF8StringEncoding];
359 [self sendMessage:AddInputMsgID data:data];
363 - (NSString *)evaluateVimExpression:(NSString *)expr
365 NSString *eval = nil;
368 eval = [backendProxy evaluateExpression:expr];
370 @catch (NSException *ex) { /* do nothing */ }
382 //NSLog(@"%@ %s", [self className], _cmd);
383 if (!isInitialized) return;
386 [toolbar setDelegate:nil];
387 [[NSNotificationCenter defaultCenter] removeObserver:self];
388 [windowController cleanup];
391 - (oneway void)showSavePanelForDirectory:(in bycopy NSString *)dir
392 title:(in bycopy NSString *)title
395 // TODO: Delay call until run loop is in default mode.
396 if (!isInitialized) return;
399 // 'dir == nil' means: set dir to the pwd of the Vim process, or let
400 // open dialog decide (depending on the below user default).
401 BOOL trackPwd = [[NSUserDefaults standardUserDefaults]
402 boolForKey:MMDialogsTrackPwdKey];
404 dir = [vimState objectForKey:@"pwd"];
408 [[NSSavePanel savePanel] beginSheetForDirectory:dir file:nil
409 modalForWindow:[windowController window]
411 didEndSelector:@selector(savePanelDidEnd:code:context:)
414 NSOpenPanel *panel = [NSOpenPanel openPanel];
415 [panel setAllowsMultipleSelection:NO];
416 [panel beginSheetForDirectory:dir file:nil types:nil
417 modalForWindow:[windowController window]
419 didEndSelector:@selector(savePanelDidEnd:code:context:)
424 - (oneway void)presentDialogWithStyle:(int)style
425 message:(in bycopy NSString *)message
426 informativeText:(in bycopy NSString *)text
427 buttonTitles:(in bycopy NSArray *)buttonTitles
428 textFieldString:(in bycopy NSString *)textFieldString
430 // TODO: Delay call until run loop is in default mode.
431 if (!(windowController && buttonTitles && [buttonTitles count])) return;
433 MMAlert *alert = [[MMAlert alloc] init];
435 // NOTE! This has to be done before setting the informative text.
437 [alert setTextFieldString:textFieldString];
439 [alert setAlertStyle:style];
442 [alert setMessageText:message];
444 // If no message text is specified 'Alert' is used, which we don't
445 // want, so set an empty string as message text.
446 [alert setMessageText:@""];
450 [alert setInformativeText:text];
451 } else if (textFieldString) {
452 // Make sure there is always room for the input text field.
453 [alert setInformativeText:@""];
456 unsigned i, count = [buttonTitles count];
457 for (i = 0; i < count; ++i) {
458 NSString *title = [buttonTitles objectAtIndex:i];
459 // NOTE: The title of the button may contain the character '&' to
460 // indicate that the following letter should be the key equivalent
461 // associated with the button. Extract this letter and lowercase it.
462 NSString *keyEquivalent = nil;
463 NSRange hotkeyRange = [title rangeOfString:@"&"];
464 if (NSNotFound != hotkeyRange.location) {
465 if ([title length] > NSMaxRange(hotkeyRange)) {
466 NSRange keyEquivRange = NSMakeRange(hotkeyRange.location+1, 1);
467 keyEquivalent = [[title substringWithRange:keyEquivRange]
471 NSMutableString *string = [NSMutableString stringWithString:title];
472 [string deleteCharactersInRange:hotkeyRange];
476 [alert addButtonWithTitle:title];
478 // Set key equivalent for the button, but only if NSAlert hasn't
479 // already done so. (Check the documentation for
480 // - [NSAlert addButtonWithTitle:] to see what key equivalents are
481 // automatically assigned.)
482 NSButton *btn = [[alert buttons] lastObject];
483 if ([[btn keyEquivalent] length] == 0 && keyEquivalent) {
484 [btn setKeyEquivalent:keyEquivalent];
488 [alert beginSheetModalForWindow:[windowController window]
490 didEndSelector:@selector(alertDidEnd:code:context:)
496 - (oneway void)processCommandQueue:(in bycopy NSArray *)queue
498 if (!isInitialized) return;
500 if (inProcessCommandQueue) {
501 // NOTE! If a synchronous DO call is made during
502 // doProcessCommandQueue: below it may happen that this method is
503 // called a second time while the synchronous message is waiting for a
504 // reply (could also happen if doProcessCommandQueue: enters a modal
505 // loop, see comment below). Since this method cannot be considered
506 // reentrant, we queue the input and return immediately.
508 // If doProcessCommandQueue: enters a modal loop (happens e.g. on
509 // ShowPopupMenuMsgID) then the receiveQueue could grow to become
510 // arbitrarily large because DO calls still get processed. To avoid
511 // this we set a cap on the size of the queue and simply clear it if it
512 // becomes too large. (That is messages will be dropped and hence Vim
513 // and MacVim will at least temporarily be out of sync.)
514 if ([receiveQueue count] >= MMReceiveQueueCap)
515 [receiveQueue removeAllObjects];
517 [receiveQueue addObject:queue];
521 inProcessCommandQueue = YES;
522 [self doProcessCommandQueue:queue];
525 for (i = 0; i < [receiveQueue count]; ++i) {
526 // Note that doProcessCommandQueue: may cause the receiveQueue to grow
527 // or get cleared (due to cap being hit). Make sure to retain the item
528 // to process or it may get released from under us.
529 NSArray *q = [[receiveQueue objectAtIndex:i] retain];
530 [self doProcessCommandQueue:q];
534 // We assume that the remaining calls make no synchronous DO calls. If
535 // that did happen anyway, the command queue could get processed out of
538 if ([sendQueue count] > 0) {
540 [backendProxy processInputAndData:sendQueue];
542 @catch (NSException *e) {
543 // Connection timed out, just ignore this.
544 //NSLog(@"WARNING! Connection timed out in %s", _cmd);
547 [sendQueue removeAllObjects];
550 [windowController processCommandQueueDidFinish];
551 [receiveQueue removeAllObjects];
552 inProcessCommandQueue = NO;
555 - (NSToolbarItem *)toolbar:(NSToolbar *)theToolbar
556 itemForItemIdentifier:(NSString *)itemId
557 willBeInsertedIntoToolbar:(BOOL)flag
559 NSToolbarItem *item = [toolbarItemDict objectForKey:itemId];
561 NSLog(@"WARNING: No toolbar item with id '%@'", itemId);
567 - (NSArray *)toolbarAllowedItemIdentifiers:(NSToolbar *)theToolbar
572 - (NSArray *)toolbarDefaultItemIdentifiers:(NSToolbar *)theToolbar
577 @end // MMVimController
581 @implementation MMVimController (Private)
583 - (void)doProcessCommandQueue:(NSArray *)queue
585 NSMutableArray *delayQueue = nil;
588 unsigned i, count = [queue count];
590 NSLog(@"WARNING: Uneven number of components (%d) in command "
591 "queue. Skipping...", count);
595 //NSLog(@"======== %s BEGIN ========", _cmd);
596 for (i = 0; i < count; i += 2) {
597 NSData *value = [queue objectAtIndex:i];
598 NSData *data = [queue objectAtIndex:i+1];
600 int msgid = *((int*)[value bytes]);
601 //NSLog(@"%s%s", _cmd, MessageStrings[msgid]);
603 BOOL inDefaultMode = [[[NSRunLoop currentRunLoop] currentMode]
604 isEqual:NSDefaultRunLoopMode];
605 if (!inDefaultMode && isUnsafeMessage(msgid)) {
606 // NOTE: Because we listen to DO messages in 'event tracking'
607 // mode we have to take extra care when doing things like
608 // releasing view items (and other Cocoa objects). Messages
609 // that may be potentially "unsafe" are delayed until the run
610 // loop is back to default mode at which time they are safe to
612 // A problem with this approach is that it is hard to
613 // classify which messages are unsafe. As a rule of thumb, if
614 // a message may release an object used by the Cocoa framework
615 // (e.g. views) then the message should be considered unsafe.
616 // Delaying messages may have undesired side-effects since it
617 // means that messages may not be processed in the order Vim
618 // sent them, so beware.
620 delayQueue = [NSMutableArray array];
622 //NSLog(@"Adding unsafe message '%s' to delay queue (mode=%@)",
623 // MessageStrings[msgid],
624 // [[NSRunLoop currentRunLoop] currentMode]);
625 [delayQueue addObject:value];
626 [delayQueue addObject:data];
628 [self handleMessage:msgid data:data];
631 //NSLog(@"======== %s END ========", _cmd);
633 @catch (NSException *e) {
634 NSLog(@"Exception caught whilst processing command queue: %@", e);
638 //NSLog(@" Flushing delay queue (%d items)", [delayQueue count]/2);
639 [self performSelectorOnMainThread:@selector(processCommandQueue:)
640 withObject:delayQueue
642 modes:[NSArray arrayWithObject:
643 NSDefaultRunLoopMode]];
647 - (void)handleMessage:(int)msgid data:(NSData *)data
649 //if (msgid != AddMenuMsgID && msgid != AddMenuItemMsgID &&
650 // msgid != EnableMenuItemMsgID)
651 // NSLog(@"%@ %s%s", [self className], _cmd, MessageStrings[msgid]);
653 if (OpenVimWindowMsgID == msgid) {
654 [windowController openWindow];
655 } else if (BatchDrawMsgID == msgid) {
656 [[[windowController vimView] textView] performBatchDrawWithData:data];
657 } else if (SelectTabMsgID == msgid) {
658 #if 0 // NOTE: Tab selection is done inside updateTabsWithData:.
659 const void *bytes = [data bytes];
660 int idx = *((int*)bytes);
661 //NSLog(@"Selecting tab with index %d", idx);
662 [windowController selectTabWithIndex:idx];
664 } else if (UpdateTabBarMsgID == msgid) {
665 [windowController updateTabsWithData:data];
666 } else if (ShowTabBarMsgID == msgid) {
667 [windowController showTabBar:YES];
668 } else if (HideTabBarMsgID == msgid) {
669 [windowController showTabBar:NO];
670 } else if (SetTextDimensionsMsgID == msgid || LiveResizeMsgID == msgid) {
671 const void *bytes = [data bytes];
672 int rows = *((int*)bytes); bytes += sizeof(int);
673 int cols = *((int*)bytes); bytes += sizeof(int);
675 [windowController setTextDimensionsWithRows:rows columns:cols
676 live:(LiveResizeMsgID==msgid)];
677 } else if (SetWindowTitleMsgID == msgid) {
678 const void *bytes = [data bytes];
679 int len = *((int*)bytes); bytes += sizeof(int);
681 NSString *string = [[NSString alloc] initWithBytes:(void*)bytes
682 length:len encoding:NSUTF8StringEncoding];
684 [windowController setTitle:string];
687 } else if (AddMenuMsgID == msgid) {
688 NSDictionary *attrs = [NSDictionary dictionaryWithData:data];
689 [self addMenuWithDescriptor:[attrs objectForKey:@"descriptor"]
690 atIndex:[[attrs objectForKey:@"index"] intValue]];
691 } else if (AddMenuItemMsgID == msgid) {
692 NSDictionary *attrs = [NSDictionary dictionaryWithData:data];
693 [self addMenuItemWithDescriptor:[attrs objectForKey:@"descriptor"]
694 atIndex:[[attrs objectForKey:@"index"] intValue]
695 tip:[attrs objectForKey:@"tip"]
696 icon:[attrs objectForKey:@"icon"]
697 keyEquivalent:[attrs objectForKey:@"keyEquivalent"]
698 modifierMask:[[attrs objectForKey:@"modifierMask"] intValue]
699 action:[attrs objectForKey:@"action"]
700 isAlternate:[[attrs objectForKey:@"isAlternate"] boolValue]];
701 } else if (RemoveMenuItemMsgID == msgid) {
702 NSDictionary *attrs = [NSDictionary dictionaryWithData:data];
703 [self removeMenuItemWithDescriptor:[attrs objectForKey:@"descriptor"]];
704 } else if (EnableMenuItemMsgID == msgid) {
705 NSDictionary *attrs = [NSDictionary dictionaryWithData:data];
706 [self enableMenuItemWithDescriptor:[attrs objectForKey:@"descriptor"]
707 state:[[attrs objectForKey:@"enable"] boolValue]];
708 } else if (ShowToolbarMsgID == msgid) {
709 const void *bytes = [data bytes];
710 int enable = *((int*)bytes); bytes += sizeof(int);
711 int flags = *((int*)bytes); bytes += sizeof(int);
713 int mode = NSToolbarDisplayModeDefault;
714 if (flags & ToolbarLabelFlag) {
715 mode = flags & ToolbarIconFlag ? NSToolbarDisplayModeIconAndLabel
716 : NSToolbarDisplayModeLabelOnly;
717 } else if (flags & ToolbarIconFlag) {
718 mode = NSToolbarDisplayModeIconOnly;
721 int size = flags & ToolbarSizeRegularFlag ? NSToolbarSizeModeRegular
722 : NSToolbarSizeModeSmall;
724 [windowController showToolbar:enable size:size mode:mode];
725 } else if (CreateScrollbarMsgID == msgid) {
726 const void *bytes = [data bytes];
727 long ident = *((long*)bytes); bytes += sizeof(long);
728 int type = *((int*)bytes); bytes += sizeof(int);
730 [windowController createScrollbarWithIdentifier:ident type:type];
731 } else if (DestroyScrollbarMsgID == msgid) {
732 const void *bytes = [data bytes];
733 long ident = *((long*)bytes); bytes += sizeof(long);
735 [windowController destroyScrollbarWithIdentifier:ident];
736 } else if (ShowScrollbarMsgID == msgid) {
737 const void *bytes = [data bytes];
738 long ident = *((long*)bytes); bytes += sizeof(long);
739 int visible = *((int*)bytes); bytes += sizeof(int);
741 [windowController showScrollbarWithIdentifier:ident state:visible];
742 } else if (SetScrollbarPositionMsgID == msgid) {
743 const void *bytes = [data bytes];
744 long ident = *((long*)bytes); bytes += sizeof(long);
745 int pos = *((int*)bytes); bytes += sizeof(int);
746 int len = *((int*)bytes); bytes += sizeof(int);
748 [windowController setScrollbarPosition:pos length:len
750 } else if (SetScrollbarThumbMsgID == msgid) {
751 const void *bytes = [data bytes];
752 long ident = *((long*)bytes); bytes += sizeof(long);
753 float val = *((float*)bytes); bytes += sizeof(float);
754 float prop = *((float*)bytes); bytes += sizeof(float);
756 [windowController setScrollbarThumbValue:val proportion:prop
758 } else if (SetFontMsgID == msgid) {
759 const void *bytes = [data bytes];
760 float size = *((float*)bytes); bytes += sizeof(float);
761 int len = *((int*)bytes); bytes += sizeof(int);
762 NSString *name = [[NSString alloc]
763 initWithBytes:(void*)bytes length:len
764 encoding:NSUTF8StringEncoding];
765 NSFont *font = [NSFont fontWithName:name size:size];
768 [windowController setFont:font];
771 } else if (SetWideFontMsgID == msgid) {
772 const void *bytes = [data bytes];
773 float size = *((float*)bytes); bytes += sizeof(float);
774 int len = *((int*)bytes); bytes += sizeof(int);
776 NSString *name = [[NSString alloc]
777 initWithBytes:(void*)bytes length:len
778 encoding:NSUTF8StringEncoding];
779 NSFont *font = [NSFont fontWithName:name size:size];
780 [windowController setWideFont:font];
784 [windowController setWideFont:nil];
786 } else if (SetDefaultColorsMsgID == msgid) {
787 const void *bytes = [data bytes];
788 unsigned bg = *((unsigned*)bytes); bytes += sizeof(unsigned);
789 unsigned fg = *((unsigned*)bytes); bytes += sizeof(unsigned);
790 NSColor *back = [NSColor colorWithArgbInt:bg];
791 NSColor *fore = [NSColor colorWithRgbInt:fg];
793 [windowController setDefaultColorsBackground:back foreground:fore];
794 } else if (ExecuteActionMsgID == msgid) {
795 const void *bytes = [data bytes];
796 int len = *((int*)bytes); bytes += sizeof(int);
797 NSString *actionName = [[NSString alloc]
798 initWithBytes:(void*)bytes length:len
799 encoding:NSUTF8StringEncoding];
801 SEL sel = NSSelectorFromString(actionName);
802 [NSApp sendAction:sel to:nil from:self];
804 [actionName release];
805 } else if (ShowPopupMenuMsgID == msgid) {
806 NSDictionary *attrs = [NSDictionary dictionaryWithData:data];
807 [self popupMenuWithDescriptor:[attrs objectForKey:@"descriptor"]
808 atRow:[attrs objectForKey:@"row"]
809 column:[attrs objectForKey:@"column"]];
810 } else if (SetMouseShapeMsgID == msgid) {
811 const void *bytes = [data bytes];
812 int shape = *((int*)bytes); bytes += sizeof(int);
814 [windowController setMouseShape:shape];
815 } else if (AdjustLinespaceMsgID == msgid) {
816 const void *bytes = [data bytes];
817 int linespace = *((int*)bytes); bytes += sizeof(int);
819 [windowController adjustLinespace:linespace];
820 } else if (ActivateMsgID == msgid) {
821 //NSLog(@"ActivateMsgID");
822 [NSApp activateIgnoringOtherApps:YES];
823 [[windowController window] makeKeyAndOrderFront:self];
824 } else if (SetServerNameMsgID == msgid) {
825 NSString *name = [[NSString alloc] initWithData:data
826 encoding:NSUTF8StringEncoding];
827 [self setServerName:name];
829 } else if (EnterFullscreenMsgID == msgid) {
830 const void *bytes = [data bytes];
831 int fuoptions = *((int*)bytes); bytes += sizeof(int);
832 int bg = *((int*)bytes);
833 NSColor *back = [NSColor colorWithArgbInt:bg];
835 [windowController enterFullscreen:fuoptions backgroundColor:back];
836 } else if (LeaveFullscreenMsgID == msgid) {
837 [windowController leaveFullscreen];
838 } else if (BuffersNotModifiedMsgID == msgid) {
839 [windowController setBuffersModified:NO];
840 } else if (BuffersModifiedMsgID == msgid) {
841 [windowController setBuffersModified:YES];
842 } else if (SetPreEditPositionMsgID == msgid) {
843 const int *dim = (const int*)[data bytes];
844 [[[windowController vimView] textView] setPreEditRow:dim[0]
846 } else if (EnableAntialiasMsgID == msgid) {
847 [[[windowController vimView] textView] setAntialias:YES];
848 } else if (DisableAntialiasMsgID == msgid) {
849 [[[windowController vimView] textView] setAntialias:NO];
850 } else if (SetVimStateMsgID == msgid) {
851 NSDictionary *dict = [NSDictionary dictionaryWithData:data];
854 vimState = [dict retain];
856 // IMPORTANT: When adding a new message, make sure to update
857 // isUnsafeMessage() if necessary!
859 NSLog(@"WARNING: Unknown message received (msgid=%d)", msgid);
863 - (void)savePanelDidEnd:(NSSavePanel *)panel code:(int)code
864 context:(void *)context
866 NSString *path = (code == NSOKButton) ? [panel filename] : nil;
868 // NOTE! setDialogReturn: is a synchronous call so set a proper timeout to
869 // avoid waiting forever for it to finish. We make this a synchronous call
870 // so that we can be fairly certain that Vim doesn't think the dialog box
871 // is still showing when MacVim has in fact already dismissed it.
872 NSConnection *conn = [backendProxy connectionForProxy];
873 NSTimeInterval oldTimeout = [conn requestTimeout];
874 [conn setRequestTimeout:MMSetDialogReturnTimeout];
877 [backendProxy setDialogReturn:path];
879 // Add file to the "Recent Files" menu (this ensures that files that
880 // are opened/saved from a :browse command are added to this menu).
882 [[NSDocumentController sharedDocumentController]
883 noteNewRecentFilePath:path];
885 @catch (NSException *e) {
886 NSLog(@"Exception caught in %s %@", _cmd, e);
889 [conn setRequestTimeout:oldTimeout];
893 - (void)alertDidEnd:(MMAlert *)alert code:(int)code context:(void *)context
897 code = code - NSAlertFirstButtonReturn + 1;
899 if ([alert isKindOfClass:[MMAlert class]] && [alert textField]) {
900 ret = [NSArray arrayWithObjects:[NSNumber numberWithInt:code],
901 [[alert textField] stringValue], nil];
903 ret = [NSArray arrayWithObject:[NSNumber numberWithInt:code]];
907 [backendProxy setDialogReturn:ret];
909 @catch (NSException *e) {
910 NSLog(@"Exception caught in %s %@", _cmd, e);
914 - (NSMenuItem *)menuItemForDescriptor:(NSArray *)desc
916 if (!(desc && [desc count] > 0)) return nil;
918 NSString *rootName = [desc objectAtIndex:0];
919 NSArray *rootItems = [rootName hasPrefix:@"PopUp"] ? popupMenuItems
920 : [mainMenu itemArray];
922 NSMenuItem *item = nil;
923 int i, count = [rootItems count];
924 for (i = 0; i < count; ++i) {
925 item = [rootItems objectAtIndex:i];
926 if ([[item title] isEqual:rootName])
930 if (i == count) return nil;
932 count = [desc count];
933 for (i = 1; i < count; ++i) {
934 item = [[item submenu] itemWithTitle:[desc objectAtIndex:i]];
935 if (!item) return nil;
941 - (NSMenu *)parentMenuForDescriptor:(NSArray *)desc
943 if (!(desc && [desc count] > 0)) return nil;
945 NSString *rootName = [desc objectAtIndex:0];
946 NSArray *rootItems = [rootName hasPrefix:@"PopUp"] ? popupMenuItems
947 : [mainMenu itemArray];
950 int i, count = [rootItems count];
951 for (i = 0; i < count; ++i) {
952 NSMenuItem *item = [rootItems objectAtIndex:i];
953 if ([[item title] isEqual:rootName]) {
954 menu = [item submenu];
959 if (!menu) return nil;
961 count = [desc count] - 1;
962 for (i = 1; i < count; ++i) {
963 NSMenuItem *item = [menu itemWithTitle:[desc objectAtIndex:i]];
964 menu = [item submenu];
965 if (!menu) return nil;
971 - (NSMenu *)topLevelMenuForTitle:(NSString *)title
973 // Search only the top-level menus.
975 unsigned i, count = [popupMenuItems count];
976 for (i = 0; i < count; ++i) {
977 NSMenuItem *item = [popupMenuItems objectAtIndex:i];
978 if ([title isEqual:[item title]])
979 return [item submenu];
982 count = [mainMenu numberOfItems];
983 for (i = 0; i < count; ++i) {
984 NSMenuItem *item = [mainMenu itemAtIndex:i];
985 if ([title isEqual:[item title]])
986 return [item submenu];
992 - (void)addMenuWithDescriptor:(NSArray *)desc atIndex:(int)idx
994 if (!(desc && [desc count] > 0 && idx >= 0)) return;
996 NSString *rootName = [desc objectAtIndex:0];
997 if ([rootName isEqual:@"ToolBar"]) {
998 // The toolbar only has one menu, we take this as a hint to create a
999 // toolbar, then we return.
1001 // NOTE! Each toolbar must have a unique identifier, else each
1002 // window will have the same toolbar.
1003 NSString *ident = [NSString stringWithFormat:@"%d", (int)self];
1004 toolbar = [[NSToolbar alloc] initWithIdentifier:ident];
1006 [toolbar setShowsBaselineSeparator:NO];
1007 [toolbar setDelegate:self];
1008 [toolbar setDisplayMode:NSToolbarDisplayModeIconOnly];
1009 [toolbar setSizeMode:NSToolbarSizeModeSmall];
1011 [windowController setToolbar:toolbar];
1017 // This is either a main menu item or a popup menu item.
1018 NSString *title = [desc lastObject];
1019 NSMenuItem *item = [[NSMenuItem alloc] init];
1020 NSMenu *menu = [[NSMenu alloc] initWithTitle:title];
1022 [item setTitle:title];
1023 [item setSubmenu:menu];
1025 NSMenu *parent = [self parentMenuForDescriptor:desc];
1026 if (!parent && [rootName hasPrefix:@"PopUp"]) {
1027 if ([popupMenuItems count] <= idx) {
1028 [popupMenuItems addObject:item];
1030 [popupMenuItems insertObject:item atIndex:idx];
1033 // If descriptor has no parent and its not a popup (or toolbar) menu,
1034 // then it must belong to main menu.
1035 if (!parent) parent = mainMenu;
1037 if ([parent numberOfItems] <= idx) {
1038 [parent addItem:item];
1040 [parent insertItem:item atIndex:idx];
1048 - (void)addMenuItemWithDescriptor:(NSArray *)desc
1051 icon:(NSString *)icon
1052 keyEquivalent:(NSString *)keyEquivalent
1053 modifierMask:(int)modifierMask
1054 action:(NSString *)action
1055 isAlternate:(BOOL)isAlternate
1057 if (!(desc && [desc count] > 1 && idx >= 0)) return;
1059 NSString *title = [desc lastObject];
1060 NSString *rootName = [desc objectAtIndex:0];
1062 if ([rootName isEqual:@"ToolBar"]) {
1063 if (toolbar && [desc count] == 2)
1064 [self addToolbarItemWithLabel:title tip:tip icon:icon atIndex:idx];
1068 NSMenu *parent = [self parentMenuForDescriptor:desc];
1070 NSLog(@"WARNING: Menu item '%@' has no parent",
1071 [desc componentsJoinedByString:@"->"]);
1075 NSMenuItem *item = nil;
1076 if (0 == [title length]
1077 || ([title hasPrefix:@"-"] && [title hasSuffix:@"-"])) {
1078 item = [NSMenuItem separatorItem];
1079 [item setTitle:title];
1081 item = [[[NSMenuItem alloc] init] autorelease];
1082 [item setTitle:title];
1084 // Note: It is possible to set the action to a message that "doesn't
1085 // exist" without problems. We take advantage of this when adding
1086 // "dummy items" e.g. when dealing with the "Recent Files" menu (in
1087 // which case a recentFilesDummy: action is set, although it is never
1089 if ([action length] > 0)
1090 [item setAction:NSSelectorFromString(action)];
1092 [item setAction:@selector(vimMenuItemAction:)];
1093 if ([tip length] > 0) [item setToolTip:tip];
1094 if ([keyEquivalent length] > 0) {
1095 [item setKeyEquivalent:keyEquivalent];
1096 [item setKeyEquivalentModifierMask:modifierMask];
1098 [item setAlternate:isAlternate];
1100 // The tag is used to indicate whether Vim thinks a menu item should be
1101 // enabled or disabled. By default Vim thinks menu items are enabled.
1105 if ([parent numberOfItems] <= idx) {
1106 [parent addItem:item];
1108 [parent insertItem:item atIndex:idx];
1112 - (void)removeMenuItemWithDescriptor:(NSArray *)desc
1114 if (!(desc && [desc count] > 0)) return;
1116 NSString *title = [desc lastObject];
1117 NSString *rootName = [desc objectAtIndex:0];
1118 if ([rootName isEqual:@"ToolBar"]) {
1120 // Only remove toolbar items, never actually remove the toolbar
1121 // itself or strange things may happen.
1122 if ([desc count] == 2) {
1123 int idx = [toolbar indexOfItemWithItemIdentifier:title];
1124 if (idx != NSNotFound)
1125 [toolbar removeItemAtIndex:idx];
1131 NSMenuItem *item = [self menuItemForDescriptor:desc];
1133 NSLog(@"Failed to remove menu item, descriptor not found: %@",
1134 [desc componentsJoinedByString:@"->"]);
1140 if ([item menu] == [NSApp mainMenu] || ![item menu]) {
1141 // NOTE: To be on the safe side we try to remove the item from
1142 // both arrays (it is ok to call removeObject: even if an array
1143 // does not contain the object to remove).
1144 [popupMenuItems removeObject:item];
1148 [[item menu] removeItem:item];
1153 - (void)enableMenuItemWithDescriptor:(NSArray *)desc state:(BOOL)on
1155 if (!(desc && [desc count] > 0)) return;
1157 /*NSLog(@"%sable item %@", on ? "En" : "Dis",
1158 [desc componentsJoinedByString:@"->"]);*/
1160 NSString *rootName = [desc objectAtIndex:0];
1161 if ([rootName isEqual:@"ToolBar"]) {
1162 if (toolbar && [desc count] == 2) {
1163 NSString *title = [desc lastObject];
1164 [[toolbar itemWithItemIdentifier:title] setEnabled:on];
1167 // Use tag to set whether item is enabled or disabled instead of
1168 // calling setEnabled:. This way the menus can autoenable themselves
1169 // but at the same time Vim can set if a menu is enabled whenever it
1171 [[self menuItemForDescriptor:desc] setTag:on];
1175 - (void)addToolbarItemToDictionaryWithLabel:(NSString *)title
1176 toolTip:(NSString *)tip
1177 icon:(NSString *)icon
1179 // If the item corresponds to a separator then do nothing, since it is
1180 // already defined by Cocoa.
1181 if (!title || [title isEqual:NSToolbarSeparatorItemIdentifier]
1182 || [title isEqual:NSToolbarSpaceItemIdentifier]
1183 || [title isEqual:NSToolbarFlexibleSpaceItemIdentifier])
1186 NSToolbarItem *item = [[NSToolbarItem alloc] initWithItemIdentifier:title];
1187 [item setLabel:title];
1188 [item setToolTip:tip];
1189 [item setAction:@selector(vimToolbarItemAction:)];
1190 [item setAutovalidates:NO];
1192 NSImage *img = [NSImage imageNamed:icon];
1194 img = [[[NSImage alloc] initByReferencingFile:icon] autorelease];
1196 NSLog(@"WARNING: Could not find image with name '%@' to use as toolbar"
1197 " image for identifier '%@';"
1198 " using default toolbar icon '%@' instead.",
1199 icon, title, MMDefaultToolbarImageName);
1201 img = [NSImage imageNamed:MMDefaultToolbarImageName];
1204 [item setImage:img];
1206 [toolbarItemDict setObject:item forKey:title];
1211 - (void)addToolbarItemWithLabel:(NSString *)label
1213 icon:(NSString *)icon
1216 if (!toolbar) return;
1218 // Check for separator items.
1220 label = NSToolbarSeparatorItemIdentifier;
1221 } else if ([label length] >= 2 && [label hasPrefix:@"-"]
1222 && [label hasSuffix:@"-"]) {
1223 // The label begins and ends with '-'; decided which kind of separator
1224 // item it is by looking at the prefix.
1225 if ([label hasPrefix:@"-space"]) {
1226 label = NSToolbarSpaceItemIdentifier;
1227 } else if ([label hasPrefix:@"-flexspace"]) {
1228 label = NSToolbarFlexibleSpaceItemIdentifier;
1230 label = NSToolbarSeparatorItemIdentifier;
1234 [self addToolbarItemToDictionaryWithLabel:label toolTip:tip icon:icon];
1236 int maxIdx = [[toolbar items] count];
1237 if (maxIdx < idx) idx = maxIdx;
1239 [toolbar insertItemWithItemIdentifier:label atIndex:idx];
1242 - (void)popupMenuWithDescriptor:(NSArray *)desc
1243 atRow:(NSNumber *)row
1244 column:(NSNumber *)col
1246 NSMenu *menu = [[self menuItemForDescriptor:desc] submenu];
1249 id textView = [[windowController vimView] textView];
1252 // TODO: Let textView convert (row,col) to NSPoint.
1253 int r = [row intValue];
1254 int c = [col intValue];
1255 NSSize cellSize = [textView cellSize];
1256 pt = NSMakePoint((c+1)*cellSize.width, (r+1)*cellSize.height);
1257 pt = [textView convertPoint:pt toView:nil];
1259 pt = [[windowController window] mouseLocationOutsideOfEventStream];
1262 NSEvent *event = [NSEvent mouseEventWithType:NSRightMouseDown
1266 windowNumber:[[windowController window] windowNumber]
1272 [NSMenu popUpContextMenu:menu withEvent:event forView:textView];
1275 - (void)connectionDidDie:(NSNotification *)notification
1277 //NSLog(@"%@ %s%@", [self className], _cmd, notification);
1279 // NOTE! This notification can arrive at pretty much anytime, e.g. while
1280 // the run loop is the 'event tracking' mode. This means that Cocoa may
1281 // well be in the middle of processing some message while this message is
1282 // received. If we were to remove the vim controller straight away we may
1283 // free objects that Cocoa is currently using (e.g. view objects). The
1284 // following call ensures that the vim controller is not released until the
1285 // run loop is back in the 'default' mode.
1286 [[MMAppController sharedInstance]
1287 performSelectorOnMainThread:@selector(removeVimController:)
1290 modes:[NSArray arrayWithObject:
1291 NSDefaultRunLoopMode]];
1294 - (NSString *)description
1296 return [NSString stringWithFormat:@"%@ : isInitialized=%d inProcessCommandQueue=%d mainMenu=%@ popupMenuItems=%@ toolbar=%@", [self className], isInitialized, inProcessCommandQueue, mainMenu, popupMenuItems, toolbar];
1299 @end // MMVimController (Private)
1304 @implementation NSToolbar (MMExtras)
1306 - (int)indexOfItemWithItemIdentifier:(NSString *)identifier
1308 NSArray *items = [self items];
1309 int i, count = [items count];
1310 for (i = 0; i < count; ++i) {
1311 id item = [items objectAtIndex:i];
1312 if ([[item itemIdentifier] isEqual:identifier])
1319 - (NSToolbarItem *)itemAtIndex:(int)idx
1321 NSArray *items = [self items];
1322 if (idx < 0 || idx >= [items count])
1325 return [items objectAtIndex:idx];
1328 - (NSToolbarItem *)itemWithItemIdentifier:(NSString *)identifier
1330 int idx = [self indexOfItemWithItemIdentifier:identifier];
1331 return idx != NSNotFound ? [self itemAtIndex:idx] : nil;
1334 @end // NSToolbar (MMExtras)
1339 @implementation MMAlert
1342 [textField release]; textField = nil;
1346 - (void)setTextFieldString:(NSString *)textFieldString
1348 [textField release];
1349 textField = [[NSTextField alloc] init];
1350 [textField setStringValue:textFieldString];
1353 - (NSTextField *)textField
1358 - (void)setInformativeText:(NSString *)text
1361 // HACK! Add some space for the text field.
1362 [super setInformativeText:[text stringByAppendingString:@"\n\n\n"]];
1364 [super setInformativeText:text];
1368 - (void)beginSheetModalForWindow:(NSWindow *)window
1369 modalDelegate:(id)delegate
1370 didEndSelector:(SEL)didEndSelector
1371 contextInfo:(void *)contextInfo
1373 [super beginSheetModalForWindow:window
1374 modalDelegate:delegate
1375 didEndSelector:didEndSelector
1376 contextInfo:contextInfo];
1378 // HACK! Place the input text field at the bottom of the informative text
1379 // (which has been made a bit larger by adding newline characters).
1380 NSView *contentView = [[self window] contentView];
1381 NSRect rect = [contentView frame];
1382 rect.origin.y = rect.size.height;
1384 NSArray *subviews = [contentView subviews];
1385 unsigned i, count = [subviews count];
1386 for (i = 0; i < count; ++i) {
1387 NSView *view = [subviews objectAtIndex:i];
1388 if ([view isKindOfClass:[NSTextField class]]
1389 && [view frame].origin.y < rect.origin.y) {
1390 // NOTE: The informative text field is the lowest NSTextField in
1391 // the alert dialog.
1392 rect = [view frame];
1396 rect.size.height = MMAlertTextFieldHeight;
1397 [textField setFrame:rect];
1398 [contentView addSubview:textField];
1399 [textField becomeFirstResponder];
1408 isUnsafeMessage(int msgid)
1410 // Messages that may release Cocoa objects must be added to this list. For
1411 // example, UpdateTabBarMsgID may delete NSTabViewItem objects so it goes
1413 static int unsafeMessages[] = { // REASON MESSAGE IS ON THIS LIST:
1414 OpenVimWindowMsgID, // Changes lots of state
1415 UpdateTabBarMsgID, // May delete NSTabViewItem
1416 RemoveMenuItemMsgID, // Deletes NSMenuItem
1417 DestroyScrollbarMsgID, // Deletes NSScroller
1418 ExecuteActionMsgID, // Impossible to predict
1419 ShowPopupMenuMsgID, // Enters modal loop
1421 EnterFullscreenMsgID, // Modifies delegate of window controller
1422 LeaveFullscreenMsgID, // Modifies delegate of window controller
1425 int i, count = sizeof(unsafeMessages)/sizeof(unsafeMessages[0]);
1426 for (i = 0; i < count; ++i)
1427 if (msgid == unsafeMessages[i])