forked from b4winckler/macvim
/
MMAppController.m
2439 lines (2056 loc) · 89.4 KB
/
MMAppController.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/* vi:set ts=8 sts=4 sw=4 ft=objc:
*
* VIM - Vi IMproved by Bram Moolenaar
* MacVim GUI port by Bjorn Winckler
*
* Do ":help uganda" in Vim to read copying and usage conditions.
* Do ":help credits" in Vim to see a list of people who contributed.
* See README.txt for an overview of the Vim source code.
*/
/*
* MMAppController
*
* MMAppController is the delegate of NSApp and as such handles file open
* requests, application termination, etc. It sets up a named NSConnection on
* which it listens to incoming connections from Vim processes. It also
* coordinates all MMVimControllers and takes care of the main menu.
*
* A new Vim process is started by calling launchVimProcessWithArguments:.
* When the Vim process is initialized it notifies the app controller by
* sending a connectBackend:pid: message. At this point a new MMVimController
* is allocated. Afterwards, the Vim process communicates directly with its
* MMVimController.
*
* A Vim process started from the command line connects directly by sending the
* connectBackend:pid: message (launchVimProcessWithArguments: is never called
* in this case).
*
* The main menu is handled as follows. Each Vim controller keeps its own main
* menu. All menus except the "MacVim" menu are controlled by the Vim process.
* The app controller also keeps a reference to the "default main menu" which
* is set up in MainMenu.nib. When no editor window is open the default main
* menu is used. When a new editor window becomes main its main menu becomes
* the new main menu, this is done in -[MMAppController setMainMenu:].
* NOTE: Certain heuristics are used to find the "MacVim", "Windows", "File",
* and "Services" menu. If MainMenu.nib changes these heuristics may have to
* change as well. For specifics see the find... methods defined in the NSMenu
* category "MMExtras".
*/
#import "MMAppController.h"
#import "MMPreferenceController.h"
#import "MMVimController.h"
#import "MMWindowController.h"
#import "MMTextView.h"
#import "Miscellaneous.h"
#import <unistd.h>
#import <CoreServices/CoreServices.h>
#if (MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5)
// Need Carbon for TIS...() functions
#import <Carbon/Carbon.h>
#endif
#define MM_HANDLE_XCODE_MOD_EVENT 0
// Default timeout intervals on all connections.
static NSTimeInterval MMRequestTimeout = 5;
static NSTimeInterval MMReplyTimeout = 5;
static NSString *MMWebsiteString = @"http://code.google.com/p/macvim/";
#if (MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5)
// Latency (in s) between FS event occuring and being reported to MacVim.
// Should be small so that MacVim is notified of changes to the ~/.vim
// directory more or less immediately.
static CFTimeInterval MMEventStreamLatency = 0.1;
#endif
static float MMCascadeHorizontalOffset = 21;
static float MMCascadeVerticalOffset = 23;
#pragma pack(push,1)
// The alignment and sizes of these fields are based on trial-and-error. It
// may be necessary to adjust them to fit if Xcode ever changes this struct.
typedef struct
{
int16_t unused1; // 0 (not used)
int16_t lineNum; // line to select (< 0 to specify range)
int32_t startRange; // start of selection range (if line < 0)
int32_t endRange; // end of selection range (if line < 0)
int32_t unused2; // 0 (not used)
int32_t theDate; // modification date/time
} MMXcodeSelectionRange;
#pragma pack(pop)
// This is a private AppKit API gleaned from class-dump.
@interface NSKeyBindingManager : NSObject
+ (id)sharedKeyBindingManager;
- (id)dictionary;
- (void)setDictionary:(id)arg1;
@end
@interface MMAppController (MMServices)
- (void)openSelection:(NSPasteboard *)pboard userData:(NSString *)userData
error:(NSString **)error;
- (void)openFile:(NSPasteboard *)pboard userData:(NSString *)userData
error:(NSString **)error;
- (void)newFileHere:(NSPasteboard *)pboard userData:(NSString *)userData
error:(NSString **)error;
@end
@interface MMAppController (Private)
- (MMVimController *)topmostVimController;
- (int)launchVimProcessWithArguments:(NSArray *)args
workingDirectory:(NSString *)cwd;
- (NSArray *)filterFilesAndNotify:(NSArray *)files;
- (NSArray *)filterOpenFiles:(NSArray *)filenames
openFilesDict:(NSDictionary **)openFiles;
#if MM_HANDLE_XCODE_MOD_EVENT
- (void)handleXcodeModEvent:(NSAppleEventDescriptor *)event
replyEvent:(NSAppleEventDescriptor *)reply;
#endif
- (void)handleGetURLEvent:(NSAppleEventDescriptor *)event
replyEvent:(NSAppleEventDescriptor *)reply;
- (NSMutableDictionary *)extractArgumentsFromOdocEvent:
(NSAppleEventDescriptor *)desc;
- (void)scheduleVimControllerPreloadAfterDelay:(NSTimeInterval)delay;
- (void)cancelVimControllerPreloadRequests;
- (void)preloadVimController:(id)sender;
- (int)maxPreloadCacheSize;
- (MMVimController *)takeVimControllerFromCache;
- (void)clearPreloadCacheWithCount:(int)count;
- (void)rebuildPreloadCache;
- (NSDate *)rcFilesModificationDate;
- (BOOL)openVimControllerWithArguments:(NSDictionary *)arguments;
- (void)activateWhenNextWindowOpens;
- (void)startWatchingVimDir;
- (void)stopWatchingVimDir;
- (void)handleFSEvent;
- (int)executeInLoginShell:(NSString *)path arguments:(NSArray *)args;
- (void)reapChildProcesses:(id)sender;
- (void)processInputQueues:(id)sender;
- (void)addVimController:(MMVimController *)vc;
- (NSDictionary *)convertVimControllerArguments:(NSDictionary *)args
toCommandLine:(NSArray **)cmdline;
- (NSString *)workingDirectoryForArguments:(NSDictionary *)args;
- (NSScreen *)screenContainingTopLeftPoint:(NSPoint)pt;
- (void)addInputSourceChangedObserver;
- (void)removeInputSourceChangedObserver;
#if (MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5)
- (void)inputSourceChanged:(NSNotification *)notification;
#endif
@end
#if (MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5)
static void
fsEventCallback(ConstFSEventStreamRef streamRef,
void *clientCallBackInfo,
size_t numEvents,
void *eventPaths,
const FSEventStreamEventFlags eventFlags[],
const FSEventStreamEventId eventIds[])
{
[[MMAppController sharedInstance] handleFSEvent];
}
#endif
@implementation MMAppController
+ (void)initialize
{
static BOOL initDone = NO;
if (initDone) return;
initDone = YES;
ASLInit();
// HACK! The following user default must be reset, else Ctrl-q (or
// whichever key is specified by the default) will be blocked by the input
// manager (interpretKeyEvents: swallows that key). (We can't use
// NSUserDefaults since it only allows us to write to the registration
// domain and this preference has "higher precedence" than that so such a
// change would have no effect.)
CFPreferencesSetAppValue(CFSTR("NSQuotedKeystrokeBinding"),
CFSTR(""),
kCFPreferencesCurrentApplication);
// Also disable NSRepeatCountBinding -- it is not enabled by default, but
// it does not make much sense to support it since Vim has its own way of
// dealing with repeat counts.
CFPreferencesSetAppValue(CFSTR("NSRepeatCountBinding"),
CFSTR(""),
kCFPreferencesCurrentApplication);
NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithBool:NO], MMNoWindowKey,
[NSNumber numberWithInt:64], MMTabMinWidthKey,
[NSNumber numberWithInt:6*64], MMTabMaxWidthKey,
[NSNumber numberWithInt:132], MMTabOptimumWidthKey,
[NSNumber numberWithBool:YES], MMShowAddTabButtonKey,
[NSNumber numberWithInt:2], MMTextInsetLeftKey,
[NSNumber numberWithInt:1], MMTextInsetRightKey,
[NSNumber numberWithInt:1], MMTextInsetTopKey,
[NSNumber numberWithInt:1], MMTextInsetBottomKey,
@"MMTypesetter", MMTypesetterKey,
[NSNumber numberWithFloat:1], MMCellWidthMultiplierKey,
[NSNumber numberWithFloat:-1], MMBaselineOffsetKey,
[NSNumber numberWithBool:YES], MMTranslateCtrlClickKey,
[NSNumber numberWithInt:0], MMOpenInCurrentWindowKey,
[NSNumber numberWithBool:NO], MMNoFontSubstitutionKey,
[NSNumber numberWithBool:YES], MMLoginShellKey,
[NSNumber numberWithInt:0], MMRendererKey,
[NSNumber numberWithInt:MMUntitledWindowAlways],
MMUntitledWindowKey,
[NSNumber numberWithBool:NO], MMTexturedWindowKey,
[NSNumber numberWithBool:NO], MMZoomBothKey,
@"", MMLoginShellCommandKey,
@"", MMLoginShellArgumentKey,
[NSNumber numberWithBool:YES], MMDialogsTrackPwdKey,
[NSNumber numberWithInt:3], MMOpenLayoutKey,
[NSNumber numberWithBool:NO], MMVerticalSplitKey,
[NSNumber numberWithInt:0], MMPreloadCacheSizeKey,
[NSNumber numberWithInt:0], MMLastWindowClosedBehaviorKey,
#ifdef INCLUDE_OLD_IM_CODE
[NSNumber numberWithBool:YES], MMUseInlineImKey,
#endif // INCLUDE_OLD_IM_CODE
nil];
[[NSUserDefaults standardUserDefaults] registerDefaults:dict];
NSArray *types = [NSArray arrayWithObject:NSStringPboardType];
[NSApp registerServicesMenuSendTypes:types returnTypes:types];
// NOTE: Set the current directory to user's home directory, otherwise it
// will default to the root directory. (This matters since new Vim
// processes inherit MacVim's environment variables.)
[[NSFileManager defaultManager] changeCurrentDirectoryPath:
NSHomeDirectory()];
}
- (id)init
{
if (!(self = [super init])) return nil;
vimControllers = [NSMutableArray new];
cachedVimControllers = [NSMutableArray new];
preloadPid = -1;
pidArguments = [NSMutableDictionary new];
inputQueues = [NSMutableDictionary new];
// NOTE: Do not use the default connection since the Logitech Control
// Center (LCC) input manager steals and this would cause MacVim to
// never open any windows. (This is a bug in LCC but since they are
// unlikely to fix it, we graciously give them the default connection.)
connection = [[NSConnection alloc] initWithReceivePort:[NSPort port]
sendPort:nil];
[connection setRootObject:self];
[connection setRequestTimeout:MMRequestTimeout];
[connection setReplyTimeout:MMReplyTimeout];
// NOTE! If the name of the connection changes here it must also be
// updated in MMBackend.m.
NSString *name = [NSString stringWithFormat:@"%@-connection",
[[NSBundle mainBundle] bundlePath]];
if (![connection registerName:name]) {
ASLogCrit(@"Failed to register connection with name '%@'", name);
[connection release]; connection = nil;
}
return self;
}
- (void)dealloc
{
ASLogDebug(@"");
[connection release]; connection = nil;
[inputQueues release]; inputQueues = nil;
[pidArguments release]; pidArguments = nil;
[vimControllers release]; vimControllers = nil;
[cachedVimControllers release]; cachedVimControllers = nil;
[openSelectionString release]; openSelectionString = nil;
[recentFilesMenuItem release]; recentFilesMenuItem = nil;
[defaultMainMenu release]; defaultMainMenu = nil;
[appMenuItemTemplate release]; appMenuItemTemplate = nil;
[super dealloc];
}
- (void)applicationWillFinishLaunching:(NSNotification *)notification
{
// Remember the default menu so that it can be restored if the user closes
// all editor windows.
defaultMainMenu = [[NSApp mainMenu] retain];
// Store a copy of the default app menu so we can use this as a template
// for all other menus. We make a copy here because the "Services" menu
// will not yet have been populated at this time. If we don't we get
// problems trying to set key equivalents later on because they might clash
// with items on the "Services" menu.
appMenuItemTemplate = [defaultMainMenu itemAtIndex:0];
appMenuItemTemplate = [appMenuItemTemplate copy];
// Set up the "Open Recent" menu. See
// http://lapcatsoftware.com/blog/2007/07/10/
// working-without-a-nib-part-5-open-recent-menu/
// and
// http://www.cocoabuilder.com/archive/message/cocoa/2007/8/15/187793
// for more information.
//
// The menu itself is created in MainMenu.nib but we still seem to have to
// hack around a bit to get it to work. (This has to be done in
// applicationWillFinishLaunching at the latest, otherwise it doesn't
// work.)
NSMenu *fileMenu = [defaultMainMenu findFileMenu];
if (fileMenu) {
int idx = [fileMenu indexOfItemWithAction:@selector(fileOpen:)];
if (idx >= 0 && idx+1 < [fileMenu numberOfItems])
recentFilesMenuItem = [fileMenu itemWithTitle:@"Open Recent"];
[[recentFilesMenuItem submenu] performSelector:@selector(_setMenuName:)
withObject:@"NSRecentDocumentsMenu"];
// Note: The "Recent Files" menu must be moved around since there is no
// -[NSApp setRecentFilesMenu:] method. We keep a reference to it to
// facilitate this move (see setMainMenu: below).
[recentFilesMenuItem retain];
}
#if MM_HANDLE_XCODE_MOD_EVENT
[[NSAppleEventManager sharedAppleEventManager]
setEventHandler:self
andSelector:@selector(handleXcodeModEvent:replyEvent:)
forEventClass:'KAHL'
andEventID:'MOD '];
#endif
// Register 'mvim://' URL handler
[[NSAppleEventManager sharedAppleEventManager]
setEventHandler:self
andSelector:@selector(handleGetURLEvent:replyEvent:)
forEventClass:kInternetEventClass
andEventID:kAEGetURL];
// Disable the default Cocoa "Key Bindings" since they interfere with the
// way Vim handles keyboard input. Cocoa reads bindings from
// /System/Library/Frameworks/AppKit.framework/Resources/
// StandardKeyBinding.dict
// and
// ~/Library/KeyBindings/DefaultKeyBinding.dict
// To avoid having the user accidentally break keyboard handling (by
// modifying the latter in some unexpected way) in MacVim we load our own
// key binding dictionary from Resource/KeyBinding.plist. We can't disable
// the bindings completely since it would break keyboard handling in
// dialogs so the our custom dictionary contains all the entries from the
// former location.
//
// It is possible to disable key bindings completely by not calling
// interpretKeyEvents: in keyDown: but this also disables key bindings used
// by certain input methods. E.g. Ctrl-Shift-; would no longer work in
// the Kotoeri input manager.
//
// To solve this problem we access a private API and set the key binding
// dictionary to our own custom dictionary here. At this time Cocoa will
// have already read the above mentioned dictionaries so it (hopefully)
// won't try to change the key binding dictionary again after this point.
NSKeyBindingManager *mgr = [NSKeyBindingManager sharedKeyBindingManager];
NSBundle *mainBundle = [NSBundle mainBundle];
NSString *path = [mainBundle pathForResource:@"KeyBinding"
ofType:@"plist"];
NSDictionary *dict = [NSDictionary dictionaryWithContentsOfFile:path];
if (mgr && dict) {
[mgr setDictionary:dict];
} else {
ASLogNotice(@"Failed to override the Cocoa key bindings. Keyboard "
"input may behave strangely as a result (path=%@).", path);
}
}
- (void)applicationDidFinishLaunching:(NSNotification *)notification
{
[NSApp setServicesProvider:self];
if ([self maxPreloadCacheSize] > 0) {
[self scheduleVimControllerPreloadAfterDelay:2];
[self startWatchingVimDir];
}
[self addInputSourceChangedObserver];
ASLogInfo(@"MacVim finished launching");
}
- (BOOL)applicationShouldOpenUntitledFile:(NSApplication *)sender
{
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
NSAppleEventManager *aem = [NSAppleEventManager sharedAppleEventManager];
NSAppleEventDescriptor *desc = [aem currentAppleEvent];
// The user default MMUntitledWindow can be set to control whether an
// untitled window should open on 'Open' and 'Reopen' events.
int untitledWindowFlag = [ud integerForKey:MMUntitledWindowKey];
BOOL isAppOpenEvent = [desc eventID] == kAEOpenApplication;
if (isAppOpenEvent && (untitledWindowFlag & MMUntitledWindowOnOpen) == 0)
return NO;
BOOL isAppReopenEvent = [desc eventID] == kAEReopenApplication;
if (isAppReopenEvent
&& (untitledWindowFlag & MMUntitledWindowOnReopen) == 0)
return NO;
// When a process is started from the command line, the 'Open' event may
// contain a parameter to surpress the opening of an untitled window.
desc = [desc paramDescriptorForKeyword:keyAEPropData];
desc = [desc paramDescriptorForKeyword:keyMMUntitledWindow];
if (desc && ![desc booleanValue])
return NO;
// Never open an untitled window if there is at least one open window or if
// there are processes that are currently launching.
if ([vimControllers count] > 0 || [pidArguments count] > 0)
return NO;
// NOTE! This way it possible to start the app with the command-line
// argument '-nowindow yes' and no window will be opened by default but
// this argument will only be heeded when the application is opening.
if (isAppOpenEvent && [ud boolForKey:MMNoWindowKey] == YES)
return NO;
return YES;
}
- (BOOL)applicationOpenUntitledFile:(NSApplication *)sender
{
ASLogDebug(@"Opening untitled window...");
[self newWindow:self];
return YES;
}
- (void)application:(NSApplication *)sender openFiles:(NSArray *)filenames
{
ASLogInfo(@"Opening files %@", filenames);
// Extract ODB/Xcode/Spotlight parameters from the current Apple event,
// sort the filenames, and then let openFiles:withArguments: do the heavy
// lifting.
if (!(filenames && [filenames count] > 0))
return;
// Sort filenames since the Finder doesn't take care in preserving the
// order in which files are selected anyway (and "sorted" is more
// predictable than "random").
if ([filenames count] > 1)
filenames = [filenames sortedArrayUsingSelector:
@selector(localizedCompare:)];
// Extract ODB/Xcode/Spotlight parameters from the current Apple event
NSMutableDictionary *arguments = [self extractArgumentsFromOdocEvent:
[[NSAppleEventManager sharedAppleEventManager] currentAppleEvent]];
if ([self openFiles:filenames withArguments:arguments]) {
[NSApp replyToOpenOrPrint:NSApplicationDelegateReplySuccess];
} else {
// TODO: Notify user of failure?
[NSApp replyToOpenOrPrint:NSApplicationDelegateReplyFailure];
}
}
- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender
{
return (MMTerminateWhenLastWindowClosed ==
[[NSUserDefaults standardUserDefaults]
integerForKey:MMLastWindowClosedBehaviorKey]);
}
- (NSApplicationTerminateReply)applicationShouldTerminate:
(NSApplication *)sender
{
// TODO: Follow Apple's guidelines for 'Graceful Application Termination'
// (in particular, allow user to review changes and save).
int reply = NSTerminateNow;
BOOL modifiedBuffers = NO;
// Go through Vim controllers, checking for modified buffers.
NSEnumerator *e = [vimControllers objectEnumerator];
id vc;
while ((vc = [e nextObject])) {
if ([vc hasModifiedBuffer]) {
modifiedBuffers = YES;
break;
}
}
if (modifiedBuffers) {
NSAlert *alert = [[NSAlert alloc] init];
[alert setAlertStyle:NSWarningAlertStyle];
[alert addButtonWithTitle:NSLocalizedString(@"Quit",
@"Dialog button")];
[alert addButtonWithTitle:NSLocalizedString(@"Cancel",
@"Dialog button")];
[alert setMessageText:NSLocalizedString(@"Quit without saving?",
@"Quit dialog with changed buffers, title")];
[alert setInformativeText:NSLocalizedString(
@"There are modified buffers, "
"if you quit now all changes will be lost. Quit anyway?",
@"Quit dialog with changed buffers, text")];
if ([alert runModal] != NSAlertFirstButtonReturn)
reply = NSTerminateCancel;
[alert release];
} else {
// No unmodified buffers, but give a warning if there are multiple
// windows and/or tabs open.
int numWindows = [vimControllers count];
int numTabs = 0;
// Count the number of open tabs
e = [vimControllers objectEnumerator];
while ((vc = [e nextObject]))
numTabs += [[vc objectForVimStateKey:@"numTabs"] intValue];
if (numWindows > 1 || numTabs > 1) {
NSAlert *alert = [[NSAlert alloc] init];
[alert setAlertStyle:NSWarningAlertStyle];
[alert addButtonWithTitle:NSLocalizedString(@"Quit",
@"Dialog button")];
[alert addButtonWithTitle:NSLocalizedString(@"Cancel",
@"Dialog button")];
[alert setMessageText:NSLocalizedString(
@"Are you sure you want to quit MacVim?",
@"Quit dialog with no changed buffers, title")];
NSString *info = nil;
if (numWindows > 1) {
if (numTabs > numWindows)
info = [NSString stringWithFormat:NSLocalizedString(
@"There are %d windows open in MacVim, with a "
"total of %d tabs. Do you want to quit anyway?",
@"Quit dialog with no changed buffers, text"),
numWindows, numTabs];
else
info = [NSString stringWithFormat:NSLocalizedString(
@"There are %d windows open in MacVim. "
"Do you want to quit anyway?",
@"Quit dialog with no changed buffers, text"),
numWindows];
} else {
info = [NSString stringWithFormat:NSLocalizedString(
@"There are %d tabs open in MacVim. "
"Do you want to quit anyway?",
@"Quit dialog with no changed buffers, text"),
numTabs];
}
[alert setInformativeText:info];
if ([alert runModal] != NSAlertFirstButtonReturn)
reply = NSTerminateCancel;
[alert release];
}
}
// Tell all Vim processes to terminate now (otherwise they'll leave swap
// files behind).
if (NSTerminateNow == reply) {
e = [vimControllers objectEnumerator];
id vc;
while ((vc = [e nextObject])) {
ASLogDebug(@"Terminate pid=%d", [vc pid]);
[vc sendMessage:TerminateNowMsgID data:nil];
}
e = [cachedVimControllers objectEnumerator];
while ((vc = [e nextObject])) {
ASLogDebug(@"Terminate pid=%d (cached)", [vc pid]);
[vc sendMessage:TerminateNowMsgID data:nil];
}
// If a Vim process is being preloaded as we quit we have to forcibly
// kill it since we have not established a connection yet.
if (preloadPid > 0) {
ASLogDebug(@"Kill incomplete preloaded process pid=%d", preloadPid);
kill(preloadPid, SIGKILL);
}
// If a Vim process was loading as we quit we also have to kill it.
e = [[pidArguments allKeys] objectEnumerator];
NSNumber *pidKey;
while ((pidKey = [e nextObject])) {
ASLogDebug(@"Kill incomplete process pid=%d", [pidKey intValue]);
kill([pidKey intValue], SIGKILL);
}
// Sleep a little to allow all the Vim processes to exit.
usleep(10000);
}
return reply;
}
- (void)applicationWillTerminate:(NSNotification *)notification
{
ASLogInfo(@"Terminating MacVim...");
[self removeInputSourceChangedObserver];
[self stopWatchingVimDir];
#if MM_HANDLE_XCODE_MOD_EVENT
[[NSAppleEventManager sharedAppleEventManager]
removeEventHandlerForEventClass:'KAHL'
andEventID:'MOD '];
#endif
// This will invalidate all connections (since they were spawned from this
// connection).
[connection invalidate];
[NSApp setDelegate:nil];
// Try to wait for all child processes to avoid leaving zombies behind (but
// don't wait around for too long).
NSDate *timeOutDate = [NSDate dateWithTimeIntervalSinceNow:2];
while ([timeOutDate timeIntervalSinceNow] > 0) {
[self reapChildProcesses:nil];
if (numChildProcesses <= 0)
break;
ASLogDebug(@"%d processes still left, hold on...", numChildProcesses);
// Run in NSConnectionReplyMode while waiting instead of calling e.g.
// usleep(). Otherwise incoming messages may clog up the DO queues and
// the outgoing TerminateNowMsgID sent earlier never reaches the Vim
// process.
// This has at least one side-effect, namely we may receive the
// annoying "dropping incoming DO message". (E.g. this may happen if
// you quickly hit Cmd-n several times in a row and then immediately
// press Cmd-q, Enter.)
while (CFRunLoopRunInMode((CFStringRef)NSConnectionReplyMode,
0.05, true) == kCFRunLoopRunHandledSource)
; // do nothing
}
if (numChildProcesses > 0) {
ASLogNotice(@"%d zombies left behind", numChildProcesses);
}
}
+ (MMAppController *)sharedInstance
{
// Note: The app controller is a singleton which is instantiated in
// MainMenu.nib where it is also connected as the delegate of NSApp.
id delegate = [NSApp delegate];
return [delegate isKindOfClass:self] ? (MMAppController*)delegate : nil;
}
- (NSMenu *)defaultMainMenu
{
return defaultMainMenu;
}
- (NSMenuItem *)appMenuItemTemplate
{
return appMenuItemTemplate;
}
- (void)removeVimController:(id)controller
{
ASLogDebug(@"Remove Vim controller pid=%d id=%d (processingFlag=%d)",
[controller pid], [controller vimControllerId], processingFlag);
NSUInteger idx = [vimControllers indexOfObject:controller];
if (NSNotFound == idx) {
ASLogDebug(@"Controller not found, probably due to duplicate removal");
return;
}
[controller retain];
[vimControllers removeObjectAtIndex:idx];
[controller cleanup];
[controller release];
if (![vimControllers count]) {
// The last editor window just closed so restore the main menu back to
// its default state (which is defined in MainMenu.nib).
[self setMainMenu:defaultMainMenu];
BOOL hide = (MMHideWhenLastWindowClosed ==
[[NSUserDefaults standardUserDefaults]
integerForKey:MMLastWindowClosedBehaviorKey]);
if (hide)
[NSApp hide:self];
}
// There is a small delay before the Vim process actually exits so wait a
// little before trying to reap the child process. If the process still
// hasn't exited after this wait it won't be reaped until the next time
// reapChildProcesses: is called (but this should be harmless).
[self performSelector:@selector(reapChildProcesses:)
withObject:nil
afterDelay:0.1];
}
- (void)windowControllerWillOpen:(MMWindowController *)windowController
{
NSPoint topLeft = NSZeroPoint;
NSWindow *cascadeFrom = [[[self topmostVimController] windowController]
window];
NSWindow *win = [windowController window];
if (!win) return;
// Heuristic to determine where to position the window:
// 1. Use the default top left position (set using :winpos in .[g]vimrc)
// 2. Cascade from an existing window
// 3. Use autosaved position
// If all of the above fail, then the window position is not changed.
if ([windowController getDefaultTopLeft:&topLeft]) {
// Make sure the window is not cascaded (note that topLeft was set in
// the above call).
cascadeFrom = nil;
} else if (cascadeFrom) {
NSRect frame = [cascadeFrom frame];
topLeft = NSMakePoint(frame.origin.x, NSMaxY(frame));
} else {
NSString *topLeftString = [[NSUserDefaults standardUserDefaults]
stringForKey:MMTopLeftPointKey];
if (topLeftString)
topLeft = NSPointFromString(topLeftString);
}
if (!NSEqualPoints(topLeft, NSZeroPoint)) {
// Try to tile from the correct screen in case the user has multiple
// monitors ([win screen] always seems to return the "main" screen).
//
// TODO: Check for screen _closest_ to top left?
NSScreen *screen = [self screenContainingTopLeftPoint:topLeft];
if (!screen)
screen = [win screen];
if (cascadeFrom) {
// Do manual cascading instead of using
// -[MMWindow cascadeTopLeftFromPoint:] since it is rather
// unpredictable.
topLeft.x += MMCascadeHorizontalOffset;
topLeft.y -= MMCascadeVerticalOffset;
}
if (screen) {
// Constrain the window so that it is entirely visible on the
// screen. If it sticks out on the right, move it all the way
// left. If it sticks out on the bottom, move it all the way up.
// (Assumption: the cascading offsets are positive.)
NSRect screenFrame = [screen frame];
NSSize winSize = [win frame].size;
NSRect winFrame =
{ { topLeft.x, topLeft.y - winSize.height }, winSize };
if (NSMaxX(winFrame) > NSMaxX(screenFrame))
topLeft.x = NSMinX(screenFrame);
if (NSMinY(winFrame) < NSMinY(screenFrame))
topLeft.y = NSMaxY(screenFrame);
} else {
ASLogNotice(@"Window not on screen, don't constrain position");
}
[win setFrameTopLeftPoint:topLeft];
}
if (1 == [vimControllers count]) {
// The first window autosaves its position. (The autosaving
// features of Cocoa are not used because we need more control over
// what is autosaved and when it is restored.)
[windowController setWindowAutosaveKey:MMTopLeftPointKey];
}
if (openSelectionString) {
// TODO: Pass this as a parameter instead! Get rid of
// 'openSelectionString' etc.
//
// There is some text to paste into this window as a result of the
// services menu "Open selection ..." being used.
[[windowController vimController] dropString:openSelectionString];
[openSelectionString release];
openSelectionString = nil;
}
if (shouldActivateWhenNextWindowOpens) {
[NSApp activateIgnoringOtherApps:YES];
shouldActivateWhenNextWindowOpens = NO;
}
}
- (void)setMainMenu:(NSMenu *)mainMenu
{
if ([NSApp mainMenu] == mainMenu) return;
// If the new menu has a "Recent Files" dummy item, then swap the real item
// for the dummy. We are forced to do this since Cocoa initializes the
// "Recent Files" menu and there is no way to simply point Cocoa to a new
// item each time the menus are swapped.
NSMenu *fileMenu = [mainMenu findFileMenu];
if (recentFilesMenuItem && fileMenu) {
int dummyIdx =
[fileMenu indexOfItemWithAction:@selector(recentFilesDummy:)];
if (dummyIdx >= 0) {
NSMenuItem *dummyItem = [[fileMenu itemAtIndex:dummyIdx] retain];
[fileMenu removeItemAtIndex:dummyIdx];
NSMenu *recentFilesParentMenu = [recentFilesMenuItem menu];
int idx = [recentFilesParentMenu indexOfItem:recentFilesMenuItem];
if (idx >= 0) {
[[recentFilesMenuItem retain] autorelease];
[recentFilesParentMenu removeItemAtIndex:idx];
[recentFilesParentMenu insertItem:dummyItem atIndex:idx];
}
[fileMenu insertItem:recentFilesMenuItem atIndex:dummyIdx];
[dummyItem release];
}
}
// Now set the new menu. Notice that we keep one menu for each editor
// window since each editor can have its own set of menus. When swapping
// menus we have to tell Cocoa where the new "MacVim", "Windows", and
// "Services" menu are.
[NSApp setMainMenu:mainMenu];
// Setting the "MacVim" (or "Application") menu ensures that it is typeset
// in boldface. (The setAppleMenu: method used to be public but is now
// private so this will have to be considered a bit of a hack!)
NSMenu *appMenu = [mainMenu findApplicationMenu];
[NSApp performSelector:@selector(setAppleMenu:) withObject:appMenu];
NSMenu *servicesMenu = [mainMenu findServicesMenu];
[NSApp setServicesMenu:servicesMenu];
NSMenu *windowsMenu = [mainMenu findWindowsMenu];
if (windowsMenu) {
// Cocoa isn't clever enough to get rid of items it has added to the
// "Windows" menu so we have to do it ourselves otherwise there will be
// multiple menu items for each window in the "Windows" menu.
// This code assumes that the only items Cocoa add are ones which
// send off the action makeKeyAndOrderFront:. (Cocoa will not add
// another separator item if the last item on the "Windows" menu
// already is a separator, so we needen't worry about separators.)
int i, count = [windowsMenu numberOfItems];
for (i = count-1; i >= 0; --i) {
NSMenuItem *item = [windowsMenu itemAtIndex:i];
if ([item action] == @selector(makeKeyAndOrderFront:))
[windowsMenu removeItem:item];
}
}
[NSApp setWindowsMenu:windowsMenu];
}
- (NSArray *)filterOpenFiles:(NSArray *)filenames
{
return [self filterOpenFiles:filenames openFilesDict:nil];
}
- (BOOL)openFiles:(NSArray *)filenames withArguments:(NSDictionary *)args
{
// Opening files works like this:
// a) filter out any already open files
// b) open any remaining files
//
// A file is opened in an untitled window if there is one (it may be
// currently launching, or it may already be visible), otherwise a new
// window is opened.
//
// Each launching Vim process has a dictionary of arguments that are passed
// to the process when in checks in (via connectBackend:pid:). The
// arguments for each launching process can be looked up by its PID (in the
// pidArguments dictionary).
NSMutableDictionary *arguments = (args ? [[args mutableCopy] autorelease]
: [NSMutableDictionary dictionary]);
filenames = normalizeFilenames(filenames);
//
// a) Filter out any already open files
//
NSString *firstFile = [filenames objectAtIndex:0];
MMVimController *firstController = nil;
NSDictionary *openFilesDict = nil;
filenames = [self filterOpenFiles:filenames openFilesDict:&openFilesDict];
// Pass arguments to vim controllers that had files open.
id key;
NSEnumerator *e = [openFilesDict keyEnumerator];
// (Indicate that we do not wish to open any files at the moment.)
[arguments setObject:[NSNumber numberWithBool:YES] forKey:@"dontOpen"];
while ((key = [e nextObject])) {
NSArray *files = [openFilesDict objectForKey:key];
[arguments setObject:files forKey:@"filenames"];
MMVimController *vc = [key pointerValue];
[vc passArguments:arguments];
// If this controller holds the first file, then remember it for later.
if ([files containsObject:firstFile])
firstController = vc;
}
// The meaning of "layout" is defined by the WIN_* defines in main.c.
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
int layout = [ud integerForKey:MMOpenLayoutKey];
BOOL splitVert = [ud boolForKey:MMVerticalSplitKey];
BOOL openInCurrentWindow = [ud boolForKey:MMOpenInCurrentWindowKey];
if (splitVert && MMLayoutHorizontalSplit == layout)
layout = MMLayoutVerticalSplit;
if (layout < 0 || (layout > MMLayoutTabs && openInCurrentWindow))
layout = MMLayoutTabs;
if ([filenames count] == 0) {
// Raise the window containing the first file that was already open,
// and make sure that the tab containing that file is selected. Only
// do this when there are no more files to open, otherwise sometimes
// the window with 'firstFile' will be raised, other times it might be
// the window that will open with the files in the 'filenames' array.
firstFile = [firstFile stringByEscapingSpecialFilenameCharacters];
NSString *bufCmd = @"tab sb";
switch (layout) {
case MMLayoutHorizontalSplit: bufCmd = @"sb"; break;
case MMLayoutVerticalSplit: bufCmd = @"vert sb"; break;
case MMLayoutArglist: bufCmd = @"b"; break;
}
NSString *input = [NSString stringWithFormat:@"<C-\\><C-N>"
":let oldswb=&swb|let &swb=\"useopen,usetab\"|"
"%@ %@|let &swb=oldswb|unl oldswb|"
"cal foreground()<CR>", bufCmd, firstFile];
[firstController addVimInput:input];
return YES;
}
// Add filenames to "Recent Files" menu, unless they are being edited
// remotely (using ODB).
if ([arguments objectForKey:@"remoteID"] == nil) {
[[NSDocumentController sharedDocumentController]
noteNewRecentFilePaths:filenames];
}
//
// b) Open any remaining files
//
[arguments setObject:[NSNumber numberWithInt:layout] forKey:@"layout"];
[arguments setObject:filenames forKey:@"filenames"];
// (Indicate that files should be opened from now on.)
[arguments setObject:[NSNumber numberWithBool:NO] forKey:@"dontOpen"];
MMVimController *vc;
if (openInCurrentWindow && (vc = [self topmostVimController])) {
// Open files in an already open window.
[[[vc windowController] window] makeKeyAndOrderFront:self];
[vc passArguments:arguments];
return YES;
}
BOOL openOk = YES;
int numFiles = [filenames count];
if (MMLayoutWindows == layout && numFiles > 1) {
// Open one file at a time in a new window, but don't open too many at
// once (at most cap+1 windows will open). If the user has increased
// the preload cache size we'll take that as a hint that more windows
// should be able to open at once.
int cap = [self maxPreloadCacheSize] - 1;
if (cap < 4) cap = 4;
if (cap > numFiles) cap = numFiles;
int i;
for (i = 0; i < cap; ++i) {
NSArray *a = [NSArray arrayWithObject:[filenames objectAtIndex:i]];
[arguments setObject:a forKey:@"filenames"];
// NOTE: We have to copy the args since we'll mutate them in the
// next loop and the below call may retain the arguments while
// waiting for a process to start.
NSDictionary *args = [[arguments copy] autorelease];
openOk = [self openVimControllerWithArguments:args];
if (!openOk) break;
}
// Open remaining files in tabs in a new window.
if (openOk && numFiles > cap) {
NSRange range = { i, numFiles-cap };