/
MMAppController.m
3041 lines (2592 loc) · 115 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 "MMVimView.h"
#import "MMWindowController.h"
#import "MMTextView.h"
#import "MMWhatsNewController.h"
#import "Miscellaneous.h"
#import <unistd.h>
#import <CoreServices/CoreServices.h>
// Need Carbon for TIS...() functions
#import <Carbon/Carbon.h>
#if !DISABLE_SPARKLE
#import "MMSparkle2Delegate.h"
#import "Sparkle.framework/Headers/Sparkle.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 = @"https://macvim-dev.github.io/macvim/";
// 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;
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)
- (void)startUpdaterAndWhatsNewPage;
- (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
+ (NSDictionary*)parseOpenURL:(NSURL*)url;
- (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;
- (void)inputSourceChanged:(NSNotification *)notification;
@end
static void
fsEventCallback(ConstFSEventStreamRef streamRef,
void *clientCallBackInfo,
size_t numEvents,
void *eventPaths,
const FSEventStreamEventFlags eventFlags[],
const FSEventStreamEventId eventIds[])
{
[[MMAppController sharedInstance] handleFSEvent];
}
@implementation MMAppController
/// Register the default settings for MacVim. Supports an optional
/// "-IgnoreUserDefaults 1" command-line argument, which will override
/// persisted user settings to have a clean environment.
+ (void)registerDefaults
{
int tabMinWidthKey;
int tabMaxWidthKey;
int tabOptimumWidthKey;
if (shouldUseYosemiteTabBarStyle()) {
tabMinWidthKey = 120;
tabMaxWidthKey = 0;
tabOptimumWidthKey = 0;
} else {
tabMinWidthKey = 64;
tabMaxWidthKey = 6*64;
tabOptimumWidthKey = 132;
}
NSUserDefaults *ud = NSUserDefaults.standardUserDefaults;
NSDictionary *macvimDefaults = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithBool:NO], MMNoWindowKey,
[NSNumber numberWithInt:tabMinWidthKey],
MMTabMinWidthKey,
[NSNumber numberWithInt:tabMaxWidthKey],
MMTabMaxWidthKey,
[NSNumber numberWithInt:tabOptimumWidthKey],
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], MMFontPreserveLineSpacingKey,
[NSNumber numberWithBool:YES], MMLoginShellKey,
[NSNumber numberWithInt:MMRendererCoreText],
MMRendererKey,
[NSNumber numberWithInt:MMUntitledWindowAlways],
MMUntitledWindowKey,
[NSNumber numberWithBool:NO], MMNoWindowShadowKey,
[NSNumber numberWithBool:NO], MMDisableLaunchAnimationKey,
[NSNumber numberWithInt:0], MMAppearanceModeSelectionKey,
[NSNumber numberWithBool:NO], MMNoTitleBarWindowKey,
[NSNumber numberWithBool:NO], MMTitlebarAppearsTransparentKey,
[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
[NSNumber numberWithBool:NO], MMSuppressTerminationAlertKey,
[NSNumber numberWithBool:YES], MMNativeFullScreenKey,
[NSNumber numberWithDouble:0.0], MMFullScreenFadeTimeKey,
[NSNumber numberWithBool:NO], MMNonNativeFullScreenShowMenuKey,
[NSNumber numberWithInt:0], MMNonNativeFullScreenSafeAreaBehaviorKey,
[NSNumber numberWithBool:YES], MMShareFindPboardKey,
[NSNumber numberWithBool:NO], MMSmoothResizeKey,
[NSNumber numberWithBool:NO], MMCmdLineAlignBottomKey,
[NSNumber numberWithBool:NO], MMRendererClipToRowKey,
[NSNumber numberWithBool:YES], MMAllowForceClickLookUpKey,
[NSNumber numberWithBool:NO], MMUpdaterPrereleaseChannelKey,
@"", MMLastUsedBundleVersionKey,
[NSNumber numberWithBool:YES], MMShowWhatsNewOnStartupKey,
[NSNumber numberWithBool:0], MMScrollOneDirectionOnlyKey,
nil];
[ud registerDefaults:macvimDefaults];
NSArray<NSString *> *arguments = NSProcessInfo.processInfo.arguments;
if ([arguments containsObject:@"-IgnoreUserDefaults"]) {
NSDictionary<NSString *, id> *argDefaults = [ud volatileDomainForName:NSArgumentDomain];
NSMutableDictionary<NSString *, id> *combinedDefaults = [NSMutableDictionary dictionaryWithCapacity: macvimDefaults.count];
[combinedDefaults setDictionary:macvimDefaults];
[combinedDefaults addEntriesFromDictionary:argDefaults];
[ud setVolatileDomain:combinedDefaults forName:NSArgumentDomain];
}
}
+ (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 (interpreargumenttKeyEvents: 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);
if ([NSWindow respondsToSelector:@selector(setAllowsAutomaticWindowTabbing:)]) {
// Disable automatic tabbing on 10.12+. MacVim already has its own
// tabbing interface, so we don't want multiple hierarchy of tabs mixing
// native and Vim tabs. MacVim also doesn't work well with native tabs
// right now since it doesn't respond well to the size change, and it
// doesn't show the native menu items (e.g. move tab to new window) in
// all the tabs.
//
// Note: MacVim cannot use macOS native tabs for Vim tabs because Vim
// assumes only one tab can be shown at a time, and it would be hard to
// handle native tab's "move tab to a new window" functionality.
[NSWindow setAllowsAutomaticWindowTabbing:NO];
}
[MMAppController registerDefaults];
NSArray *types = [NSArray arrayWithObject:NSPasteboardTypeString];
[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;
#if (MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_7)
// Disable automatic relaunching
if ([NSApp respondsToSelector:@selector(disableRelaunchOnLogin)])
[NSApp disableRelaunchOnLogin];
#endif
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];
NSProtocolChecker *rootObject = [NSProtocolChecker protocolCheckerWithTarget:self
protocol:@protocol(MMAppProtocol)];
[connection setRootObject:rootObject];
[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;
NSAlert *alert = [[NSAlert alloc] init];
[alert addButtonWithTitle:NSLocalizedString(@"OK",
@"Dialog button")];
[alert setMessageText:NSLocalizedString(@"MacVim cannot be opened",
@"MacVim cannot be opened, title")];
[alert setInformativeText:[NSString stringWithFormat:NSLocalizedString(
@"MacVim could not set up its connection. It's likely you already have MacVim opened elsewhere.",
@"MacVim already opened, text")]];
[alert setAlertStyle:NSAlertStyleCritical];
[alert runModal];
[alert release];
[[NSApplication sharedApplication] terminate:nil];
}
// Register help search handler to support search Vim docs via the Help menu
[NSApp registerUserInterfaceItemSearchHandler:self];
#if !DISABLE_SPARKLE
// Sparkle is enabled (this is the default). Initialize it. It will
// automatically check for update.
#if USE_SPARKLE_1
updater = [[SUUpdater alloc] init];
#else
sparkle2delegate = [[MMSparkle2Delegate alloc] init];
// We don't immediately start the updater, because if it sees an update
// and immediately shows the dialog box it will pop up behind a new MacVim
// window. Instead, startUpdaterAndWhatsNewPage will be called later to do so.
updater = [[SPUStandardUpdaterController alloc] initWithStartingUpdater:NO updaterDelegate:sparkle2delegate userDriverDelegate:sparkle2delegate];
#endif
#endif
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;
currentMainMenu = nil;
[appMenuItemTemplate release]; appMenuItemTemplate = nil;
#if !DISABLE_SPARKLE
[updater release]; updater = nil;
#if !USE_SPARKLE_1
[sparkle2delegate release]; sparkle2delegate = nil;
#endif
#endif
[super dealloc];
}
- (void)applicationWillFinishLaunching:(NSNotification *)notification
{
// This prevents macOS from injecting "Enter Full Screen" menu item.
// MacVim already has a separate menu item to do that.
// See https://developer.apple.com/library/archive/releasenotes/AppKit/RN-AppKitOlderNotes/index.html#10_11FullScreen
[[NSUserDefaults standardUserDefaults] setBool:NO forKey:@"NSFullScreenMenuItemEverywhere"];
// 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 itemWithTag:15432];
[[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];
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
NSString *lastUsedVersion = [ud stringForKey:MMLastUsedBundleVersionKey];
NSString *currentVersion = [[NSBundle mainBundle] objectForInfoDictionaryKey:
@"CFBundleVersion"];
// This will be used for showing a "What's New" dialog box in the future. For
// now, just update the stored version for future use so later versions will
// be able to tell whether to show this dialog box or not.
if (currentVersion && currentVersion.length != 0) {
if (!lastUsedVersion || [lastUsedVersion length] == 0) {
[ud setValue:currentVersion forKey:MMLastUsedBundleVersionKey];
} else {
// If the current version is larger, set that to be stored. Don't
// want to do it otherwise to prevent testing older versions flipping
// the stored version back to an old one.
const BOOL currentVersionLarger = (compareSemanticVersions(lastUsedVersion, currentVersion) == 1);
if (currentVersionLarger) {
[ud setValue:currentVersion forKey:MMLastUsedBundleVersionKey];
// We have successfully updated to a new version. Show a
// "What's New" page to the user with latest release notes
// unless they configured not to.
BOOL showWhatsNewSetting = [ud boolForKey:MMShowWhatsNewOnStartupKey];
shouldShowWhatsNewPage = showWhatsNewSetting;
[MMWhatsNewController setRequestVersionRange:lastUsedVersion
to:currentVersion];
}
}
}
// Start the Sparkle updater and potentially show "What's New". If the user
// doesn't want a new untitled MacVim window shown, we immediately do so.
// Otherwise we want to do it *after* the untitled window is opened so the
// updater / "What's New" page can be shown on top of it. We still schedule
// a timer to open it as a backup in case something wrong happened with the
// Vim window (e.g. a crash in Vim) but we still want the updater to work since
// that update may very well be the fix for the crash.
const NSInteger untitledWindowFlag = [ud integerForKey:MMUntitledWindowKey];
if ((untitledWindowFlag & MMUntitledWindowOnOpen) == 0) {
[self startUpdaterAndWhatsNewPage];
} else {
// Per above, this is just a backup. startUpdaterAndWhatsNewPage will
// not do anything if it's called a second time.
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(startUpdaterAndWhatsNewPage) userInfo:nil repeats:NO];
}
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.
if ([vimControllers count] > 0)
return NO;
// Don't open an untitled window if there are processes about to launch...
NSUInteger numLaunching = [pidArguments count];
if (numLaunching > 0) {
// ...unless the launching process is being preloaded
NSNumber *key = [NSNumber numberWithInt:preloadPid];
if (numLaunching != 1 || [pidArguments objectForKey:key] == nil)
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
{
if (!hasShownWindowBefore) {
// If we have not opened a window before, never return YES. This can
// happen when MacVim is not configured to open window at launch. We
// want to give the user a chance to open a window first. Otherwise
// just opening the About MacVim or Settings windows could immediately
// terminate the app (since those are not proper app windows),
// depending if the OS feels like invoking this method.
return NO;
}
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:NSAlertStyleWarning];
[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 if (![[NSUserDefaults standardUserDefaults]
boolForKey:MMSuppressTerminationAlertKey]) {
// 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:NSAlertStyleWarning];
[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")];
[alert setShowsSuppressionButton:YES];
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;
if ([[alert suppressionButton] state] == NSControlStateValueOn) {
[[NSUserDefaults standardUserDefaults]
setBool:YES forKey:MMSuppressTerminationAlertKey];
}
[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
// We are hard shutting down the app here by terminating all Vim processes
// and then just quit without cleanly removing each Vim controller. We
// don't want the straggler controllers to still interact with the now
// invalid connections, so we just mark them as uninitialized.
for (NSUInteger i = 0, count = [vimControllers count]; i < count; ++i) {
MMVimController *vc = [vimControllers objectAtIndex:i];
[vc uninitialize];
}
// 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=%lu (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];
BOOL willSwitchScreens = 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");
}
// setFrameTopLeftPoint will trigger a resize event if the window is
// moved across monitors; at this point such a resize would incorrectly
// constrain the window to the default vim dimensions, so a specialized
// method is used that will avoid that behavior.
if (willSwitchScreens)
[windowController moveWindowAcrossScreens:topLeft];
else
[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;