mirror of
https://github.com/macvim-dev/macvim.git
synced 2026-06-11 15:37:29 +02:00
3def55116e
Fix #173
2482 lines
92 KiB
Objective-C
2482 lines
92 KiB
Objective-C
/* 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>
|
|
// Need Carbon for TIS...() functions
|
|
#import <Carbon/Carbon.h>
|
|
|
|
|
|
#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)
|
|
- (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;
|
|
- (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
|
|
|
|
+ (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:MMRendererCoreText],
|
|
MMRendererKey,
|
|
[NSNumber numberWithInt:MMUntitledWindowAlways],
|
|
MMUntitledWindowKey,
|
|
[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,
|
|
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;
|
|
|
|
#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];
|
|
[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.
|
|
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
|
|
{
|
|
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 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: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")];
|
|
[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] == NSOnState) {
|
|
[[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
|
|
|
|
// 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];
|
|
|
|
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;
|
|
}
|
|
|
|
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
|
|
//
|
|
// 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];
|
|
NSDictionary *openFilesDict = nil;
|
|
filenames = [self filterOpenFiles:filenames openFilesDict:&openFilesDict];
|
|
|
|
// 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;
|
|
|
|
// 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])) {
|
|
MMVimController *vc = [key pointerValue];
|
|
NSArray *files = [openFilesDict objectForKey:key];
|
|
[arguments setObject:files forKey:@"filenames"];
|
|
|
|
if ([filenames count] == 0 && [files containsObject:firstFile]) {
|
|
// 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.
|
|
//
|
|
// NOTE: Raise window before passing arguments, otherwise the
|
|
// selection will be lost when selectionRange is set.
|
|
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];
|
|
|
|
[vc addVimInput:input];
|
|
}
|
|
|
|
[vc passArguments:arguments];
|
|
}
|
|
|
|
// Add filenames to "Recent Files" menu, unless they are being edited
|
|
// remotely (using ODB).
|
|
if ([arguments objectForKey:@"remoteID"] == nil) {
|
|
[[NSDocumentController sharedDocumentController]
|
|
noteNewRecentFilePaths:filenames];
|
|
}
|
|
|
|
if ([filenames count] == 0)
|
|
return YES; // No files left to open (all were already open)
|
|
|
|
//
|
|
// 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 };
|
|
NSArray *a = [filenames subarrayWithRange:range];
|
|
[arguments setObject:a forKey:@"filenames"];
|
|
[arguments setObject:[NSNumber numberWithInt:MMLayoutTabs]
|
|
forKey:@"layout"];
|
|
|
|
openOk = [self openVimControllerWithArguments:arguments];
|
|
}
|
|
} else {
|
|
// Open all files at once.
|
|
openOk = [self openVimControllerWithArguments:arguments];
|
|
}
|
|
|
|
return openOk;
|
|
}
|
|
|
|
- (IBAction)newWindow:(id)sender
|
|
{
|
|
ASLogDebug(@"Open new window");
|
|
|
|
// A cached controller requires no loading times and results in the new
|
|
// window popping up instantaneously. If the cache is empty it may take
|
|
// 1-2 seconds to start a new Vim process.
|
|
MMVimController *vc = [self takeVimControllerFromCache];
|
|
if (vc) {
|
|
[[vc backendProxy] acknowledgeConnection];
|
|
} else {
|
|
[self launchVimProcessWithArguments:nil workingDirectory:nil];
|
|
}
|
|
}
|
|
|
|
- (IBAction)newWindowAndActivate:(id)sender
|
|
{
|
|
[self activateWhenNextWindowOpens];
|
|
[self newWindow:sender];
|
|
}
|
|
|
|
- (IBAction)fileOpen:(id)sender
|
|
{
|
|
ASLogDebug(@"Show file open panel");
|
|
|
|
NSString *dir = nil;
|
|
BOOL trackPwd = [[NSUserDefaults standardUserDefaults]
|
|
boolForKey:MMDialogsTrackPwdKey];
|
|
if (trackPwd) {
|
|
MMVimController *vc = [self keyVimController];
|
|
if (vc) dir = [vc objectForVimStateKey:@"pwd"];
|
|
}
|
|
|
|
NSOpenPanel *panel = [NSOpenPanel openPanel];
|
|
[panel setAllowsMultipleSelection:YES];
|
|
[panel setCanChooseDirectories:YES];
|
|
[panel setAccessoryView:showHiddenFilesView()];
|
|
dir = [dir stringByExpandingTildeInPath];
|
|
if (dir) {
|
|
NSURL *dirURL = [NSURL fileURLWithPath:dir isDirectory:YES];
|
|
if (dirURL)
|
|
[panel setDirectoryURL:dirURL];
|
|
}
|
|
|
|
NSInteger result = [panel runModal];
|
|
|
|
#if (MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_10)
|
|
if (NSModalResponseOK == result) {
|
|
#else
|
|
if (NSOKButton == result) {
|
|
#endif
|
|
// NOTE: -[NSOpenPanel filenames] is deprecated on 10.7 so use
|
|
// -[NSOpenPanel URLs] instead. The downside is that we have to check
|
|
// that each URL is really a path first.
|
|
NSMutableArray *filenames = [NSMutableArray array];
|
|
NSArray *urls = [panel URLs];
|
|
NSUInteger i, count = [urls count];
|
|
for (i = 0; i < count; ++i) {
|
|
NSURL *url = [urls objectAtIndex:i];
|
|
if ([url isFileURL]) {
|
|
NSString *path = [url path];
|
|
if (path)
|
|
[filenames addObject:path];
|
|
}
|
|
}
|
|
|
|
if ([filenames count] > 0)
|
|
[self application:NSApp openFiles:filenames];
|
|
}
|
|
}
|
|
|
|
- (IBAction)selectNextWindow:(id)sender
|
|
{
|
|
ASLogDebug(@"Select next window");
|
|
|
|
unsigned i, count = [vimControllers count];
|
|
if (!count) return;
|
|
|
|
NSWindow *keyWindow = [NSApp keyWindow];
|
|
for (i = 0; i < count; ++i) {
|
|
MMVimController *vc = [vimControllers objectAtIndex:i];
|
|
if ([[[vc windowController] window] isEqual:keyWindow])
|
|
break;
|
|
}
|
|
|
|
if (i < count) {
|
|
if (++i >= count)
|
|
i = 0;
|
|
MMVimController *vc = [vimControllers objectAtIndex:i];
|
|
[[vc windowController] showWindow:self];
|
|
}
|
|
}
|
|
|
|
- (IBAction)selectPreviousWindow:(id)sender
|
|
{
|
|
ASLogDebug(@"Select previous window");
|
|
|
|
unsigned i, count = [vimControllers count];
|
|
if (!count) return;
|
|
|
|
NSWindow *keyWindow = [NSApp keyWindow];
|
|
for (i = 0; i < count; ++i) {
|
|
MMVimController *vc = [vimControllers objectAtIndex:i];
|
|
if ([[[vc windowController] window] isEqual:keyWindow])
|
|
break;
|
|
}
|
|
|
|
if (i < count) {
|
|
if (i > 0) {
|
|
--i;
|
|
} else {
|
|
i = count - 1;
|
|
}
|
|
MMVimController *vc = [vimControllers objectAtIndex:i];
|
|
[[vc windowController] showWindow:self];
|
|
}
|
|
}
|
|
|
|
- (IBAction)orderFrontPreferencePanel:(id)sender
|
|
{
|
|
ASLogDebug(@"Show preferences panel");
|
|
[[MMPreferenceController sharedPrefsWindowController] showWindow:self];
|
|
}
|
|
|
|
- (IBAction)openWebsite:(id)sender
|
|
{
|
|
ASLogDebug(@"Open MacVim website");
|
|
[[NSWorkspace sharedWorkspace] openURL:
|
|
[NSURL URLWithString:MMWebsiteString]];
|
|
}
|
|
|
|
- (IBAction)showVimHelp:(id)sender
|
|
{
|
|
ASLogDebug(@"Open window with Vim help");
|
|
// Open a new window with the help window maximized.
|
|
[self launchVimProcessWithArguments:[NSArray arrayWithObjects:
|
|
@"-c", @":h gui_mac", @"-c", @":res", nil]
|
|
workingDirectory:nil];
|
|
}
|
|
|
|
- (IBAction)zoomAll:(id)sender
|
|
{
|
|
ASLogDebug(@"Zoom all windows");
|
|
[NSApp makeWindowsPerform:@selector(performZoom:) inOrder:YES];
|
|
}
|
|
|
|
- (IBAction)coreTextButtonClicked:(id)sender
|
|
{
|
|
ASLogDebug(@"Toggle CoreText renderer");
|
|
NSInteger renderer = MMRendererDefault;
|
|
BOOL enable = ([sender state] == NSOnState);
|
|
|
|
if (enable) {
|
|
renderer = MMRendererCoreText;
|
|
}
|
|
|
|
// Update the user default MMRenderer and synchronize the change so that
|
|
// any new Vim process will pick up on the changed setting.
|
|
CFPreferencesSetAppValue(
|
|
(CFStringRef)MMRendererKey,
|
|
(CFPropertyListRef)[NSNumber numberWithInt:renderer],
|
|
kCFPreferencesCurrentApplication);
|
|
CFPreferencesAppSynchronize(kCFPreferencesCurrentApplication);
|
|
|
|
ASLogInfo(@"Use renderer=%ld", renderer);
|
|
|
|
// This action is called when the user clicks the "use CoreText renderer"
|
|
// button in the advanced preferences pane.
|
|
[self rebuildPreloadCache];
|
|
}
|
|
|
|
- (IBAction)loginShellButtonClicked:(id)sender
|
|
{
|
|
ASLogDebug(@"Toggle login shell option");
|
|
// This action is called when the user clicks the "use login shell" button
|
|
// in the advanced preferences pane.
|
|
[self rebuildPreloadCache];
|
|
}
|
|
|
|
- (IBAction)quickstartButtonClicked:(id)sender
|
|
{
|
|
ASLogDebug(@"Toggle Quickstart option");
|
|
if ([self maxPreloadCacheSize] > 0) {
|
|
[self scheduleVimControllerPreloadAfterDelay:1.0];
|
|
[self startWatchingVimDir];
|
|
} else {
|
|
[self cancelVimControllerPreloadRequests];
|
|
[self clearPreloadCacheWithCount:-1];
|
|
[self stopWatchingVimDir];
|
|
}
|
|
}
|
|
|
|
- (MMVimController *)keyVimController
|
|
{
|
|
NSWindow *keyWindow = [NSApp keyWindow];
|
|
if (keyWindow) {
|
|
unsigned i, count = [vimControllers count];
|
|
for (i = 0; i < count; ++i) {
|
|
MMVimController *vc = [vimControllers objectAtIndex:i];
|
|
if ([[[vc windowController] window] isEqual:keyWindow])
|
|
return vc;
|
|
}
|
|
}
|
|
|
|
return nil;
|
|
}
|
|
|
|
- (unsigned)connectBackend:(byref in id <MMBackendProtocol>)proxy pid:(int)pid
|
|
{
|
|
ASLogDebug(@"pid=%d", pid);
|
|
|
|
[(NSDistantObject*)proxy setProtocolForProxy:@protocol(MMBackendProtocol)];
|
|
|
|
// NOTE: Allocate the vim controller now but don't add it to the list of
|
|
// controllers since this is a distributed object call and as such can
|
|
// arrive at unpredictable times (e.g. while iterating the list of vim
|
|
// controllers).
|
|
// (What if input arrives before the vim controller is added to the list of
|
|
// controllers? This should not be a problem since the input isn't
|
|
// processed immediately (see processInput:forIdentifier:).)
|
|
// Also, since the app may be multithreaded (e.g. as a result of showing
|
|
// the open panel) we have to ensure this call happens on the main thread,
|
|
// else there is a race condition that may lead to a crash.
|
|
MMVimController *vc = [[MMVimController alloc] initWithBackend:proxy
|
|
pid:pid];
|
|
[self performSelectorOnMainThread:@selector(addVimController:)
|
|
withObject:vc
|
|
waitUntilDone:NO
|
|
modes:[NSArray arrayWithObject:
|
|
NSDefaultRunLoopMode]];
|
|
|
|
[vc release];
|
|
|
|
return [vc vimControllerId];
|
|
}
|
|
|
|
- (oneway void)processInput:(in bycopy NSArray *)queue
|
|
forIdentifier:(unsigned)identifier
|
|
{
|
|
// NOTE: Input is not handled immediately since this is a distributed
|
|
// object call and as such can arrive at unpredictable times. Instead,
|
|
// queue the input and process it when the run loop is updated.
|
|
|
|
if (!(queue && identifier)) {
|
|
ASLogWarn(@"Bad input for identifier=%d", identifier);
|
|
return;
|
|
}
|
|
|
|
ASLogDebug(@"QUEUE for identifier=%d: <<< %@>>>", identifier,
|
|
debugStringForMessageQueue(queue));
|
|
|
|
NSNumber *key = [NSNumber numberWithUnsignedInt:identifier];
|
|
NSArray *q = [inputQueues objectForKey:key];
|
|
if (q) {
|
|
q = [q arrayByAddingObjectsFromArray:queue];
|
|
[inputQueues setObject:q forKey:key];
|
|
} else {
|
|
[inputQueues setObject:queue forKey:key];
|
|
}
|
|
|
|
// NOTE: We must use "event tracking mode" as well as "default mode",
|
|
// otherwise the input queue will not be processed e.g. during live
|
|
// resizing.
|
|
// Also, since the app may be multithreaded (e.g. as a result of showing
|
|
// the open panel) we have to ensure this call happens on the main thread,
|
|
// else there is a race condition that may lead to a crash.
|
|
[self performSelectorOnMainThread:@selector(processInputQueues:)
|
|
withObject:nil
|
|
waitUntilDone:NO
|
|
modes:[NSArray arrayWithObjects:
|
|
NSDefaultRunLoopMode,
|
|
NSEventTrackingRunLoopMode, nil]];
|
|
}
|
|
|
|
- (NSArray *)serverList
|
|
{
|
|
NSMutableArray *array = [NSMutableArray array];
|
|
|
|
unsigned i, count = [vimControllers count];
|
|
for (i = 0; i < count; ++i) {
|
|
MMVimController *controller = [vimControllers objectAtIndex:i];
|
|
if ([controller serverName])
|
|
[array addObject:[controller serverName]];
|
|
}
|
|
|
|
return array;
|
|
}
|
|
|
|
@end // MMAppController
|
|
|
|
|
|
|
|
|
|
@implementation MMAppController (MMServices)
|
|
|
|
- (void)openSelection:(NSPasteboard *)pboard userData:(NSString *)userData
|
|
error:(NSString **)error
|
|
{
|
|
if (![[pboard types] containsObject:NSStringPboardType]) {
|
|
ASLogNotice(@"Pasteboard contains no NSStringPboardType");
|
|
return;
|
|
}
|
|
|
|
ASLogInfo(@"Open new window containing current selection");
|
|
|
|
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
|
|
BOOL openInCurrentWindow = [ud boolForKey:MMOpenInCurrentWindowKey];
|
|
MMVimController *vc;
|
|
|
|
if (openInCurrentWindow && (vc = [self topmostVimController])) {
|
|
[vc sendMessage:AddNewTabMsgID data:nil];
|
|
[vc dropString:[pboard stringForType:NSStringPboardType]];
|
|
} else {
|
|
// Save the text, open a new window, and paste the text when the next
|
|
// window opens. (If this is called several times in a row, then all
|
|
// but the last call may be ignored.)
|
|
if (openSelectionString) [openSelectionString release];
|
|
openSelectionString = [[pboard stringForType:NSStringPboardType] copy];
|
|
|
|
[self newWindow:self];
|
|
}
|
|
}
|
|
|
|
- (void)openFile:(NSPasteboard *)pboard userData:(NSString *)userData
|
|
error:(NSString **)error
|
|
{
|
|
if (![[pboard types] containsObject:NSStringPboardType]) {
|
|
ASLogNotice(@"Pasteboard contains no NSStringPboardType");
|
|
return;
|
|
}
|
|
|
|
// TODO: Parse multiple filenames and create array with names.
|
|
NSString *string = [pboard stringForType:NSStringPboardType];
|
|
string = [string stringByTrimmingCharactersInSet:
|
|
[NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
|
string = [string stringByStandardizingPath];
|
|
|
|
ASLogInfo(@"Open new window with selected file: %@", string);
|
|
|
|
NSArray *filenames = [self filterFilesAndNotify:
|
|
[NSArray arrayWithObject:string]];
|
|
if ([filenames count] == 0)
|
|
return;
|
|
|
|
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
|
|
BOOL openInCurrentWindow = [ud boolForKey:MMOpenInCurrentWindowKey];
|
|
MMVimController *vc;
|
|
|
|
if (openInCurrentWindow && (vc = [self topmostVimController])) {
|
|
[vc dropFiles:filenames forceOpen:YES];
|
|
} else {
|
|
[self openFiles:filenames withArguments:nil];
|
|
}
|
|
}
|
|
|
|
- (void)newFileHere:(NSPasteboard *)pboard userData:(NSString *)userData
|
|
error:(NSString **)error
|
|
{
|
|
if (![[pboard types] containsObject:NSFilenamesPboardType]) {
|
|
ASLogNotice(@"Pasteboard contains no NSFilenamesPboardType");
|
|
return;
|
|
}
|
|
|
|
NSArray *filenames = [pboard propertyListForType:NSFilenamesPboardType];
|
|
NSString *path = [filenames lastObject];
|
|
|
|
BOOL dirIndicator;
|
|
if (![[NSFileManager defaultManager] fileExistsAtPath:path
|
|
isDirectory:&dirIndicator]) {
|
|
ASLogNotice(@"Invalid path. Cannot open new document at: %@", path);
|
|
return;
|
|
}
|
|
|
|
ASLogInfo(@"Open new file at path=%@", path);
|
|
|
|
if (!dirIndicator)
|
|
path = [path stringByDeletingLastPathComponent];
|
|
|
|
path = [path stringByEscapingSpecialFilenameCharacters];
|
|
|
|
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
|
|
BOOL openInCurrentWindow = [ud boolForKey:MMOpenInCurrentWindowKey];
|
|
MMVimController *vc;
|
|
|
|
if (openInCurrentWindow && (vc = [self topmostVimController])) {
|
|
NSString *input = [NSString stringWithFormat:@"<C-\\><C-N>"
|
|
":tabe|cd %@<CR>", path];
|
|
[vc addVimInput:input];
|
|
} else {
|
|
[self launchVimProcessWithArguments:nil workingDirectory:path];
|
|
}
|
|
}
|
|
|
|
@end // MMAppController (MMServices)
|
|
|
|
|
|
|
|
|
|
@implementation MMAppController (Private)
|
|
|
|
- (MMVimController *)topmostVimController
|
|
{
|
|
// Find the topmost visible window which has an associated vim controller
|
|
// as follows:
|
|
//
|
|
// 1. Search through ordered windows as determined by NSApp. Unfortunately
|
|
// this method can fail, e.g. if a full-screen window is on another
|
|
// "Space" (in this case NSApp returns no windows at all), so we have to
|
|
// fall back on ...
|
|
// 2. Search through all Vim controllers and return the first visible
|
|
// window.
|
|
|
|
NSEnumerator *e = [[NSApp orderedWindows] objectEnumerator];
|
|
id window;
|
|
while ((window = [e nextObject]) && [window isVisible]) {
|
|
unsigned i, count = [vimControllers count];
|
|
for (i = 0; i < count; ++i) {
|
|
MMVimController *vc = [vimControllers objectAtIndex:i];
|
|
if ([[[vc windowController] window] isEqual:window])
|
|
return vc;
|
|
}
|
|
}
|
|
|
|
unsigned i, count = [vimControllers count];
|
|
for (i = 0; i < count; ++i) {
|
|
MMVimController *vc = [vimControllers objectAtIndex:i];
|
|
if ([[[vc windowController] window] isVisible]) {
|
|
return vc;
|
|
}
|
|
}
|
|
|
|
return nil;
|
|
}
|
|
|
|
- (int)launchVimProcessWithArguments:(NSArray *)args
|
|
workingDirectory:(NSString *)cwd
|
|
{
|
|
int pid = -1;
|
|
NSString *path = [[NSBundle mainBundle] pathForAuxiliaryExecutable:@"Vim"];
|
|
|
|
if (!path) {
|
|
ASLogCrit(@"Vim executable could not be found inside app bundle!");
|
|
return -1;
|
|
}
|
|
|
|
// Change current working directory so that the child process picks it up.
|
|
NSFileManager *fm = [NSFileManager defaultManager];
|
|
NSString *restoreCwd = nil;
|
|
if (cwd) {
|
|
restoreCwd = [fm currentDirectoryPath];
|
|
[fm changeCurrentDirectoryPath:cwd];
|
|
}
|
|
|
|
NSArray *taskArgs = [NSArray arrayWithObjects:@"-g", @"-f", nil];
|
|
if (args)
|
|
taskArgs = [taskArgs arrayByAddingObjectsFromArray:args];
|
|
|
|
BOOL useLoginShell = [[NSUserDefaults standardUserDefaults]
|
|
boolForKey:MMLoginShellKey];
|
|
if (useLoginShell) {
|
|
// Run process with a login shell, roughly:
|
|
// echo "exec Vim -g -f args" | ARGV0=-`basename $SHELL` $SHELL [-l]
|
|
pid = [self executeInLoginShell:path arguments:taskArgs];
|
|
} else {
|
|
// Run process directly:
|
|
// Vim -g -f args
|
|
NSTask *task = [NSTask launchedTaskWithLaunchPath:path
|
|
arguments:taskArgs];
|
|
pid = task ? [task processIdentifier] : -1;
|
|
}
|
|
|
|
if (-1 != pid) {
|
|
// The 'pidArguments' dictionary keeps arguments to be passed to the
|
|
// process when it connects (this is in contrast to arguments which are
|
|
// passed on the command line, like '-f' and '-g').
|
|
// NOTE: If there are no arguments to pass we still add a null object
|
|
// so that we can use this dictionary to check if there are any
|
|
// processes loading.
|
|
NSNumber *pidKey = [NSNumber numberWithInt:pid];
|
|
if (![pidArguments objectForKey:pidKey])
|
|
[pidArguments setObject:[NSNull null] forKey:pidKey];
|
|
} else {
|
|
ASLogWarn(@"Failed to launch Vim process: args=%@, useLoginShell=%d",
|
|
args, useLoginShell);
|
|
}
|
|
|
|
// Now that child has launched, restore the current working directory.
|
|
if (restoreCwd)
|
|
[fm changeCurrentDirectoryPath:restoreCwd];
|
|
|
|
return pid;
|
|
}
|
|
|
|
- (NSArray *)filterFilesAndNotify:(NSArray *)filenames
|
|
{
|
|
// Go trough 'filenames' array and make sure each file exists. Present
|
|
// warning dialog if some file was missing.
|
|
|
|
NSString *firstMissingFile = nil;
|
|
NSMutableArray *files = [NSMutableArray array];
|
|
unsigned i, count = [filenames count];
|
|
|
|
for (i = 0; i < count; ++i) {
|
|
NSString *name = [filenames objectAtIndex:i];
|
|
if ([[NSFileManager defaultManager] fileExistsAtPath:name]) {
|
|
[files addObject:name];
|
|
} else if (!firstMissingFile) {
|
|
firstMissingFile = name;
|
|
}
|
|
}
|
|
|
|
if (firstMissingFile) {
|
|
NSAlert *alert = [[NSAlert alloc] init];
|
|
[alert addButtonWithTitle:NSLocalizedString(@"OK",
|
|
@"Dialog button")];
|
|
|
|
NSString *text;
|
|
if ([files count] >= count-1) {
|
|
[alert setMessageText:NSLocalizedString(@"File not found",
|
|
@"File not found dialog, title")];
|
|
text = [NSString stringWithFormat:NSLocalizedString(
|
|
@"Could not open file with name %@.",
|
|
@"File not found dialog, text"), firstMissingFile];
|
|
} else {
|
|
[alert setMessageText:NSLocalizedString(@"Multiple files not found",
|
|
@"File not found dialog, title")];
|
|
text = [NSString stringWithFormat:NSLocalizedString(
|
|
@"Could not open file with name %@, and %d other files.",
|
|
@"File not found dialog, text"),
|
|
firstMissingFile, count-[files count]-1];
|
|
}
|
|
|
|
[alert setInformativeText:text];
|
|
[alert setAlertStyle:NSWarningAlertStyle];
|
|
|
|
[alert runModal];
|
|
[alert release];
|
|
|
|
[NSApp replyToOpenOrPrint:NSApplicationDelegateReplyFailure];
|
|
}
|
|
|
|
return files;
|
|
}
|
|
|
|
- (NSArray *)filterOpenFiles:(NSArray *)filenames
|
|
openFilesDict:(NSDictionary **)openFiles
|
|
{
|
|
// Filter out any files in the 'filenames' array that are open and return
|
|
// all files that are not already open. On return, the 'openFiles'
|
|
// parameter (if non-nil) will point to a dictionary of open files, indexed
|
|
// by Vim controller.
|
|
|
|
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
|
|
NSMutableArray *files = [filenames mutableCopy];
|
|
|
|
// TODO: Escape special characters in 'files'?
|
|
NSString *expr = [NSString stringWithFormat:
|
|
@"map([\"%@\"],\"bufloaded(v:val)\")",
|
|
[files componentsJoinedByString:@"\",\""]];
|
|
|
|
unsigned i, count = [vimControllers count];
|
|
for (i = 0; i < count && [files count] > 0; ++i) {
|
|
MMVimController *vc = [vimControllers objectAtIndex:i];
|
|
|
|
// Query Vim for which files in the 'files' array are open.
|
|
NSString *eval = [vc evaluateVimExpression:expr];
|
|
if (!eval) continue;
|
|
|
|
NSIndexSet *idxSet = [NSIndexSet indexSetWithVimList:eval];
|
|
if ([idxSet count] > 0) {
|
|
[dict setObject:[files objectsAtIndexes:idxSet]
|
|
forKey:[NSValue valueWithPointer:vc]];
|
|
|
|
// Remove all the files that were open in this Vim process and
|
|
// create a new expression to evaluate.
|
|
[files removeObjectsAtIndexes:idxSet];
|
|
expr = [NSString stringWithFormat:
|
|
@"map([\"%@\"],\"bufloaded(v:val)\")",
|
|
[files componentsJoinedByString:@"\",\""]];
|
|
}
|
|
}
|
|
|
|
if (openFiles != nil)
|
|
*openFiles = dict;
|
|
|
|
return [files autorelease];
|
|
}
|
|
|
|
#if MM_HANDLE_XCODE_MOD_EVENT
|
|
- (void)handleXcodeModEvent:(NSAppleEventDescriptor *)event
|
|
replyEvent:(NSAppleEventDescriptor *)reply
|
|
{
|
|
#if 0
|
|
// Xcode sends this event to query MacVim which open files have been
|
|
// modified.
|
|
ASLogDebug(@"reply:%@", reply);
|
|
ASLogDebug(@"event:%@", event);
|
|
|
|
NSEnumerator *e = [vimControllers objectEnumerator];
|
|
id vc;
|
|
while ((vc = [e nextObject])) {
|
|
DescType type = [reply descriptorType];
|
|
unsigned len = [[type data] length];
|
|
NSMutableData *data = [NSMutableData data];
|
|
|
|
[data appendBytes:&type length:sizeof(DescType)];
|
|
[data appendBytes:&len length:sizeof(unsigned)];
|
|
[data appendBytes:[reply data] length:len];
|
|
|
|
[vc sendMessage:XcodeModMsgID data:data];
|
|
}
|
|
#endif
|
|
}
|
|
#endif
|
|
|
|
- (void)handleGetURLEvent:(NSAppleEventDescriptor *)event
|
|
replyEvent:(NSAppleEventDescriptor *)reply
|
|
{
|
|
NSURL *url = [NSURL URLWithString:[[event
|
|
paramDescriptorForKeyword:keyDirectObject]
|
|
stringValue]];
|
|
|
|
// We try to be compatible with TextMate's URL scheme here, as documented
|
|
// at http://blog.macromates.com/2007/the-textmate-url-scheme/ . Currently,
|
|
// this means that:
|
|
//
|
|
// The format is: mvim://open?<arguments> where arguments can be:
|
|
//
|
|
// * url — the actual file to open (i.e. a file://… URL), if you leave
|
|
// out this argument, the frontmost document is implied.
|
|
// * line — line number to go to (one based).
|
|
// * column — column number to go to (one based).
|
|
//
|
|
// Example: mvim://open?url=file:///etc/profile&line=20
|
|
|
|
if ([[url host] isEqualToString:@"open"]) {
|
|
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
|
|
|
|
// Parse query ("url=file://...&line=14") into a dictionary
|
|
NSArray *queries = [[url query] componentsSeparatedByString:@"&"];
|
|
NSEnumerator *enumerator = [queries objectEnumerator];
|
|
NSString *param;
|
|
while ((param = [enumerator nextObject])) {
|
|
NSArray *arr = [param componentsSeparatedByString:@"="];
|
|
if ([arr count] == 2) {
|
|
#if MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_11
|
|
[dict setValue:[[arr lastObject] stringByRemovingPercentEncoding]
|
|
forKey:[[arr objectAtIndex:0] stringByRemovingPercentEncoding]];
|
|
#else
|
|
[dict setValue:[[arr lastObject]
|
|
stringByReplacingPercentEscapesUsingEncoding:
|
|
NSUTF8StringEncoding]
|
|
forKey:[[arr objectAtIndex:0]
|
|
stringByReplacingPercentEscapesUsingEncoding:
|
|
NSUTF8StringEncoding]];
|
|
#endif
|
|
}
|
|
}
|
|
|
|
// Actually open the file.
|
|
NSString *file = [dict objectForKey:@"url"];
|
|
if (file != nil) {
|
|
NSURL *fileUrl= [NSURL URLWithString:file];
|
|
// TextMate only opens files that already exist.
|
|
if ([fileUrl isFileURL]
|
|
&& [[NSFileManager defaultManager] fileExistsAtPath:
|
|
[fileUrl path]]) {
|
|
// Strip 'file://' path, else application:openFiles: might think
|
|
// the file is not yet open.
|
|
NSArray *filenames = [NSArray arrayWithObject:[fileUrl path]];
|
|
|
|
// Look for the line and column options.
|
|
NSDictionary *args = nil;
|
|
NSString *line = [dict objectForKey:@"line"];
|
|
if (line) {
|
|
NSString *column = [dict objectForKey:@"column"];
|
|
if (column)
|
|
args = [NSDictionary dictionaryWithObjectsAndKeys:
|
|
line, @"cursorLine",
|
|
column, @"cursorColumn",
|
|
nil];
|
|
else
|
|
args = [NSDictionary dictionaryWithObject:line
|
|
forKey:@"cursorLine"];
|
|
}
|
|
|
|
[self openFiles:filenames withArguments:args];
|
|
}
|
|
}
|
|
} else {
|
|
NSAlert *alert = [[NSAlert alloc] init];
|
|
[alert addButtonWithTitle:NSLocalizedString(@"OK",
|
|
@"Dialog button")];
|
|
|
|
[alert setMessageText:NSLocalizedString(@"Unknown URL Scheme",
|
|
@"Unknown URL Scheme dialog, title")];
|
|
[alert setInformativeText:[NSString stringWithFormat:NSLocalizedString(
|
|
@"This version of MacVim does not support \"%@\""
|
|
@" in its URL scheme.",
|
|
@"Unknown URL Scheme dialog, text"),
|
|
[url host]]];
|
|
|
|
[alert setAlertStyle:NSWarningAlertStyle];
|
|
[alert runModal];
|
|
[alert release];
|
|
}
|
|
}
|
|
|
|
- (NSMutableDictionary *)extractArgumentsFromOdocEvent:
|
|
(NSAppleEventDescriptor *)desc
|
|
{
|
|
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
|
|
|
|
// 1. Extract ODB parameters (if any)
|
|
NSAppleEventDescriptor *odbdesc = desc;
|
|
if (![odbdesc paramDescriptorForKeyword:keyFileSender]) {
|
|
// The ODB paramaters may hide inside the 'keyAEPropData' descriptor.
|
|
odbdesc = [odbdesc paramDescriptorForKeyword:keyAEPropData];
|
|
if (![odbdesc paramDescriptorForKeyword:keyFileSender])
|
|
odbdesc = nil;
|
|
}
|
|
|
|
if (odbdesc) {
|
|
NSAppleEventDescriptor *p =
|
|
[odbdesc paramDescriptorForKeyword:keyFileSender];
|
|
if (p)
|
|
[dict setObject:[NSNumber numberWithUnsignedInt:[p typeCodeValue]]
|
|
forKey:@"remoteID"];
|
|
|
|
p = [odbdesc paramDescriptorForKeyword:keyFileCustomPath];
|
|
if (p)
|
|
[dict setObject:[p stringValue] forKey:@"remotePath"];
|
|
|
|
p = [odbdesc paramDescriptorForKeyword:keyFileSenderToken];
|
|
if (p) {
|
|
[dict setObject:[NSNumber numberWithUnsignedLong:[p descriptorType]]
|
|
forKey:@"remoteTokenDescType"];
|
|
[dict setObject:[p data] forKey:@"remoteTokenData"];
|
|
}
|
|
}
|
|
|
|
// 2. Extract Xcode parameters (if any)
|
|
NSAppleEventDescriptor *xcodedesc =
|
|
[desc paramDescriptorForKeyword:keyAEPosition];
|
|
if (xcodedesc) {
|
|
NSRange range;
|
|
NSData *data = [xcodedesc data];
|
|
NSUInteger length = [data length];
|
|
|
|
if (length == sizeof(MMXcodeSelectionRange)) {
|
|
MMXcodeSelectionRange *sr = (MMXcodeSelectionRange*)[data bytes];
|
|
ASLogDebug(@"Xcode selection range (%d,%d,%d,%d,%d,%d)",
|
|
sr->unused1, sr->lineNum, sr->startRange, sr->endRange,
|
|
sr->unused2, sr->theDate);
|
|
|
|
if (sr->lineNum < 0) {
|
|
// Should select a range of characters.
|
|
range.location = sr->startRange + 1;
|
|
range.length = sr->endRange > sr->startRange
|
|
? sr->endRange - sr->startRange : 1;
|
|
} else {
|
|
// Should only move cursor to a line.
|
|
range.location = sr->lineNum + 1;
|
|
range.length = 0;
|
|
}
|
|
|
|
[dict setObject:NSStringFromRange(range) forKey:@"selectionRange"];
|
|
} else {
|
|
ASLogErr(@"Xcode selection range size mismatch! got=%ld "
|
|
"expected=%ld", length, sizeof(MMXcodeSelectionRange));
|
|
}
|
|
}
|
|
|
|
// 3. Extract Spotlight search text (if any)
|
|
NSAppleEventDescriptor *spotlightdesc =
|
|
[desc paramDescriptorForKeyword:keyAESearchText];
|
|
if (spotlightdesc) {
|
|
NSString *s = [[spotlightdesc stringValue]
|
|
stringBySanitizingSpotlightSearch];
|
|
if (s && [s length] > 0)
|
|
[dict setObject:s forKey:@"searchText"];
|
|
}
|
|
|
|
return dict;
|
|
}
|
|
|
|
- (void)scheduleVimControllerPreloadAfterDelay:(NSTimeInterval)delay
|
|
{
|
|
[self performSelector:@selector(preloadVimController:)
|
|
withObject:nil
|
|
afterDelay:delay];
|
|
}
|
|
|
|
- (void)cancelVimControllerPreloadRequests
|
|
{
|
|
[NSObject cancelPreviousPerformRequestsWithTarget:self
|
|
selector:@selector(preloadVimController:)
|
|
object:nil];
|
|
}
|
|
|
|
- (void)preloadVimController:(id)sender
|
|
{
|
|
// We only allow preloading of one Vim process at a time (to avoid hogging
|
|
// CPU), so schedule another preload in a little while if necessary.
|
|
if (-1 != preloadPid) {
|
|
[self scheduleVimControllerPreloadAfterDelay:2];
|
|
return;
|
|
}
|
|
|
|
if ([cachedVimControllers count] >= [self maxPreloadCacheSize])
|
|
return;
|
|
|
|
preloadPid = [self launchVimProcessWithArguments:
|
|
[NSArray arrayWithObject:@"--mmwaitforack"]
|
|
workingDirectory:nil];
|
|
|
|
// This method is kicked off via FSEvents, so if MacVim is in the
|
|
// background, the runloop won't bother flushing the autorelease pool.
|
|
// Triggering an NSEvent works around this.
|
|
// http://www.mikeash.com/pyblog/more-fun-with-autorelease.html
|
|
NSEvent* event = [NSEvent otherEventWithType:NSApplicationDefined
|
|
location:NSZeroPoint
|
|
modifierFlags:0
|
|
timestamp:0
|
|
windowNumber:0
|
|
context:nil
|
|
subtype:0
|
|
data1:0
|
|
data2:0];
|
|
[NSApp postEvent:event atStart:NO];
|
|
}
|
|
|
|
- (int)maxPreloadCacheSize
|
|
{
|
|
// The maximum number of Vim processes to keep in the cache can be
|
|
// controlled via the user default "MMPreloadCacheSize".
|
|
int maxCacheSize = [[NSUserDefaults standardUserDefaults]
|
|
integerForKey:MMPreloadCacheSizeKey];
|
|
if (maxCacheSize < 0) maxCacheSize = 0;
|
|
else if (maxCacheSize > 10) maxCacheSize = 10;
|
|
|
|
return maxCacheSize;
|
|
}
|
|
|
|
- (MMVimController *)takeVimControllerFromCache
|
|
{
|
|
// NOTE: After calling this message the backend corresponding to the
|
|
// returned vim controller must be sent an acknowledgeConnection message,
|
|
// else the vim process will be stuck.
|
|
//
|
|
// This method may return nil even though the cache might be non-empty; the
|
|
// caller should handle this by starting a new Vim process.
|
|
|
|
int i, count = [cachedVimControllers count];
|
|
if (0 == count) return nil;
|
|
|
|
// Locate the first Vim controller with up-to-date rc-files sourced.
|
|
NSDate *rcDate = [self rcFilesModificationDate];
|
|
for (i = 0; i < count; ++i) {
|
|
MMVimController *vc = [cachedVimControllers objectAtIndex:i];
|
|
NSDate *date = [vc creationDate];
|
|
if ([date compare:rcDate] != NSOrderedAscending)
|
|
break;
|
|
}
|
|
|
|
if (i > 0) {
|
|
// Clear out cache entries whose vimrc/gvimrc files were sourced before
|
|
// the latest modification date for those files. This ensures that the
|
|
// latest rc-files are always sourced for new windows.
|
|
[self clearPreloadCacheWithCount:i];
|
|
}
|
|
|
|
if ([cachedVimControllers count] == 0) {
|
|
[self scheduleVimControllerPreloadAfterDelay:2.0];
|
|
return nil;
|
|
}
|
|
|
|
MMVimController *vc = [cachedVimControllers objectAtIndex:0];
|
|
[vimControllers addObject:vc];
|
|
[cachedVimControllers removeObjectAtIndex:0];
|
|
[vc setIsPreloading:NO];
|
|
|
|
// If the Vim process has finished loading then the window will displayed
|
|
// now, otherwise it will be displayed when the OpenWindowMsgID message is
|
|
// received.
|
|
[[vc windowController] presentWindow:nil];
|
|
|
|
// Since we've taken one controller from the cache we take the opportunity
|
|
// to preload another.
|
|
[self scheduleVimControllerPreloadAfterDelay:1];
|
|
|
|
return vc;
|
|
}
|
|
|
|
- (void)clearPreloadCacheWithCount:(int)count
|
|
{
|
|
// Remove the 'count' first entries in the preload cache. It is assumed
|
|
// that objects are added/removed from the cache in a FIFO manner so that
|
|
// this effectively clears the 'count' oldest entries.
|
|
// If 'count' is negative, then the entire cache is cleared.
|
|
|
|
if ([cachedVimControllers count] == 0 || count == 0)
|
|
return;
|
|
|
|
if (count < 0)
|
|
count = [cachedVimControllers count];
|
|
|
|
// Make sure the preloaded Vim processes get killed or they'll just hang
|
|
// around being useless until MacVim is terminated.
|
|
NSEnumerator *e = [cachedVimControllers objectEnumerator];
|
|
MMVimController *vc;
|
|
int n = count;
|
|
while ((vc = [e nextObject]) && n-- > 0) {
|
|
[[NSNotificationCenter defaultCenter] removeObserver:vc];
|
|
[vc sendMessage:TerminateNowMsgID data:nil];
|
|
|
|
// Since the preloaded processes were killed "prematurely" we have to
|
|
// manually tell them to cleanup (it is not enough to simply release
|
|
// them since deallocation and cleanup are separated).
|
|
[vc cleanup];
|
|
}
|
|
|
|
n = count;
|
|
while (n-- > 0 && [cachedVimControllers count] > 0)
|
|
[cachedVimControllers removeObjectAtIndex:0];
|
|
|
|
// 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)rebuildPreloadCache
|
|
{
|
|
if ([self maxPreloadCacheSize] > 0) {
|
|
[self clearPreloadCacheWithCount:-1];
|
|
[self cancelVimControllerPreloadRequests];
|
|
[self scheduleVimControllerPreloadAfterDelay:1.0];
|
|
}
|
|
}
|
|
|
|
- (NSDate *)rcFilesModificationDate
|
|
{
|
|
// Check modification dates for ~/.vimrc and ~/.gvimrc and return the
|
|
// latest modification date. If ~/.vimrc does not exist, check ~/_vimrc
|
|
// and similarly for gvimrc.
|
|
// Returns distantPath if no rc files were found.
|
|
|
|
NSDate *date = [NSDate distantPast];
|
|
NSFileManager *fm = [NSFileManager defaultManager];
|
|
|
|
NSString *path = [@"~/.vimrc" stringByExpandingTildeInPath];
|
|
NSDictionary *attr = [fm attributesOfItemAtPath:path error:NULL];
|
|
if (!attr) {
|
|
path = [@"~/_vimrc" stringByExpandingTildeInPath];
|
|
attr = [fm attributesOfItemAtPath:path error:NULL];
|
|
}
|
|
NSDate *modDate = [attr objectForKey:NSFileModificationDate];
|
|
if (modDate)
|
|
date = modDate;
|
|
|
|
path = [@"~/.gvimrc" stringByExpandingTildeInPath];
|
|
attr = [fm attributesOfItemAtPath:path error:NULL];
|
|
if (!attr) {
|
|
path = [@"~/_gvimrc" stringByExpandingTildeInPath];
|
|
attr = [fm attributesOfItemAtPath:path error:NULL];
|
|
}
|
|
modDate = [attr objectForKey:NSFileModificationDate];
|
|
if (modDate)
|
|
date = [date laterDate:modDate];
|
|
|
|
return date;
|
|
}
|
|
|
|
- (BOOL)openVimControllerWithArguments:(NSDictionary *)arguments
|
|
{
|
|
MMVimController *vc = [self takeVimControllerFromCache];
|
|
if (vc) {
|
|
// Open files in a new window using a cached vim controller. This
|
|
// requires virtually no loading time so the new window will pop up
|
|
// instantaneously.
|
|
[vc passArguments:arguments];
|
|
[[vc backendProxy] acknowledgeConnection];
|
|
} else {
|
|
NSArray *cmdline = nil;
|
|
NSString *cwd = [self workingDirectoryForArguments:arguments];
|
|
arguments = [self convertVimControllerArguments:arguments
|
|
toCommandLine:&cmdline];
|
|
int pid = [self launchVimProcessWithArguments:cmdline
|
|
workingDirectory:cwd];
|
|
if (-1 == pid)
|
|
return NO;
|
|
|
|
// TODO: If the Vim process fails to start, or if it changes PID,
|
|
// then the memory allocated for these parameters will leak.
|
|
// Ensure that this cannot happen or somehow detect it.
|
|
|
|
if ([arguments count] > 0)
|
|
[pidArguments setObject:arguments
|
|
forKey:[NSNumber numberWithInt:pid]];
|
|
}
|
|
|
|
return YES;
|
|
}
|
|
|
|
- (void)activateWhenNextWindowOpens
|
|
{
|
|
ASLogDebug(@"Activate MacVim when next window opens");
|
|
shouldActivateWhenNextWindowOpens = YES;
|
|
}
|
|
|
|
- (void)startWatchingVimDir
|
|
{
|
|
if (fsEventStream)
|
|
return;
|
|
|
|
NSString *path = [@"~/.vim" stringByExpandingTildeInPath];
|
|
NSArray *pathsToWatch = [NSArray arrayWithObject:path];
|
|
|
|
fsEventStream = FSEventStreamCreate(NULL, &fsEventCallback, NULL,
|
|
(CFArrayRef)pathsToWatch, kFSEventStreamEventIdSinceNow,
|
|
MMEventStreamLatency, kFSEventStreamCreateFlagNone);
|
|
|
|
FSEventStreamScheduleWithRunLoop(fsEventStream,
|
|
[[NSRunLoop currentRunLoop] getCFRunLoop],
|
|
kCFRunLoopDefaultMode);
|
|
|
|
FSEventStreamStart(fsEventStream);
|
|
ASLogDebug(@"Started FS event stream");
|
|
}
|
|
|
|
- (void)stopWatchingVimDir
|
|
{
|
|
if (fsEventStream) {
|
|
FSEventStreamStop(fsEventStream);
|
|
FSEventStreamInvalidate(fsEventStream);
|
|
FSEventStreamRelease(fsEventStream);
|
|
fsEventStream = NULL;
|
|
ASLogDebug(@"Stopped FS event stream");
|
|
}
|
|
}
|
|
|
|
- (void)handleFSEvent
|
|
{
|
|
[self clearPreloadCacheWithCount:-1];
|
|
|
|
// Several FS events may arrive in quick succession so make sure to cancel
|
|
// any previous preload requests before making a new one.
|
|
[self cancelVimControllerPreloadRequests];
|
|
[self scheduleVimControllerPreloadAfterDelay:0.5];
|
|
}
|
|
|
|
- (int)executeInLoginShell:(NSString *)path arguments:(NSArray *)args
|
|
{
|
|
// Start a login shell and execute the command 'path' with arguments 'args'
|
|
// in the shell. This ensures that user environment variables are set even
|
|
// when MacVim was started from the Finder.
|
|
|
|
int pid = -1;
|
|
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
|
|
|
|
// Determine which shell to use to execute the command. The user
|
|
// may decide which shell to use by setting a user default or the
|
|
// $SHELL environment variable.
|
|
NSString *shell = [ud stringForKey:MMLoginShellCommandKey];
|
|
if (!shell || [shell length] == 0)
|
|
shell = [[[NSProcessInfo processInfo] environment]
|
|
objectForKey:@"SHELL"];
|
|
if (!shell)
|
|
shell = @"/bin/bash";
|
|
|
|
// Bash needs the '-l' flag to launch a login shell. The user may add
|
|
// flags by setting a user default.
|
|
NSString *shellArgument = [ud stringForKey:MMLoginShellArgumentKey];
|
|
if (!shellArgument || [shellArgument length] == 0) {
|
|
if ([[shell lastPathComponent] isEqual:@"bash"])
|
|
shellArgument = @"-l";
|
|
else
|
|
shellArgument = nil;
|
|
}
|
|
|
|
// Build input string to pipe to the login shell.
|
|
NSMutableString *input = [NSMutableString stringWithFormat:
|
|
@"exec \"%@\"", path];
|
|
if (args) {
|
|
// Append all arguments, making sure they are properly quoted, even
|
|
// when they contain single quotes.
|
|
NSEnumerator *e = [args objectEnumerator];
|
|
id obj;
|
|
|
|
while ((obj = [e nextObject])) {
|
|
NSMutableString *arg = [NSMutableString stringWithString:obj];
|
|
[arg replaceOccurrencesOfString:@"'" withString:@"'\"'\"'"
|
|
options:NSLiteralSearch
|
|
range:NSMakeRange(0, [arg length])];
|
|
[input appendFormat:@" '%@'", arg];
|
|
}
|
|
}
|
|
|
|
// Build the argument vector used to start the login shell.
|
|
NSString *shellArg0 = [NSString stringWithFormat:@"-%@",
|
|
[shell lastPathComponent]];
|
|
char *shellArgv[3] = { (char *)[shellArg0 UTF8String], NULL, NULL };
|
|
if (shellArgument)
|
|
shellArgv[1] = (char *)[shellArgument UTF8String];
|
|
|
|
// Get the C string representation of the shell path before the fork since
|
|
// we must not call Foundation functions after a fork.
|
|
const char *shellPath = [shell fileSystemRepresentation];
|
|
|
|
// Fork and execute the process.
|
|
int ds[2];
|
|
if (pipe(ds)) return -1;
|
|
|
|
pid = fork();
|
|
if (pid == -1) {
|
|
return -1;
|
|
} else if (pid == 0) {
|
|
// Child process
|
|
|
|
if (close(ds[1]) == -1) exit(255);
|
|
if (dup2(ds[0], 0) == -1) exit(255);
|
|
|
|
// Without the following call warning messages like this appear on the
|
|
// console:
|
|
// com.apple.launchd[69] : Stray process with PGID equal to this
|
|
// dead job: PID 1589 PPID 1 Vim
|
|
setsid();
|
|
|
|
execv(shellPath, shellArgv);
|
|
|
|
// Never reached unless execv fails
|
|
exit(255);
|
|
} else {
|
|
// Parent process
|
|
if (close(ds[0]) == -1) return -1;
|
|
|
|
// Send input to execute to the child process
|
|
[input appendString:@"\n"];
|
|
int bytes = [input lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
|
|
|
|
if (write(ds[1], [input UTF8String], bytes) != bytes) return -1;
|
|
if (close(ds[1]) == -1) return -1;
|
|
|
|
++numChildProcesses;
|
|
ASLogDebug(@"new process pid=%d (count=%d)", pid, numChildProcesses);
|
|
}
|
|
|
|
return pid;
|
|
}
|
|
|
|
- (void)reapChildProcesses:(id)sender
|
|
{
|
|
// NOTE: numChildProcesses (currently) only counts the number of Vim
|
|
// processes that have been started with executeInLoginShell::. If other
|
|
// processes are spawned this code may need to be adjusted (or
|
|
// numChildProcesses needs to be incremented when such a process is
|
|
// started).
|
|
while (numChildProcesses > 0) {
|
|
int status = 0;
|
|
int pid = waitpid(-1, &status, WNOHANG);
|
|
if (pid <= 0)
|
|
break;
|
|
|
|
ASLogDebug(@"Wait for pid=%d complete", pid);
|
|
--numChildProcesses;
|
|
}
|
|
}
|
|
|
|
- (void)processInputQueues:(id)sender
|
|
{
|
|
// NOTE: Because we use distributed objects it is quite possible for this
|
|
// function to be re-entered. This can cause all sorts of unexpected
|
|
// problems so we guard against it here so that the rest of the code does
|
|
// not need to worry about it.
|
|
|
|
// The processing flag is > 0 if this function is already on the call
|
|
// stack; < 0 if this function was also re-entered.
|
|
if (processingFlag != 0) {
|
|
ASLogDebug(@"BUSY!");
|
|
processingFlag = -1;
|
|
return;
|
|
}
|
|
|
|
// NOTE: Be _very_ careful that no exceptions can be raised between here
|
|
// and the point at which 'processingFlag' is reset. Otherwise the above
|
|
// test could end up always failing and no input queues would ever be
|
|
// processed!
|
|
processingFlag = 1;
|
|
|
|
// NOTE: New input may arrive while we're busy processing; we deal with
|
|
// this by putting the current queue aside and creating a new input queue
|
|
// for future input.
|
|
NSDictionary *queues = inputQueues;
|
|
inputQueues = [NSMutableDictionary new];
|
|
|
|
// Pass each input queue on to the vim controller with matching
|
|
// identifier (and note that it could be cached).
|
|
NSEnumerator *e = [queues keyEnumerator];
|
|
NSNumber *key;
|
|
while ((key = [e nextObject])) {
|
|
unsigned ukey = [key unsignedIntValue];
|
|
int i = 0, count = [vimControllers count];
|
|
for (i = 0; i < count; ++i) {
|
|
MMVimController *vc = [vimControllers objectAtIndex:i];
|
|
if (ukey == [vc vimControllerId]) {
|
|
[vc processInputQueue:[queues objectForKey:key]]; // !exceptions
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (i < count) continue;
|
|
|
|
count = [cachedVimControllers count];
|
|
for (i = 0; i < count; ++i) {
|
|
MMVimController *vc = [cachedVimControllers objectAtIndex:i];
|
|
if (ukey == [vc vimControllerId]) {
|
|
[vc processInputQueue:[queues objectForKey:key]]; // !exceptions
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (i == count) {
|
|
ASLogWarn(@"No Vim controller for identifier=%d", ukey);
|
|
}
|
|
}
|
|
|
|
[queues release];
|
|
|
|
// If new input arrived while we were processing it would have been
|
|
// blocked so we have to schedule it to be processed again.
|
|
if (processingFlag < 0)
|
|
[self performSelectorOnMainThread:@selector(processInputQueues:)
|
|
withObject:nil
|
|
waitUntilDone:NO
|
|
modes:[NSArray arrayWithObjects:
|
|
NSDefaultRunLoopMode,
|
|
NSEventTrackingRunLoopMode, nil]];
|
|
|
|
processingFlag = 0;
|
|
}
|
|
|
|
- (void)addVimController:(MMVimController *)vc
|
|
{
|
|
ASLogDebug(@"Add Vim controller pid=%d id=%d",
|
|
[vc pid], [vc vimControllerId]);
|
|
|
|
int pid = [vc pid];
|
|
NSNumber *pidKey = [NSNumber numberWithInt:pid];
|
|
id args = [pidArguments objectForKey:pidKey];
|
|
|
|
if (preloadPid == pid) {
|
|
// This controller was preloaded, so add it to the cache and
|
|
// schedule another vim process to be preloaded.
|
|
preloadPid = -1;
|
|
[vc setIsPreloading:YES];
|
|
[cachedVimControllers addObject:vc];
|
|
[self scheduleVimControllerPreloadAfterDelay:1];
|
|
} else {
|
|
[vimControllers addObject:vc];
|
|
|
|
if (args && [NSNull null] != args)
|
|
[vc passArguments:args];
|
|
|
|
// HACK! MacVim does not get activated if it is launched from the
|
|
// terminal, so we forcibly activate here. Note that each process
|
|
// launched from MacVim has an entry in the pidArguments dictionary,
|
|
// which is how we detect if the process was launched from the
|
|
// terminal.
|
|
if (!args) [self activateWhenNextWindowOpens];
|
|
}
|
|
|
|
if (args)
|
|
[pidArguments removeObjectForKey:pidKey];
|
|
}
|
|
|
|
- (NSDictionary *)convertVimControllerArguments:(NSDictionary *)args
|
|
toCommandLine:(NSArray **)cmdline
|
|
{
|
|
// Take all arguments out of 'args' and put them on an array suitable to
|
|
// pass as arguments to launchVimProcessWithArguments:. The untouched
|
|
// dictionary items are returned in a new autoreleased dictionary.
|
|
|
|
if (cmdline)
|
|
*cmdline = nil;
|
|
|
|
NSArray *filenames = [args objectForKey:@"filenames"];
|
|
int numFiles = filenames ? [filenames count] : 0;
|
|
BOOL openFiles = ![[args objectForKey:@"dontOpen"] boolValue];
|
|
|
|
if (numFiles <= 0 || !openFiles)
|
|
return args;
|
|
|
|
NSMutableArray *a = [NSMutableArray array];
|
|
NSMutableDictionary *d = [[args mutableCopy] autorelease];
|
|
|
|
// Search for text and highlight it (this Vim script avoids warnings in
|
|
// case there is no match for the search text).
|
|
NSString *searchText = [args objectForKey:@"searchText"];
|
|
if (searchText && [searchText length] > 0) {
|
|
[a addObject:@"-c"];
|
|
NSString *s = [NSString stringWithFormat:@"if search('\\V\\c%@','cW')"
|
|
"|let @/='\\V\\c%@'|set hls|endif", searchText, searchText];
|
|
[a addObject:s];
|
|
|
|
[d removeObjectForKey:@"searchText"];
|
|
}
|
|
|
|
// Position cursor using "+line" or "-c :cal cursor(line,column)".
|
|
NSString *lineString = [args objectForKey:@"cursorLine"];
|
|
if (lineString && [lineString intValue] > 0) {
|
|
NSString *columnString = [args objectForKey:@"cursorColumn"];
|
|
if (columnString && [columnString intValue] > 0) {
|
|
[a addObject:@"-c"];
|
|
[a addObject:[NSString stringWithFormat:@":cal cursor(%@,%@)",
|
|
lineString, columnString]];
|
|
|
|
[d removeObjectForKey:@"cursorColumn"];
|
|
} else {
|
|
[a addObject:[NSString stringWithFormat:@"+%@", lineString]];
|
|
}
|
|
|
|
[d removeObjectForKey:@"cursorLine"];
|
|
}
|
|
|
|
// Set selection using normal mode commands.
|
|
NSString *rangeString = [args objectForKey:@"selectionRange"];
|
|
if (rangeString) {
|
|
NSRange r = NSRangeFromString(rangeString);
|
|
[a addObject:@"-c"];
|
|
if (r.length > 0) {
|
|
// Select given range of characters.
|
|
// TODO: This only works for encodings where 1 byte == 1 character
|
|
[a addObject:[NSString stringWithFormat:@"norm %ldgov%ldgo",
|
|
r.location, NSMaxRange(r)-1]];
|
|
} else {
|
|
// Position cursor on line at start of range.
|
|
[a addObject:[NSString stringWithFormat:@"norm %ldGz.0",
|
|
r.location]];
|
|
}
|
|
|
|
[d removeObjectForKey:@"selectionRange"];
|
|
}
|
|
|
|
// Choose file layout using "-[o|O|p]".
|
|
int layout = [[args objectForKey:@"layout"] intValue];
|
|
switch (layout) {
|
|
case MMLayoutHorizontalSplit: [a addObject:@"-o"]; break;
|
|
case MMLayoutVerticalSplit: [a addObject:@"-O"]; break;
|
|
case MMLayoutTabs: [a addObject:@"-p"]; break;
|
|
}
|
|
[d removeObjectForKey:@"layout"];
|
|
|
|
|
|
// Last of all add the names of all files to open (DO NOT add more args
|
|
// after this point).
|
|
[a addObjectsFromArray:filenames];
|
|
|
|
if ([args objectForKey:@"remoteID"]) {
|
|
// These files should be edited remotely so keep the filenames on the
|
|
// argument list -- they will need to be passed back to Vim when it
|
|
// checks in. Also set the 'dontOpen' flag or the files will be
|
|
// opened twice.
|
|
[d setObject:[NSNumber numberWithBool:YES] forKey:@"dontOpen"];
|
|
} else {
|
|
[d removeObjectForKey:@"dontOpen"];
|
|
[d removeObjectForKey:@"filenames"];
|
|
}
|
|
|
|
if (cmdline)
|
|
*cmdline = a;
|
|
|
|
return d;
|
|
}
|
|
|
|
- (NSString *)workingDirectoryForArguments:(NSDictionary *)args
|
|
{
|
|
// Find the "filenames" argument and pick the first path that actually
|
|
// exists and return it.
|
|
// TODO: Return common parent directory in the case of multiple files?
|
|
NSFileManager *fm = [NSFileManager defaultManager];
|
|
NSArray *filenames = [args objectForKey:@"filenames"];
|
|
NSUInteger i, count = [filenames count];
|
|
for (i = 0; i < count; ++i) {
|
|
BOOL isdir;
|
|
NSString *file = [filenames objectAtIndex:i];
|
|
if ([fm fileExistsAtPath:file isDirectory:&isdir])
|
|
return isdir ? file : [file stringByDeletingLastPathComponent];
|
|
}
|
|
|
|
return nil;
|
|
}
|
|
|
|
- (NSScreen *)screenContainingTopLeftPoint:(NSPoint)pt
|
|
{
|
|
// NOTE: The top left point has y-coordinate which lies one pixel above the
|
|
// window which must be taken into consideration (this method used to be
|
|
// called screenContainingPoint: but that method is "off by one" in
|
|
// y-coordinate).
|
|
|
|
NSArray *screens = [NSScreen screens];
|
|
NSUInteger i, count = [screens count];
|
|
for (i = 0; i < count; ++i) {
|
|
NSScreen *screen = [screens objectAtIndex:i];
|
|
NSRect frame = [screen frame];
|
|
if (pt.x >= frame.origin.x && pt.x < NSMaxX(frame)
|
|
// NOTE: inequalities below are correct due to this being a top
|
|
// left test (see comment above)
|
|
&& pt.y > frame.origin.y && pt.y <= NSMaxY(frame))
|
|
return screen;
|
|
}
|
|
|
|
return nil;
|
|
}
|
|
|
|
- (void)addInputSourceChangedObserver
|
|
{
|
|
id nc = [NSDistributedNotificationCenter defaultCenter];
|
|
NSString *notifyInputSourceChanged =
|
|
(NSString *)kTISNotifySelectedKeyboardInputSourceChanged;
|
|
[nc addObserver:self
|
|
selector:@selector(inputSourceChanged:)
|
|
name:notifyInputSourceChanged
|
|
object:nil];
|
|
}
|
|
|
|
- (void)removeInputSourceChangedObserver
|
|
{
|
|
id nc = [NSDistributedNotificationCenter defaultCenter];
|
|
[nc removeObserver:self];
|
|
}
|
|
|
|
- (void)inputSourceChanged:(NSNotification *)notification
|
|
{
|
|
unsigned i, count = [vimControllers count];
|
|
for (i = 0; i < count; ++i) {
|
|
MMVimController *controller = [vimControllers objectAtIndex:i];
|
|
MMWindowController *wc = [controller windowController];
|
|
MMTextView *tv = (MMTextView *)[[wc vimView] textView];
|
|
[tv checkImState];
|
|
}
|
|
}
|
|
|
|
@end // MMAppController (Private)
|