diff --git a/.gitignore b/.gitignore index 993e1285a..af9371e4d 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,7 @@ build .xcodebuild-info node_modules npm-debug.log -/tags +tags /cef/ /sources.gypi /node/ diff --git a/atom.gyp b/atom.gyp index c586fc011..b8ce02cc3 100644 --- a/atom.gyp +++ b/atom.gyp @@ -251,8 +251,6 @@ 'native/message_translation.cpp', 'native/message_translation.h', 'native/message_translation.h', - 'native/path_watcher.h', - 'native/path_watcher.mm', 'native/v8_extensions/atom.h', 'native/v8_extensions/atom.mm', 'native/v8_extensions/native.h', @@ -269,6 +267,13 @@ 'native/mac/English.lproj/AtomWindow.xib', 'native/mac/English.lproj/MainMenu.xib', ], + 'conditions': [ + ['CODE_SIGN', { + 'defines': [ + 'CODE_SIGNING_ENABLED=1', + ], + }], + ], 'postbuilds': [ { 'postbuild_name': 'Copy Static Files', diff --git a/native/atom_application.mm b/native/atom_application.mm index ab26df02d..9c9824579 100644 --- a/native/atom_application.mm +++ b/native/atom_application.mm @@ -143,12 +143,34 @@ } - (void)open:(NSString *)path pidToKillWhenWindowCloses:(NSNumber *)pid { - for (NSWindow *window in [self windows]) { - if (![window isExcludedFromWindowsMenu]) { - AtomWindowController *controller = [window windowController]; - if ([path isEqualToString:controller.pathToOpen]) { - [window makeKeyAndOrderFront:nil]; - return; + BOOL openingDirectory = false; + [[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&openingDirectory]; + + if (!pid) { + for (NSWindow *window in [self windows]) { + if (![window isExcludedFromWindowsMenu]) { + AtomWindowController *controller = [window windowController]; + if (!openingDirectory) { + BOOL openedPathIsDirectory = false; + [[NSFileManager defaultManager] fileExistsAtPath:controller.pathToOpen isDirectory:&openedPathIsDirectory]; + NSString *projectPath = NULL; + if (openedPathIsDirectory) { + projectPath = [NSString stringWithFormat:@"%@/", controller.pathToOpen]; + } + else { + projectPath = [controller.pathToOpen stringByDeletingLastPathComponent]; + } + if ([path hasPrefix:projectPath]) { + [window makeKeyAndOrderFront:nil]; + [controller openPath:path]; + return; + } + } + + if ([path isEqualToString:controller.pathToOpen]) { + [window makeKeyAndOrderFront:nil]; + return; + } } } } @@ -232,12 +254,14 @@ } else { _backgroundWindowController = [[AtomWindowController alloc] initInBackground]; - if (![self.arguments objectForKey:@"dev"]) { - SUUpdater.sharedUpdater.delegate = self; - SUUpdater.sharedUpdater.automaticallyChecksForUpdates = YES; - SUUpdater.sharedUpdater.automaticallyDownloadsUpdates = YES; - [SUUpdater.sharedUpdater checkForUpdatesInBackground]; - } + +#if defined(CODE_SIGNING_ENABLED) + SUUpdater.sharedUpdater.delegate = self; + SUUpdater.sharedUpdater.automaticallyChecksForUpdates = YES; + SUUpdater.sharedUpdater.automaticallyDownloadsUpdates = YES; + [SUUpdater.sharedUpdater checkForUpdatesInBackground]; +#endif + } } diff --git a/native/atom_cef_client.cpp b/native/atom_cef_client.cpp index 6ddf00ce1..8c697fba8 100644 --- a/native/atom_cef_client.cpp +++ b/native/atom_cef_client.cpp @@ -90,6 +90,9 @@ bool AtomCefClient::OnProcessMessageReceived(CefRefPtr browser, else if (name == "crash") { __builtin_trap(); } + else if (name == "restartRendererProcess") { + RestartRendererProcess(browser); + } else { return false; } @@ -252,3 +255,11 @@ bool AtomCefClient::Save(const std::string& path, const std::string& data) { fclose(f); return true; } + +void AtomCefClient::RestartRendererProcess(CefRefPtr browser) { + // Navigating to the same URL has the effect of restarting the renderer + // process, because cefode has overridden ContentBrowserClient's + // ShouldSwapProcessesForNavigation method. + CefRefPtr frame = browser->GetFocusedFrame(); + frame->LoadURL(frame->GetURL()); +} diff --git a/native/atom_cef_client.h b/native/atom_cef_client.h index 494009c5c..0e49194b5 100644 --- a/native/atom_cef_client.h +++ b/native/atom_cef_client.h @@ -99,6 +99,7 @@ class AtomCefClient : public CefClient, void EndTracing(); bool Save(const std::string& path, const std::string& data); + void RestartRendererProcess(CefRefPtr browser); protected: CefRefPtr m_Browser; diff --git a/native/atom_cef_render_process_handler.h b/native/atom_cef_render_process_handler.h index 96fe15a5c..9bcb2da60 100644 --- a/native/atom_cef_render_process_handler.h +++ b/native/atom_cef_render_process_handler.h @@ -18,7 +18,6 @@ class AtomCefRenderProcessHandler : public CefRenderProcessHandler { CefRefPtr message) OVERRIDE; void Reload(CefRefPtr browser); - void Shutdown(CefRefPtr browser); bool CallMessageReceivedHandler(CefRefPtr context, CefRefPtr message); void InjectExtensionsIntoV8Context(CefRefPtr context); diff --git a/native/atom_cef_render_process_handler.mm b/native/atom_cef_render_process_handler.mm index b3147478c..4f9a02d82 100644 --- a/native/atom_cef_render_process_handler.mm +++ b/native/atom_cef_render_process_handler.mm @@ -2,7 +2,6 @@ #import "native/v8_extensions/atom.h" #import "native/v8_extensions/native.h" #import "native/message_translation.h" -#import "path_watcher.h" #import "atom_cef_render_process_handler.h" @@ -18,7 +17,6 @@ void AtomCefRenderProcessHandler::OnContextCreated(CefRefPtr browser void AtomCefRenderProcessHandler::OnContextReleased(CefRefPtr browser, CefRefPtr frame, CefRefPtr context) { - [PathWatcher removePathWatcherForContext:context]; } bool AtomCefRenderProcessHandler::OnProcessMessageReceived(CefRefPtr browser, @@ -30,10 +28,6 @@ bool AtomCefRenderProcessHandler::OnProcessMessageReceived(CefRefPtr Reload(browser); return true; } - else if (name == "shutdown") { - Shutdown(browser); - return true; - } else { return CallMessageReceivedHandler(browser->GetMainFrame()->GetV8Context(), message); } @@ -54,17 +48,6 @@ void AtomCefRenderProcessHandler::Reload(CefRefPtr browser) { context->Exit(); } -void AtomCefRenderProcessHandler::Shutdown(CefRefPtr browser) { - CefRefPtr context = browser->GetMainFrame()->GetV8Context(); - CefRefPtr global = context->GetGlobal(); - - context->Enter(); - CefV8ValueList arguments; - CefRefPtr shutdownFunction = global->GetValue("shutdown"); - shutdownFunction->ExecuteFunction(global, arguments); - context->Exit(); -} - bool AtomCefRenderProcessHandler::CallMessageReceivedHandler(CefRefPtr context, CefRefPtr message) { context->Enter(); diff --git a/native/atom_window_controller.h b/native/atom_window_controller.h index 15e5983b9..089907ba9 100644 --- a/native/atom_window_controller.h +++ b/native/atom_window_controller.h @@ -34,5 +34,6 @@ class AtomCefClient; - (void)toggleDevTools; - (void)showDevTools; +- (void)openPath:(NSString*)path; @end diff --git a/native/atom_window_controller.mm b/native/atom_window_controller.mm index 957f84a4b..efb838b3e 100644 --- a/native/atom_window_controller.mm +++ b/native/atom_window_controller.mm @@ -215,6 +215,16 @@ _cefClient->GetBrowser()->GetHost()->SetFocus(true); } +- (void)openPath:(NSString*)path { + if (_cefClient && _cefClient->GetBrowser()) { + CefRefPtr openMessage = CefProcessMessage::Create("openPath"); + CefRefPtr openArguments = openMessage->GetArgumentList(); + openArguments->SetSize(1); + openArguments->SetString(0, [path UTF8String]); + _cefClient->GetBrowser()->SendProcessMessage(PID_RENDERER, openMessage); + } +} + - (void)setPidToKillOnClose:(NSNumber *)pid { _pidToKillOnClose = [pid retain]; } @@ -236,7 +246,7 @@ - (BOOL)windowShouldClose:(NSNotification *)notification { if (_cefClient && _cefClient->GetBrowser()) { - _cefClient->GetBrowser()->SendProcessMessage(PID_RENDERER, CefProcessMessage::Create("shutdown")); + _cefClient->GetBrowser()->GetHost()->CloseBrowser(false); } if (_pidToKillOnClose) kill([_pidToKillOnClose intValue], SIGQUIT); diff --git a/native/path_watcher.h b/native/path_watcher.h deleted file mode 100644 index 56da47018..000000000 --- a/native/path_watcher.h +++ /dev/null @@ -1,25 +0,0 @@ -#import "include/cef_base.h" -#import "include/cef_v8.h" -#import - -typedef void (^WatchCallback)(NSString *, NSString *); - -@interface PathWatcher : NSObject { - int _kq; - CefRefPtr _context; - NSMutableDictionary *_callbacksByPath; - NSMutableDictionary *_fileDescriptorsByPath; - - bool _keepWatching; -} - -+ (PathWatcher *)pathWatcherForContext:(CefRefPtr)context; -+ (void)removePathWatcherForContext:(CefRefPtr)context; - -- (id)initWithContext:(CefRefPtr)context; -- (NSString *)watchPath:(NSString *)path callback:(WatchCallback)callback; -- (void)unwatchPath:(NSString *)path callbackId:(NSString *)callbackId error:(NSError **)error; -- (void)unwatchAllPaths; -- (NSArray *)watchedPaths; - -@end diff --git a/native/path_watcher.mm b/native/path_watcher.mm deleted file mode 100644 index 47c7dee2f..000000000 --- a/native/path_watcher.mm +++ /dev/null @@ -1,273 +0,0 @@ -#import -#import -#import -#import - -#import "path_watcher.h" - -static NSMutableArray *gPathWatchers; - -@interface PathWatcher () -- (bool)usesContext:(CefRefPtr)context; -- (NSString *)watchPath:(NSString *)path callback:(WatchCallback)callback callbackId:(NSString *)callbackId; -- (void)stopWatching; -- (bool)isAtomicWrite:(struct kevent)event; -@end - -@implementation PathWatcher - -+ (PathWatcher *)pathWatcherForContext:(CefRefPtr)context { - if (!gPathWatchers) gPathWatchers = [[NSMutableArray alloc] init]; - - PathWatcher *pathWatcher = nil; - for (PathWatcher *p in gPathWatchers) { - if ([p usesContext:context]) { - pathWatcher = p; - break; - } - } - - if (!pathWatcher) { - pathWatcher = [[[PathWatcher alloc] initWithContext:context] autorelease]; - [gPathWatchers addObject:pathWatcher]; - } - - return pathWatcher; -} - -+ (void)removePathWatcherForContext:(CefRefPtr)context { - PathWatcher *pathWatcher = nil; - for (PathWatcher *p in gPathWatchers) { - if ([p usesContext:context]) { - pathWatcher = p; - break; - } - } - - if (pathWatcher) { - [pathWatcher stopWatching]; - [gPathWatchers removeObject:pathWatcher]; - } - -} - -- (void)dealloc { - @synchronized(self) { - close(_kq); - for (NSString *path in [_callbacksByPath allKeys]) { - [self removeKeventForPath:path]; - } - [_callbacksByPath release]; - _context = nil; - _keepWatching = false; - } - - [super dealloc]; -} - -- (id)initWithContext:(CefRefPtr)context { - self = [super init]; - - _keepWatching = YES; - _callbacksByPath = [[NSMutableDictionary alloc] init]; - _fileDescriptorsByPath = [[NSMutableDictionary alloc] init]; - _kq = kqueue(); - _context = context; - - if (_kq == -1) { - [NSException raise:@"PathWatcher" format:@"Could not create kqueue"]; - } - - [self performSelectorInBackground:@selector(watch) withObject:NULL]; - return self; -} - -- (bool)usesContext:(CefRefPtr)context { - return _context->IsSame(context); -} - -- (void)stopWatching { - @synchronized(self) { - [self unwatchAllPaths]; - _keepWatching = false; - } -} - -- (NSString *)watchPath:(NSString *)path callback:(WatchCallback)callback { - NSString *callbackId = [[NSProcessInfo processInfo] globallyUniqueString]; - return [self watchPath:path callback:callback callbackId:callbackId]; -} - -- (NSString *)watchPath:(NSString *)path callback:(WatchCallback)callback callbackId:(NSString *)callbackId { - @synchronized(self) { - if (![self createKeventForPath:path]) { - NSLog(@"WARNING: Failed to create kevent for path '%@'", path); - return nil; - } - - NSMutableDictionary *callbacks = [_callbacksByPath objectForKey:path]; - if (!callbacks) { - callbacks = [NSMutableDictionary dictionary]; - [_callbacksByPath setObject:callbacks forKey:path]; - } - - [callbacks setObject:callback forKey:callbackId]; - } - - return callbackId; -} - -- (void)unwatchPath:(NSString *)path callbackId:(NSString *)callbackId error:(NSError **)error { - @synchronized(self) { - NSMutableDictionary *callbacks = [_callbacksByPath objectForKey:path]; - - if (callbacks) { - if (callbackId) { - [callbacks removeObjectForKey:callbackId]; - } - else { - [callbacks removeAllObjects]; - } - - if (callbacks.count == 0) { - [self removeKeventForPath:path]; - [_callbacksByPath removeObjectForKey:path]; - } - } - } -} - -- (NSArray *)watchedPaths { - return [_callbacksByPath allKeys]; -} - -- (void)unwatchAllPaths { - @synchronized(self) { - NSArray *paths = [_callbacksByPath allKeys]; - for (NSString *path in paths) { - [self unwatchPath:path callbackId:nil error:nil]; - } - } -} - -- (bool)createKeventForPath:(NSString *)path { - @synchronized(self) { - if ([_fileDescriptorsByPath objectForKey:path]) { - NSLog(@"we already have a kevent"); - return YES; - } - - int fd = open([path fileSystemRepresentation], O_EVTONLY, 0); - if (fd < 0) { - NSLog(@"WARNING: Could not create file descriptor for path '%@'. Error code %d.", path, errno); - return NO; - } - - [_fileDescriptorsByPath setObject:[NSNumber numberWithInt:fd] forKey:path]; - - struct timespec timeout = { 0, 0 }; - struct kevent event; - int filter = EVFILT_VNODE; - int flags = EV_ADD | EV_ENABLE | EV_CLEAR; - int filterFlags = NOTE_WRITE | NOTE_DELETE | NOTE_RENAME; - EV_SET(&event, fd, filter, flags, filterFlags, 0, path); - kevent(_kq, &event, 1, NULL, 0, &timeout); - return YES; - } -} - -- (void)removeKeventForPath:(NSString *)path { - @synchronized(self) { - NSNumber *fdNumber = [_fileDescriptorsByPath objectForKey:path]; - if (!fdNumber) { - NSLog(@"WARNING: Could not find file descriptor for path '%@'", path); - return; - } - close([fdNumber integerValue]); - [_fileDescriptorsByPath removeObjectForKey:path]; - } - -} - -- (bool)isAtomicWrite:(struct kevent)event { - if (!event.fflags & NOTE_DELETE) return NO; - const char *path = [(NSString *)event.udata fileSystemRepresentation]; - bool fileExists = access(path, F_OK) != -1; - return fileExists; -} - -- (void)changePath:(NSString *)path toNewPath:(NSString *)newPath { - @synchronized(self) { - NSDictionary *callbacks = [NSDictionary dictionaryWithDictionary:[_callbacksByPath objectForKey:path]]; - [self unwatchPath:path callbackId:nil error:nil]; - for (NSString *callbackId in [callbacks allKeys]) { - [self watchPath:newPath callback:[callbacks objectForKey:callbackId] callbackId:callbackId]; - } - } -} - -- (void)watch { - struct kevent event; - struct timespec timeout = { 5, 0 }; // 5 seconds timeout. - - while (_keepWatching) { - @autoreleasepool { - int numberOfEvents = kevent(_kq, NULL, 0, &event, 1, &timeout); - if (numberOfEvents == 0) { - continue; - } - - NSString *eventFlag = nil; - NSString *newPath = nil; - NSString *path = [(NSString *)event.udata retain]; - - if (event.fflags & NOTE_WRITE) { - eventFlag = @"contents-change"; - } - else if ([self isAtomicWrite:event]) { - eventFlag = @"contents-change"; - // Atomic writes require the kqueue to be recreated - [self removeKeventForPath:path]; - [self createKeventForPath:path]; - } - else if (event.fflags & NOTE_DELETE) { - eventFlag = @"remove"; - } - else if (event.fflags & NOTE_RENAME) { - eventFlag = @"move"; - char pathBuffer[MAXPATHLEN]; - fcntl((int)event.ident, F_GETPATH, &pathBuffer); - close(event.ident); - newPath = [NSString stringWithUTF8String:pathBuffer]; - if (!newPath) { - NSLog(@"WARNING: Ignoring rename event for deleted file '%@'", path); - continue; - } - } - - NSDictionary *callbacks; - @synchronized(self) { - callbacks = [NSDictionary dictionaryWithDictionary:[_callbacksByPath objectForKey:path]]; - } - - if ([eventFlag isEqual:@"move"]) { - [self changePath:path toNewPath:newPath]; - } - - if ([eventFlag isEqual:@"remove"]) { - [self unwatchPath:path callbackId:nil error:nil]; - } - - dispatch_sync(dispatch_get_main_queue(), ^{ - for (NSString *key in callbacks) { - WatchCallback callback = [callbacks objectForKey:key]; - callback(eventFlag, newPath ? newPath : path); - } - }); - - [path release]; - } - } -} - -@end diff --git a/native/v8_extensions/native.mm b/native/v8_extensions/native.mm index b1ff00f3f..2cab4765f 100644 --- a/native/v8_extensions/native.mm +++ b/native/v8_extensions/native.mm @@ -4,7 +4,6 @@ #import "atom_application.h" #import "native.h" #import "include/cef_base.h" -#import "path_watcher.h" #import @@ -22,8 +21,7 @@ namespace v8_extensions { void Native::CreateContextBinding(CefRefPtr context) { const char* methodNames[] = { - "writeToPasteboard", "readFromPasteboard", "quit", "watchPath", - "unwatchPath", "getWatchedPaths", "unwatchAllPaths", "moveToTrash", + "writeToPasteboard", "readFromPasteboard", "quit", "moveToTrash", "reload", "setWindowState", "getWindowState", "beep", "crash" }; @@ -67,67 +65,6 @@ namespace v8_extensions { [NSApp terminate:nil]; return true; } - else if (name == "watchPath") { - NSString *path = stringFromCefV8Value(arguments[0]); - CefRefPtr function = arguments[1]; - - CefRefPtr context = CefV8Context::GetCurrentContext(); - - WatchCallback callback = ^(NSString *eventType, NSString *path) { - context->Enter(); - - CefV8ValueList args; - - args.push_back(CefV8Value::CreateString(string([eventType UTF8String], [eventType lengthOfBytesUsingEncoding:NSUTF8StringEncoding]))); - args.push_back(CefV8Value::CreateString(string([path UTF8String], [path lengthOfBytesUsingEncoding:NSUTF8StringEncoding]))); - function->ExecuteFunction(function, args); - - context->Exit(); - }; - - PathWatcher *pathWatcher = [PathWatcher pathWatcherForContext:CefV8Context::GetCurrentContext()]; - NSString *watchId = [pathWatcher watchPath:path callback:[[callback copy] autorelease]]; - if (watchId) { - retval = CefV8Value::CreateString([watchId UTF8String]); - } - else { - exception = string("Failed to watch path '") + string([path UTF8String]) + string("' (it may not exist)"); - } - - return true; - } - else if (name == "unwatchPath") { - NSString *path = stringFromCefV8Value(arguments[0]); - NSString *callbackId = stringFromCefV8Value(arguments[1]); - NSError *error = nil; - PathWatcher *pathWatcher = [PathWatcher pathWatcherForContext:CefV8Context::GetCurrentContext()]; - [pathWatcher unwatchPath:path callbackId:callbackId error:&error]; - - if (error) { - exception = [[error localizedDescription] UTF8String]; - } - - return true; - } - else if (name == "getWatchedPaths") { - PathWatcher *pathWatcher = [PathWatcher pathWatcherForContext:CefV8Context::GetCurrentContext()]; - NSArray *paths = [pathWatcher watchedPaths]; - - CefRefPtr pathsArray = CefV8Value::CreateArray([paths count]); - - for (int i = 0; i < [paths count]; i++) { - CefRefPtr path = CefV8Value::CreateString([[paths objectAtIndex:i] UTF8String]); - pathsArray->SetValue(i, path); - } - retval = pathsArray; - - return true; - } - else if (name == "unwatchAllPaths") { - PathWatcher *pathWatcher = [PathWatcher pathWatcherForContext:CefV8Context::GetCurrentContext()]; - [pathWatcher unwatchAllPaths]; - return true; - } else if (name == "moveToTrash") { NSString *sourcePath = stringFromCefV8Value(arguments[0]); bool success = [[NSWorkspace sharedWorkspace] performFileOperation:NSWorkspaceRecycleOperation diff --git a/package.json b/package.json index c691b0b27..c82ee7ea6 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "async": "0.2.6", "nak": "0.2.12", "spellchecker": "0.2.0", + "pathwatcher": "0.1.5", "plist": "git://github.com/nathansobo/node-plist.git", "space-pen": "git://github.com/nathansobo/space-pen.git" }, diff --git a/script/cibuild b/script/cibuild index 01a5298d3..dea583466 100755 --- a/script/cibuild +++ b/script/cibuild @@ -2,4 +2,4 @@ set -ex rm -rf ~/.atom -CI_BUILD=true rake clean test +rake clean test diff --git a/script/update-cefode b/script/update-cefode index 29e6a8074..e0f97c171 100755 --- a/script/update-cefode +++ b/script/update-cefode @@ -13,9 +13,7 @@ else TARGET=$1 fi -DISTURL="https://gh-contractor-zcbenz.s3.amazonaws.com/cefode2/prebuilt-cef" -CEF_BASENAME="cef_binary_3.1423.1133_macosx" -CEF_SYMBOLS_BASENAME="${CEF_BASENAME}_symbols" +DISTURL="https://gh-contractor-zcbenz.s3.amazonaws.com/cefode3/prebuilt-cef" TEMP_DIR=$(mktemp -d -t prebuilt-cef-download.XXXXXX) trap "rm -rf \"${TEMP_DIR}\"" EXIT @@ -29,8 +27,8 @@ fi CURRENT_VERSION=`cat cef/version 2>&1` if [[ $LATEST_VERSION != $CURRENT_VERSION ]]; then - echo "Downloading/extracting cefode2 u${LATEST_VERSION}..." - if [ -z "$CI_BUILD" ]; then + echo "Downloading/extracting cefode3 u${LATEST_VERSION}..." + if [ -t 1 ] ; then # If run from the terminal CURL_ARGS="--progress-bar" else CURL_ARGS="-fsS" @@ -38,7 +36,7 @@ if [[ $LATEST_VERSION != $CURRENT_VERSION ]]; then curl $CURL_ARGS "${DISTURL}/cef_binary_latest.zip" > "${TEMP_DIR}/cef.zip" unzip -q "${TEMP_DIR}/cef.zip" -d "${TEMP_DIR}" [ -e "${TARGET}" ] && rm -rf "${TARGET}" - mv "${TEMP_DIR}/${CEF_BASENAME}" "${TARGET}" + mv "${TEMP_DIR}"/*_macosx "${TARGET}" echo ${LATEST_VERSION} > 'cef/version' fi @@ -46,7 +44,7 @@ if [[ "${SYMBOLS}" != "1" ]]; then exit 0 fi -echo "Downloading/extracting symbols for cefode2 u${LATEST_VERSION}..." +echo "Downloading/extracting symbols for cefode3 u${LATEST_VERSION}..." curl --progress-bar "${DISTURL}/cef_binary_latest_symbols.zip" > "${TEMP_DIR}/symbols.zip" unzip -q "${TEMP_DIR}/symbols.zip" -d "${TEMP_DIR}" -mv "${TEMP_DIR}/${CEF_SYMBOLS_BASENAME}"/* "${TARGET}/Release" +mv "${TEMP_DIR}"/*_macosx_symbols/* "${TARGET}/Release" diff --git a/spec/app/edit-session-spec.coffee b/spec/app/edit-session-spec.coffee index bd4537617..3327b9f06 100644 --- a/spec/app/edit-session-spec.coffee +++ b/spec/app/edit-session-spec.coffee @@ -708,6 +708,168 @@ describe "EditSession", -> expect(editSession.selectMarker('bogus')).toBeFalsy() expect(editSession.getSelectedBufferRange()).toEqual rangeBefore + describe ".addSelectionBelow()", -> + describe "when the selection is non-empty", -> + it "selects the same region of the line below current selections if possible", -> + editSession.setSelectedBufferRange([[3, 16], [3, 21]]) + editSession.addSelectionForBufferRange([[3, 25], [3, 34]]) + editSession.addSelectionBelow() + expect(editSession.getSelectedBufferRanges()).toEqual [ + [[3, 16], [3, 21]] + [[3, 25], [3, 34]] + [[4, 16], [4, 21]] + [[4, 25], [4, 29]] + ] + for cursor in editSession.getCursors() + expect(cursor.isVisible()).toBeFalsy() + + it "skips lines that are too short to create a non-empty selection", -> + editSession.setSelectedBufferRange([[3, 31], [3, 38]]) + editSession.addSelectionBelow() + expect(editSession.getSelectedBufferRanges()).toEqual [ + [[3, 31], [3, 38]] + [[6, 31], [6, 38]] + ] + + it "honors the original selection's range (goal range) when adding across shorter lines", -> + editSession.setSelectedBufferRange([[3, 22], [3, 38]]) + editSession.addSelectionBelow() + editSession.addSelectionBelow() + editSession.addSelectionBelow() + expect(editSession.getSelectedBufferRanges()).toEqual [ + [[3, 22], [3, 38]] + [[4, 22], [4, 29]] + [[5, 22], [5, 30]] + [[6, 22], [6, 38]] + ] + + it "clears selection goal ranges when the selection changes", -> + editSession.setSelectedBufferRange([[3, 22], [3, 38]]) + editSession.addSelectionBelow() + editSession.selectLeft() + editSession.addSelectionBelow() + expect(editSession.getSelectedBufferRanges()).toEqual [ + [[3, 22], [3, 37]] + [[4, 22], [4, 29]] + [[5, 22], [5, 28]] + ] + + # goal range from previous add selection is honored next time + editSession.addSelectionBelow() + expect(editSession.getSelectedBufferRanges()).toEqual [ + [[3, 22], [3, 37]] + [[4, 22], [4, 29]] + [[5, 22], [5, 30]] # select to end of line 5 because line 4's goal range was reset by line 3 previously + [[6, 22], [6, 28]] + ] + + describe "when the selection is empty", -> + it "does not skip lines that are shorter than the current column", -> + editSession.setCursorBufferPosition([3, 36]) + editSession.addSelectionBelow() + editSession.addSelectionBelow() + editSession.addSelectionBelow() + expect(editSession.getSelectedBufferRanges()).toEqual [ + [[3, 36], [3, 36]] + [[4, 29], [4, 29]] + [[5, 30], [5, 30]] + [[6, 36], [6, 36]] + ] + + it "skips empty lines when the column is non-zero", -> + editSession.setCursorBufferPosition([9, 4]) + editSession.addSelectionBelow() + expect(editSession.getSelectedBufferRanges()).toEqual [ + [[9, 4], [9, 4]] + [[11, 4], [11, 4]] + ] + + it "does not skip empty lines when the column is zero", -> + editSession.setCursorBufferPosition([9, 0]) + editSession.addSelectionBelow() + expect(editSession.getSelectedBufferRanges()).toEqual [ + [[9, 0], [9, 0]] + [[10, 0], [10, 0]] + ] + + describe ".addSelectionAbove()", -> + describe "when the selection is non-empty", -> + it "selects the same region of the line above current selections if possible", -> + editSession.setSelectedBufferRange([[3, 16], [3, 21]]) + editSession.addSelectionForBufferRange([[3, 37], [3, 44]]) + editSession.addSelectionAbove() + expect(editSession.getSelectedBufferRanges()).toEqual [ + [[2, 16], [2, 21]] + [[2, 37], [2, 40]] + [[3, 16], [3, 21]] + [[3, 37], [3, 44]] + ] + for cursor in editSession.getCursors() + expect(cursor.isVisible()).toBeFalsy() + + it "skips lines that are too short to create a non-empty selection", -> + editSession.setSelectedBufferRange([[6, 31], [6, 38]]) + editSession.addSelectionAbove() + expect(editSession.getSelectedBufferRanges()).toEqual [ + [[3, 31], [3, 38]] + [[6, 31], [6, 38]] + ] + + it "honors the original selection's range (goal range) when adding across shorter lines", -> + editSession.setSelectedBufferRange([[6, 22], [6, 38]]) + editSession.addSelectionAbove() + editSession.addSelectionAbove() + editSession.addSelectionAbove() + expect(editSession.getSelectedBufferRanges()).toEqual [ + [[3, 22], [3, 38]] + [[4, 22], [4, 29]] + [[5, 22], [5, 30]] + [[6, 22], [6, 38]] + ] + + describe "when the selection is empty", -> + it "does not skip lines that are shorter than the current column", -> + editSession.setCursorBufferPosition([6, 36]) + editSession.addSelectionAbove() + editSession.addSelectionAbove() + editSession.addSelectionAbove() + expect(editSession.getSelectedBufferRanges()).toEqual [ + [[3, 36], [3, 36]] + [[4, 29], [4, 29]] + [[5, 30], [5, 30]] + [[6, 36], [6, 36]] + ] + + it "skips empty lines when the column is non-zero", -> + editSession.setCursorBufferPosition([11, 4]) + editSession.addSelectionAbove() + expect(editSession.getSelectedBufferRanges()).toEqual [ + [[9, 4], [9, 4]] + [[11, 4], [11, 4]] + ] + + it "does not skip empty lines when the column is zero", -> + editSession.setCursorBufferPosition([10, 0]) + editSession.addSelectionAbove() + expect(editSession.getSelectedBufferRanges()).toEqual [ + [[9, 0], [9, 0]] + [[10, 0], [10, 0]] + ] + + describe ".consolidateSelections()", -> + it "destroys all selections but the most recent, returning true if any selections were destroyed", -> + editSession.setSelectedBufferRange([[3, 16], [3, 21]]) + selection1 = editSession.getSelection() + selection2 = editSession.addSelectionForBufferRange([[3, 25], [3, 34]]) + selection3 = editSession.addSelectionForBufferRange([[8, 4], [8, 10]]) + + expect(editSession.getSelections()).toEqual [selection1, selection2, selection3] + expect(editSession.consolidateSelections()).toBeTruthy() + expect(editSession.getSelections()).toEqual [selection3] + expect(selection3.isEmpty()).toBeFalsy() + expect(editSession.consolidateSelections()).toBeFalsy() + expect(editSession.getSelections()).toEqual [selection3] + describe "when the cursor is moved while there is a selection", -> makeSelection = -> selection.setBufferRange [[1, 2], [1, 5]] diff --git a/spec/app/editor-spec.coffee b/spec/app/editor-spec.coffee index f5c7612c4..6e3de12a0 100644 --- a/spec/app/editor-spec.coffee +++ b/spec/app/editor-spec.coffee @@ -765,6 +765,18 @@ describe "Editor", -> expect(editor.getSelectionViews().length).toBe 1 expect(editor.find('.region').length).toBe 3 + describe "when a selection is added and removed before the display is updated", -> + it "does not attempt to render the selection", -> + # don't update display until we request it + jasmine.unspy(editor, 'requestDisplayUpdate') + spyOn(editor, 'requestDisplayUpdate') + + editSession = editor.activeEditSession + selection = editSession.addSelectionForBufferRange([[3, 0], [3, 4]]) + selection.destroy() + editor.updateDisplay() + expect(editor.getSelectionViews().length).toBe 1 + describe "when the selection is created with the selectAll event", -> it "does not scroll to the end of the buffer", -> editor.height(150) @@ -2460,3 +2472,19 @@ describe "Editor", -> expect(fsUtils.write).toHaveBeenCalled() expect(fsUtils.write.argsForCall[0][0]).toBe '/tmp/state' expect(typeof fsUtils.write.argsForCall[0][1]).toBe 'string' + + describe "when the escape key is pressed on the editor", -> + it "clears multiple selections if there are any, and otherwise allows other bindings to be handled", -> + keymap.bindKeys '.editor', 'escape': 'test-event' + testEventHandler = jasmine.createSpy("testEventHandler") + + editor.on 'test-event', testEventHandler + editor.activeEditSession.addSelectionForBufferRange([[3, 0], [3, 0]]) + expect(editor.activeEditSession.getSelections().length).toBe 2 + + editor.trigger(keydownEvent('escape')) + expect(editor.activeEditSession.getSelections().length).toBe 1 + expect(testEventHandler).not.toHaveBeenCalled() + + editor.trigger(keydownEvent('escape')) + expect(testEventHandler).toHaveBeenCalled() diff --git a/spec/app/file-spec.coffee b/spec/app/file-spec.coffee index 9cbd613c0..f47738081 100644 --- a/spec/app/file-spec.coffee +++ b/spec/app/file-spec.coffee @@ -54,6 +54,7 @@ describe 'File', -> waitsFor "remove event", (done) -> file.on 'removed', done it "it updates its path", -> + jasmine.unspy(window, "setTimeout") moveHandler = null moveHandler = jasmine.createSpy('moveHandler') file.on 'moved', moveHandler @@ -67,6 +68,7 @@ describe 'File', -> expect(file.getPath()).toBe newPath it "maintains 'contents-changed' events set on previous path", -> + jasmine.unspy(window, "setTimeout") moveHandler = null moveHandler = jasmine.createSpy('moveHandler') file.on 'moved', moveHandler diff --git a/spec/app/keymap-spec.coffee b/spec/app/keymap-spec.coffee index 2e6e7a534..e83f73a95 100644 --- a/spec/app/keymap-spec.coffee +++ b/spec/app/keymap-spec.coffee @@ -113,7 +113,8 @@ describe "Keymap", -> describe "when the matching selectors differ in specificity", -> it "triggers the binding for the most specific selector", -> keymap.bindKeys 'div .child-node', 'x': 'foo' - keymap.bindKeys '.command-mode .child-node', 'x': 'baz' + keymap.bindKeys '.command-mode .child-node !important', 'x': 'baz' + keymap.bindKeys '.command-mode .child-node', 'x': 'quux' keymap.bindKeys '.child-node', 'x': 'bar' fooHandler = jasmine.createSpy 'fooHandler' diff --git a/spec/app/project-spec.coffee b/spec/app/project-spec.coffee index 14e0af834..dffdd8152 100644 --- a/spec/app/project-spec.coffee +++ b/spec/app/project-spec.coffee @@ -259,6 +259,32 @@ describe "Project", -> match: 'aa' range: [[1, 3], [1, 5]] + describe "when the core.excludeVcsIgnoredPaths config is truthy", -> + [projectPath, ignoredPath] = [] + + beforeEach -> + projectPath = fsUtils.resolveOnLoadPath('fixtures/git/working-dir') + ignoredPath = fsUtils.join(projectPath, 'ignored.txt') + fsUtils.write(ignoredPath, 'this match should not be included') + + afterEach -> + fsUtils.remove(ignoredPath) if fsUtils.exists(ignoredPath) + + it "excludes ignored files", -> + project.setPath(projectPath) + config.set('core.excludeVcsIgnoredPaths', true) + paths = [] + matches = [] + waitsForPromise -> + project.scan /match/, ({path, match, range}) -> + paths.push(path) + matches.push(match) + + runs -> + expect(paths.length).toBe 0 + expect(matches.length).toBe 0 + + describe "serialization", -> it "restores the project path", -> newProject = Project.deserialize(project.serialize()) diff --git a/spec/app/text-buffer-spec.coffee b/spec/app/text-buffer-spec.coffee index 8181e2ab6..93e39af75 100644 --- a/spec/app/text-buffer-spec.coffee +++ b/spec/app/text-buffer-spec.coffee @@ -32,10 +32,12 @@ describe 'Buffer', -> expect(buffer.undoManager.undoHistory.length).toBe 0 describe "when no file exists for the path", -> - it "throws an exception", -> + it "is modified and is initially empty", -> filePath = "does-not-exist.txt" expect(fsUtils.exists(filePath)).toBeFalsy() - expect(-> project.bufferForPath(filePath)).toThrow() + buffer = project.bufferForPath(filePath) + expect(buffer.isModified()).toBeTruthy() + expect(buffer.getText()).toBe '' describe "when no path is given", -> it "creates an empty buffer", -> @@ -63,6 +65,8 @@ describe 'Buffer', -> expect(eventHandler).toHaveBeenCalledWith(bufferToChange) it "triggers a `path-changed` event when the file is moved", -> + jasmine.unspy(window, "setTimeout") + fsUtils.remove(newPath) if fsUtils.exists(newPath) fsUtils.move(path, newPath) @@ -264,18 +268,44 @@ describe 'Buffer', -> expect(modifiedHandler).toHaveBeenCalledWith(true) expect(buffer.isModified()).toBe true + it "reports the modified status changing to false after a buffer to a non-existent file is saved", -> + filePath = "/tmp/atom-tmp-file" + fsUtils.remove(filePath) if fsUtils.exists(filePath) + expect(fsUtils.exists(filePath)).toBeFalsy() + buffer.release() + buffer = project.bufferForPath(filePath) + modifiedHandler = jasmine.createSpy("modifiedHandler") + buffer.on 'modified-status-changed', modifiedHandler + + buffer.insert([0,0], "hi") + advanceClock(buffer.stoppedChangingDelay) + expect(buffer.isModified()).toBe true + modifiedHandler.reset() + + buffer.save() + expect(fsUtils.exists(filePath)).toBeTruthy() + + expect(modifiedHandler).toHaveBeenCalledWith(false) + expect(buffer.isModified()).toBe false + modifiedHandler.reset() + + buffer.insert([0, 0], 'x') + advanceClock(buffer.stoppedChangingDelay) + expect(modifiedHandler).toHaveBeenCalledWith(true) + expect(buffer.isModified()).toBe true + it "returns false for an empty buffer with no path", -> buffer.release() buffer = project.bufferForPath(null) expect(buffer.isModified()).toBeFalsy() it "returns true for a non-empty buffer with no path", -> - buffer.release() - buffer = project.bufferForPath(null) - buffer.setText('a') - expect(buffer.isModified()).toBeTruthy() - buffer.setText('\n') - expect(buffer.isModified()).toBeTruthy() + buffer.release() + buffer = project.bufferForPath(null) + buffer.setText('a') + expect(buffer.isModified()).toBeTruthy() + buffer.setText('\n') + expect(buffer.isModified()).toBeTruthy() describe ".getLines()", -> it "returns an array of lines in the text contents", -> diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee index d4dcac2cb..4414240bb 100644 --- a/spec/spec-helper.coffee +++ b/spec/spec-helper.coffee @@ -13,6 +13,7 @@ File = require 'file' Editor = require 'editor' TokenizedBuffer = require 'tokenized-buffer' fsUtils = require 'fs-utils' +pathwatcher = require 'pathwatcher' RootView = require 'root-view' Git = require 'git' requireStylesheet "jasmine" @@ -95,8 +96,8 @@ afterEach -> waits(0) # yield to ui thread to make screen update more frequently ensureNoPathSubscriptions = -> - watchedPaths = $native.getWatchedPaths() - $native.unwatchAllPaths() + watchedPaths = pathwatcher.getWatchedPaths() + pathwatcher.closeAllWatchers() if watchedPaths.length > 0 throw new Error("Leaking subscriptions for paths: " + watchedPaths.join(", ")) diff --git a/src/app/atom.coffee b/src/app/atom.coffee index debc9b02f..ada93c5d4 100644 --- a/src/app/atom.coffee +++ b/src/app/atom.coffee @@ -1,7 +1,6 @@ fsUtils = require 'fs-utils' _ = require 'underscore' Package = require 'package' -TextMatePackage = require 'text-mate-package' Theme = require 'theme' messageIdCounter = 1 @@ -127,6 +126,9 @@ _.extend atom, newWindow: (args...) -> @sendMessageToBrowserProcess('newWindow', args) + restartRendererProcess: -> + @sendMessageToBrowserProcess('restartRendererProcess') + confirm: (message, detailedMessage, buttonLabelsAndCallbacks...) -> wrapCallback = (callback) => => @dismissModal(callback) @presentModal => @@ -208,9 +210,13 @@ _.extend atom, originalSendMessageToBrowserProcess(name, data) receiveMessageFromBrowserProcess: (name, data) -> - if name is 'reply' - [messageId, callbackIndex] = data.shift() - @pendingBrowserProcessCallbacks[messageId]?[callbackIndex]?(data...) + switch name + when 'reply' + [messageId, callbackIndex] = data.shift() + @pendingBrowserProcessCallbacks[messageId]?[callbackIndex]?(data...) + when 'openPath' + path = data[0] + rootView?.open(path) setWindowState: (keyPath, value) -> windowState = @getWindowState() diff --git a/src/app/binding-set.coffee b/src/app/binding-set.coffee index e9017d1f9..9fadc8756 100644 --- a/src/app/binding-set.coffee +++ b/src/app/binding-set.coffee @@ -16,9 +16,10 @@ class BindingSet parser: null name: null - constructor: (@selector, commandsByKeystrokes, @index, @name) -> + constructor: (selector, commandsByKeystrokes, @index, @name) -> BindingSet.parser ?= PEG.buildParser(fsUtils.read(require.resolve 'keystroke-pattern.pegjs')) - @specificity = Specificity(@selector) + @specificity = Specificity(selector) + @selector = selector.replace(/!important/g, '') @commandsByKeystrokes = @normalizeCommandsByKeystrokes(commandsByKeystrokes) commandForEvent: (event) -> diff --git a/src/app/buffer-marker.coffee b/src/app/buffer-marker.coffee index 199171595..ebc843546 100644 --- a/src/app/buffer-marker.coffee +++ b/src/app/buffer-marker.coffee @@ -27,6 +27,9 @@ class BufferMarker isReversed: -> @tailPosition? and @headPosition.isLessThan(@tailPosition) + isRangeEmpty: -> + @getHeadPosition().isEqual(@getTailPosition()) + getRange: -> if @tailPosition new Range(@tailPosition, @headPosition) diff --git a/src/app/cursor.coffee b/src/app/cursor.coffee index 8f1a21d67..52760c63b 100644 --- a/src/app/cursor.coffee +++ b/src/app/cursor.coffee @@ -12,9 +12,9 @@ class Cursor needsAutoscroll: null constructor: ({@editSession, @marker}) -> + @updateVisibility() @editSession.observeMarker @marker, (e) => - @setVisible(@selection.isEmpty()) - + @updateVisibility() {oldHeadScreenPosition, newHeadScreenPosition} = e {oldHeadBufferPosition, newHeadBufferPosition} = e {bufferChanged} = e @@ -34,6 +34,7 @@ class Cursor @needsAutoscroll = true destroy: -> + @destroyed = true @editSession.destroyMarker(@marker) @editSession.removeCursor(this) @trigger 'destroyed' @@ -59,6 +60,9 @@ class Cursor unless fn() @trigger 'autoscrolled' if @needsAutoscroll + updateVisibility: -> + @setVisible(@editSession.isMarkerRangeEmpty(@marker)) + setVisible: (visible) -> if @visible != visible @visible = visible @@ -84,6 +88,7 @@ class Cursor clearSelection: -> if @selection + @selection.goalBufferRange = null @selection.clear() unless @selection.retainSelection getScreenRow: -> diff --git a/src/app/directory.coffee b/src/app/directory.coffee index d73612fb0..aadc75a94 100644 --- a/src/app/directory.coffee +++ b/src/app/directory.coffee @@ -1,6 +1,7 @@ _ = require 'underscore' fs = require 'fs' fsUtils = require 'fs-utils' +pathWatcher = require 'pathwatcher' File = require 'file' EventEmitter = require 'event-emitter' @@ -39,12 +40,12 @@ class Directory @unsubscribeFromNativeChangeEvents() if @subscriptionCount() == 0 subscribeToNativeChangeEvents: -> - @watchSubscription = fsUtils.watchPath @path, (eventType) => - @trigger "contents-changed" if eventType is "contents-change" + @watchSubscription = pathWatcher.watch @path, (eventType) => + @trigger "contents-changed" if eventType is "change" unsubscribeFromNativeChangeEvents: -> if @watchSubscription? - @watchSubscription.unwatch() + @watchSubscription.close() @watchSubscription = null _.extend Directory.prototype, EventEmitter diff --git a/src/app/display-buffer.coffee b/src/app/display-buffer.coffee index 0f24a235d..37aba8bff 100644 --- a/src/app/display-buffer.coffee +++ b/src/app/display-buffer.coffee @@ -400,6 +400,9 @@ class DisplayBuffer isMarkerReversed: (id) -> @buffer.isMarkerReversed(id) + isMarkerRangeEmpty: (id) -> + @buffer.isMarkerRangeEmpty(id) + observeMarker: (id, callback) -> @getMarker(id).observe(callback) diff --git a/src/app/edit-session.coffee b/src/app/edit-session.coffee index a17c914f0..e3365b9bf 100644 --- a/src/app/edit-session.coffee +++ b/src/app/edit-session.coffee @@ -125,8 +125,8 @@ class EditSession getTabLength: -> @displayBuffer.getTabLength() setTabLength: (tabLength) -> @displayBuffer.setTabLength(tabLength) - clipBufferPosition: (bufferPosition) -> - @buffer.clipPosition(bufferPosition) + clipBufferPosition: (bufferPosition) -> @buffer.clipPosition(bufferPosition) + clipBufferRange: (range) -> @buffer.clipRange(range) indentationForBufferRow: (bufferRow) -> @indentLevelForLine(@lineForBufferRow(bufferRow)) @@ -556,6 +556,9 @@ class EditSession isMarkerReversed: (args...) -> @displayBuffer.isMarkerReversed(args...) + isMarkerRangeEmpty: (args...) -> + @displayBuffer.isMarkerRangeEmpty(args...) + hasMultipleCursors: -> @getCursors().length > 1 @@ -585,10 +588,10 @@ class EditSession unless options.preserveFolds @destroyFoldsIntersectingBufferRange(@getMarkerBufferRange(marker)) cursor = @addCursor(marker) - selection = new Selection({editSession: this, marker, cursor}) + selection = new Selection(_.extend({editSession: this, marker, cursor}, options)) @selections.push(selection) selectionBufferRange = selection.getBufferRange() - @mergeIntersectingSelections() + @mergeIntersectingSelections() unless options.suppressMerge if selection.destroyed for selection in @getSelections() if selection.intersectsBufferRange(selectionBufferRange) @@ -600,7 +603,7 @@ class EditSession addSelectionForBufferRange: (bufferRange, options={}) -> options = _.defaults({invalidationStrategy: 'never'}, options) marker = @markBufferRange(bufferRange, options) - @addSelection(marker) + @addSelection(marker, options) setSelectedBufferRange: (bufferRange, options) -> @setSelectedBufferRanges([bufferRange], options) @@ -623,13 +626,16 @@ class EditSession _.remove(@selections, selection) clearSelections: -> - lastSelection = @getLastSelection() - for selection in @getSelections() when selection != lastSelection - selection.destroy() - lastSelection.clear() + @consolidateSelections() + @getSelection().clear() - clearAllSelections: -> - selection.destroy() for selection in @getSelections() + consolidateSelections: -> + selections = @getSelections() + if selections.length > 1 + selection.destroy() for selection in selections[0...-1] + true + else + false getSelections: -> new Array(@selections...) @@ -761,6 +767,12 @@ class EditSession selectLine: -> @expandSelectionsForward (selection) => selection.selectLine() + addSelectionBelow: -> + @expandSelectionsForward (selection) => selection.addSelectionBelow() + + addSelectionAbove: -> + @expandSelectionsBackward (selection) => selection.addSelectionAbove() + transpose: -> @mutateSelectedText (selection) => if selection.isEmpty() diff --git a/src/app/editor.coffee b/src/app/editor.coffee index fa5aeb783..ff93dccf0 100644 --- a/src/app/editor.coffee +++ b/src/app/editor.coffee @@ -60,7 +60,7 @@ class Editor extends View if editSessionOrOptions instanceof EditSession editSession = editSessionOrOptions else - {editSession, @mini} = (editSessionOrOptions ? {}) + {editSession, @mini} = editSessionOrOptions ? {} requireStylesheet 'editor' @@ -104,6 +104,7 @@ class Editor extends View 'editor:move-to-previous-word': @moveCursorToPreviousWord 'editor:select-word': @selectWord 'editor:newline': @insertNewline + 'editor:consolidate-selections': @consolidateSelections 'editor:indent': @indent 'editor:auto-indent': @autoIndent 'editor:indent-selected-rows': @indentSelectedRows @@ -124,6 +125,8 @@ class Editor extends View 'editor:select-to-end-of-word': @selectToEndOfWord 'editor:select-to-beginning-of-word': @selectToBeginningOfWord 'editor:select-to-beginning-of-next-word': @selectToBeginningOfNextWord + 'editor:add-selection-below': @addSelectionBelow + 'editor:add-selection-above': @addSelectionAbove 'editor:select-line': @selectLine 'editor:transpose': @transpose 'editor:upper-case': @upperCase @@ -165,7 +168,7 @@ class Editor extends View documentation = {} for name, method of editorBindings do (name, method) => - @command name, => method.call(this); false + @command name, (e) => method.call(this, e); false getCursor: -> @activeEditSession.getCursor() getCursors: -> @activeEditSession.getCursors() @@ -214,6 +217,8 @@ class Editor extends View selectAll: -> @activeEditSession.selectAll() selectToBeginningOfLine: -> @activeEditSession.selectToBeginningOfLine() selectToEndOfLine: -> @activeEditSession.selectToEndOfLine() + addSelectionBelow: -> @activeEditSession.addSelectionBelow() + addSelectionAbove: -> @activeEditSession.addSelectionAbove() selectToBeginningOfWord: -> @activeEditSession.selectToBeginningOfWord() selectToEndOfWord: -> @activeEditSession.selectToEndOfWord() selectToBeginningOfNextWord: -> @activeEditSession.selectToEndOfWord(); @activeEditSession.selectRight() @@ -234,6 +239,7 @@ class Editor extends View cutToEndOfLine: -> @activeEditSession.cutToEndOfLine() insertText: (text, options) -> @activeEditSession.insertText(text, options) insertNewline: -> @activeEditSession.insertNewline() + consolidateSelections: (e) -> e.abortKeyBinding() unless @activeEditSession.consolidateSelections() insertNewlineBelow: -> @activeEditSession.insertNewlineBelow() insertNewlineAbove: -> @activeEditSession.insertNewlineAbove() indent: (options) -> @activeEditSession.indent(options) @@ -782,7 +788,7 @@ class Editor extends View updateCursorViews: -> if @newCursors.length > 0 - @addCursorView(cursor) for cursor in @newCursors + @addCursorView(cursor) for cursor in @newCursors when not cursor.destroyed @syncCursorAnimations() @newCursors = [] @@ -794,11 +800,11 @@ class Editor extends View updateSelectionViews: -> if @newSelections.length > 0 - @addSelectionView(selection) for selection in @newSelections + @addSelectionView(selection) for selection in @newSelections when not selection.destroyed @newSelections = [] for selectionView in @getSelectionViews() - if selectionView.destroyed + if selectionView.needsRemoval selectionView.remove() else selectionView.updateDisplay() diff --git a/src/app/file.coffee b/src/app/file.coffee index f982223b5..ec06ef189 100644 --- a/src/app/file.coffee +++ b/src/app/file.coffee @@ -2,6 +2,7 @@ EventEmitter = require 'event-emitter' fs = require 'fs' fsUtils = require 'fs-utils' +pathWatcher = require 'pathwatcher' _ = require 'underscore' module.exports = @@ -45,12 +46,13 @@ class File @unsubscribeFromNativeChangeEvents() if @subscriptionCount() == 0 handleNativeChangeEvent: (eventType, path) -> - if eventType is "remove" + if eventType is "delete" + @unsubscribeFromNativeChangeEvents() @detectResurrectionAfterDelay() - else if eventType is "move" + else if eventType is "rename" @setPath(path) @trigger "moved" - else if eventType is "contents-change" + else if eventType is "change" oldContents = @read() newContents = @read(true) return if oldContents == newContents @@ -62,19 +64,18 @@ class File detectResurrection: -> if @exists() @subscribeToNativeChangeEvents() - @handleNativeChangeEvent("contents-change", @getPath()) + @handleNativeChangeEvent("change", @getPath()) else @cachedContents = null - @unsubscribeFromNativeChangeEvents() @trigger "removed" subscribeToNativeChangeEvents: -> - @watchSubscription = fsUtils.watchPath @path, (eventType, path) => + @watchSubscription = pathWatcher.watch @path, (eventType, path) => @handleNativeChangeEvent(eventType, path) unsubscribeFromNativeChangeEvents: -> if @watchSubscription - @watchSubscription.unwatch() + @watchSubscription.close() @watchSubscription = null _.extend File.prototype, EventEmitter diff --git a/src/app/keymaps/editor.cson b/src/app/keymaps/editor.cson index 52522ce73..335be5587 100644 --- a/src/app/keymaps/editor.cson +++ b/src/app/keymaps/editor.cson @@ -9,6 +9,8 @@ 'ctrl-]': 'editor:unfold-current-row' 'ctrl-{': 'editor:fold-all' 'ctrl-}': 'editor:unfold-all' + 'alt-shift-down': 'editor:add-selection-below' + 'alt-shift-up': 'editor:add-selection-above' 'alt-meta-ctrl-f': 'editor:fold-selection' 'shift-tab': 'editor:outdent-selected-rows' 'meta-[': 'editor:outdent-selected-rows' @@ -30,3 +32,6 @@ 'enter': 'core:confirm', 'escape': 'core:cancel' 'meta-w': 'core:cancel' + +'.editor !important, .editor.mini !important': + 'escape': 'editor:consolidate-selections' diff --git a/src/app/keymaps/emacs.cson b/src/app/keymaps/emacs.cson index 108ac95cf..4d6caf192 100644 --- a/src/app/keymaps/emacs.cson +++ b/src/app/keymaps/emacs.cson @@ -7,6 +7,8 @@ 'ctrl-N': 'core:select-down' 'ctrl-F': 'core:select-right' 'ctrl-B': 'core:select-left' + 'alt-ctrl-n': 'editor:add-selection-below' + 'alt-ctrl-p': 'editor:add-selection-above' 'ctrl-h': 'core:backspace' 'ctrl-d': 'core:delete' diff --git a/src/app/root-view.coffee b/src/app/root-view.coffee index a8a2ada13..6b4bf73bb 100644 --- a/src/app/root-view.coffee +++ b/src/app/root-view.coffee @@ -97,11 +97,8 @@ class RootView extends View changeFocus = options.changeFocus ? true path = project.resolve(path) if path? if activePane = @getActivePane() - if editSession = activePane.itemForUri(path) - activePane.showItem(editSession) - else - editSession = project.buildEditSession(path) - activePane.showItem(editSession) + editSession = activePane.itemForUri(path) ? project.buildEditSession(path) + activePane.showItem(editSession) else editSession = project.buildEditSession(path) activePane = new Pane(editSession) diff --git a/src/app/selection-view.coffee b/src/app/selection-view.coffee index 1fb43c18a..4b66e3d97 100644 --- a/src/app/selection-view.coffee +++ b/src/app/selection-view.coffee @@ -8,13 +8,13 @@ class SelectionView extends View @div class: 'selection' regions: null - destroyed: false + needsRemoval: false initialize: ({@editor, @selection} = {}) -> @regions = [] @selection.on 'screen-range-changed', => @editor.requestDisplayUpdate() @selection.on 'destroyed', => - @destroyed = true + @needsRemoval = true @editor.requestDisplayUpdate() updateDisplay: -> diff --git a/src/app/selection.coffee b/src/app/selection.coffee index 817769281..03bf8aca4 100644 --- a/src/app/selection.coffee +++ b/src/app/selection.coffee @@ -4,11 +4,15 @@ _ = require 'underscore' module.exports = class Selection - wordwise: false + cursor: null + marker: null + editSession: null initialScreenRange: null + goalBufferRange: null + wordwise: false needsAutoscroll: null - constructor: ({@cursor, @marker, @editSession}) -> + constructor: ({@cursor, @marker, @editSession, @goalBufferRange}) -> @cursor.selection = this @editSession.observeMarker @marker, => @screenRangeChanged() @cursor.on 'destroyed.selection', => @@ -148,6 +152,40 @@ class Selection selectToEndOfWord: -> @modifySelection => @cursor.moveToEndOfWord() + addSelectionBelow: -> + range = (@goalBufferRange ? @getBufferRange()).copy() + nextRow = range.end.row + 1 + + for row in [nextRow..@editSession.getLastBufferRow()] + range.start.row = row + range.end.row = row + clippedRange = @editSession.clipBufferRange(range) + + if range.isEmpty() + continue if range.end.column > 0 and clippedRange.end.column is 0 + else + continue if clippedRange.isEmpty() + + @editSession.addSelectionForBufferRange(range, goalBufferRange: range, suppressMerge: true) + break + + addSelectionAbove: -> + range = (@goalBufferRange ? @getBufferRange()).copy() + previousRow = range.end.row - 1 + + for row in [previousRow..0] + range.start.row = row + range.end.row = row + clippedRange = @editSession.clipBufferRange(range) + + if range.isEmpty() + continue if range.end.column > 0 and clippedRange.end.column is 0 + else + continue if clippedRange.isEmpty() + + @editSession.addSelectionForBufferRange(range, goalBufferRange: range, suppressMerge: true) + break + insertText: (text, options={}) -> oldBufferRange = @getBufferRange() @editSession.destroyFoldsContainingBufferRow(oldBufferRange.end.row) @@ -369,10 +407,14 @@ class Selection @getBufferRange().intersectsWith(bufferRange) intersectsWith: (otherSelection) -> - @getScreenRange().intersectsWith(otherSelection.getScreenRange()) + @getBufferRange().intersectsWith(otherSelection.getBufferRange()) merge: (otherSelection, options) -> @setBufferRange(@getBufferRange().union(otherSelection.getBufferRange()), options) + if @goalBufferRange and otherSelection.goalBufferRange + @goalBufferRange = @goalBufferRange.union(otherSelection.goalBufferRange) + else if otherSelection.goalBufferRange + @goalBufferRange = otherSelection.goalBufferRange otherSelection.destroy() _.extend Selection.prototype, EventEmitter diff --git a/src/app/text-buffer.coffee b/src/app/text-buffer.coffee index ea6116a12..7542d4d01 100644 --- a/src/app/text-buffer.coffee +++ b/src/app/text-buffer.coffee @@ -37,17 +37,17 @@ class Buffer @lineEndings = [] if path - throw "Path '#{path}' does not exist" unless fsUtils.exists(path) @setPath(path) if initialText? @setText(initialText) @updateCachedDiskContents() - else + else if fsUtils.exists(path) @reload() + else + @setText('') else @setText(initialText ? '') - @undoManager = new UndoManager(this) destroy: -> @@ -340,6 +340,9 @@ class Buffer isMarkerReversed: (id) -> @validMarkers[id]?.isReversed() + isMarkerRangeEmpty: (id) -> + @validMarkers[id]?.isRangeEmpty() + observeMarker: (id, callback) -> @validMarkers[id]?.observe(callback) diff --git a/src/app/window.coffee b/src/app/window.coffee index 272f91029..d78f54caa 100644 --- a/src/app/window.coffee +++ b/src/app/window.coffee @@ -25,6 +25,12 @@ window.setUpEnvironment = -> $(document).on 'keydown', keymap.handleKeyEvent keymap.bindDefaultKeys() + ignoreEvents = (e) -> + e.preventDefault() + e.stopPropagation() + $(document).on 'dragover', ignoreEvents + $(document).on 'drop', ignoreEvents + requireStylesheet 'reset' requireStylesheet 'atom' requireStylesheet 'overlay' @@ -148,7 +154,16 @@ window.applyStylesheet = (id, text, ttype = 'bundled') -> $("head").append "" window.reload = -> - $native.reload() + timesReloaded = process.global.timesReloaded ? 0 + ++timesReloaded + + restartValue = if window.location.search.indexOf('spec-bootstrap') == -1 then 10 else 3 + + if timesReloaded > restartValue + atom.restartRendererProcess() + else + $native.reload() + process.global.timesReloaded = timesReloaded window.onerror = -> atom.showDevTools() diff --git a/src/packages/bracket-matcher/lib/bracket-matcher.coffee b/src/packages/bracket-matcher/lib/bracket-matcher.coffee index 4d80300fa..5fba6b280 100644 --- a/src/packages/bracket-matcher/lib/bracket-matcher.coffee +++ b/src/packages/bracket-matcher/lib/bracket-matcher.coffee @@ -42,6 +42,7 @@ module.exports = goToMatchingPair: (editor) -> return unless @pairHighlighted return unless underlayer = editor.getPane()?.find('.underlayer') + return unless underlayer.isVisible() position = editor.getCursorBufferPosition() previousPosition = position.translate([0, -1]) diff --git a/src/packages/gfm/grammars/gfm.cson b/src/packages/gfm/grammars/gfm.cson index be815f5ca..ff77790d0 100644 --- a/src/packages/gfm/grammars/gfm.cson +++ b/src/packages/gfm/grammars/gfm.cson @@ -34,8 +34,12 @@ 'name': 'markup.heading.gfm' } { - 'match': '\\:[^\\:\\s]+\\:' + 'match': '(\\:)([^\\:\\s]+)(\\:)' 'name': 'string.emoji.gfm' + 'captures': + '1': 'name': 'string.emoji.start.gfm' + '2': 'name': 'string.emoji.word.gfm' + '3': 'name': 'string.emoji.end.gfm' } { 'match': '^\\s*[\\*]{3,}\\s*$' diff --git a/src/packages/gfm/spec/gfm-spec.coffee b/src/packages/gfm/spec/gfm-spec.coffee index 3ca1b3a79..39f70236a 100644 --- a/src/packages/gfm/spec/gfm-spec.coffee +++ b/src/packages/gfm/spec/gfm-spec.coffee @@ -78,7 +78,9 @@ describe "GitHub Flavored Markdown grammar", -> it "tokenizies an :emoji:", -> {tokens} = grammar.tokenizeLine("this is :no_good:") expect(tokens[0]).toEqual value: "this is ", scopes: ["source.gfm"] - expect(tokens[1]).toEqual value: ":no_good:", scopes: ["source.gfm", "string.emoji.gfm"] + expect(tokens[1]).toEqual value: ":", scopes: ["source.gfm", "string.emoji.gfm", "string.emoji.start.gfm"] + expect(tokens[2]).toEqual value: "no_good", scopes: ["source.gfm", "string.emoji.gfm", "string.emoji.word.gfm"] + expect(tokens[3]).toEqual value: ":", scopes: ["source.gfm", "string.emoji.gfm", "string.emoji.end.gfm"] {tokens} = grammar.tokenizeLine("this is :no good:") expect(tokens[0]).toEqual value: "this is :no good:", scopes: ["source.gfm"] diff --git a/src/packages/spell-check/lib/spell-check-view.coffee b/src/packages/spell-check/lib/spell-check-view.coffee index 323d0e873..3f5bba2d6 100644 --- a/src/packages/spell-check/lib/spell-check-view.coffee +++ b/src/packages/spell-check/lib/spell-check-view.coffee @@ -18,7 +18,7 @@ class SpellCheckView extends View @subscribeToBuffer() - subscribeToBuffer: -> + unsubscribeFromBuffer: -> @destroyViews() @task?.abort() @@ -26,6 +26,9 @@ class SpellCheckView extends View @buffer.off '.spell-check' @buffer = null + subscribeToBuffer: -> + @unsubscribeFromBuffer() + if @spellCheckCurrentGrammar() @buffer = @editor.getBuffer() @buffer.on 'contents-modified.spell-check', => @updateMisspellings() @@ -47,6 +50,10 @@ class SpellCheckView extends View @append(view) updateMisspellings: -> + unless @editor.activeEditSession? + @unsubscribeFromBuffer() + return + @task?.abort() callback = (misspellings) => diff --git a/src/packages/tabs/lib/tab-bar-view.coffee b/src/packages/tabs/lib/tab-bar-view.coffee index 926e5c3ae..ea8abd24b 100644 --- a/src/packages/tabs/lib/tab-bar-view.coffee +++ b/src/packages/tabs/lib/tab-bar-view.coffee @@ -81,6 +81,8 @@ class TabBarView extends View event.preventDefault() return + event.originalEvent.dataTransfer.setData 'atom-event', 'true' + el = $(event.target).closest('.sortable') el.addClass 'is-dragging' event.originalEvent.dataTransfer.setData 'sortable-index', el.index() @@ -93,6 +95,11 @@ class TabBarView extends View @find(".is-dragging").removeClass 'is-dragging' onDragOver: (event) => + unless event.originalEvent.dataTransfer.getData('atom-event') is 'true' + event.preventDefault() + event.stopPropagation() + return + event.preventDefault() currentDropTargetIndex = @find(".is-drop-target").index() newDropTargetIndex = @getDropTargetIndex(event) @@ -107,6 +114,11 @@ class TabBarView extends View onDrop: (event) => + unless event.originalEvent.dataTransfer.getData('atom-event') is 'true' + event.preventDefault() + event.stopPropagation() + return + event.stopPropagation() @children('.is-drop-target').removeClass 'is-drop-target' @children('.drop-target-is-after').removeClass 'drop-target-is-after' diff --git a/src/packages/tabs/spec/tabs-spec.coffee b/src/packages/tabs/spec/tabs-spec.coffee index 7eb59d232..b5b733c17 100644 --- a/src/packages/tabs/spec/tabs-spec.coffee +++ b/src/packages/tabs/spec/tabs-spec.coffee @@ -278,3 +278,19 @@ describe "TabBarView", -> expect(pane2.getItems()).toEqual [item2b, item1] expect(pane2.activeItem).toBe item1 expect(pane2.focus).toHaveBeenCalled() + + describe 'when a non-tab is dragged to pane', -> + it 'has no effect', -> + expect(tabBar.getTabs().map (tab) -> tab.text()).toEqual ["Item 1", "sample.js", "Item 2"] + expect(pane.getItems()).toEqual [item1, editSession1, item2] + expect(pane.activeItem).toBe item2 + spyOn(pane, 'focus') + + [dragStartEvent, dropEvent] = buildDragEvents(tabBar.tabAtIndex(0), tabBar.tabAtIndex(0)) + tabBar.onDrop(dropEvent) + + expect(tabBar.getTabs().map (tab) -> tab.text()).toEqual ["Item 1", "sample.js", "Item 2"] + expect(pane.getItems()).toEqual [item1, editSession1, item2] + expect(pane.activeItem).toBe item2 + expect(pane.focus).not.toHaveBeenCalled() + diff --git a/src/packages/tree-view/lib/directory-view.coffee b/src/packages/tree-view/lib/directory-view.coffee index 6120d9c8e..8d58bcfb2 100644 --- a/src/packages/tree-view/lib/directory-view.coffee +++ b/src/packages/tree-view/lib/directory-view.coffee @@ -30,7 +30,7 @@ class DirectoryView extends View iconClass = 'submodule-icon' else @subscribe git, 'status-changed', (path, status) => - @updateStatus() if path.substring("#{@getPath()}/") is 0 + @updateStatus() if path.indexOf("#{@getPath()}/") is 0 @subscribe git, 'statuses-changed', => @updateStatus() @updateStatus() diff --git a/src/stdlib/fs-utils.coffee b/src/stdlib/fs-utils.coffee index a422f1e10..8fc11d36a 100644 --- a/src/stdlib/fs-utils.coffee +++ b/src/stdlib/fs-utils.coffee @@ -312,11 +312,3 @@ module.exports = cson.readObjectAsync(path, done) else @readPlistAsync(path, done) - - watchPath: (path, callback) -> - path = @absolute(path) - watchCallback = (eventType, eventPath) => - path = @absolute(eventPath) if eventType is 'move' - callback(arguments...) - id = $native.watchPath(path, watchCallback) - unwatch: -> $native.unwatchPath(path, id) diff --git a/src/stdlib/jquery-extensions.coffee b/src/stdlib/jquery-extensions.coffee index 7a62a988c..74ca342cc 100644 --- a/src/stdlib/jquery-extensions.coffee +++ b/src/stdlib/jquery-extensions.coffee @@ -34,6 +34,9 @@ $.fn.pageDown = -> $.fn.isOnDom = -> @closest(document.body).length is 1 +$.fn.isVisible = -> + @is(':visible') + $.fn.containsElement = (element) -> (element[0].compareDocumentPosition(this[0]) & 8) == 8 diff --git a/themes/atom-dark-ui/tree-view.less b/themes/atom-dark-ui/tree-view.less index f1d8a9796..0ba227929 100644 --- a/themes/atom-dark-ui/tree-view.less +++ b/themes/atom-dark-ui/tree-view.less @@ -11,7 +11,8 @@ } .tree-view .directory.selected > .header > .name, -.tree-view .selected > .name { +.tree-view .selected > .name, +.tree-view .selected > .header > .disclosure-arrow { color: #d2d2d2; } @@ -39,7 +40,9 @@ .tree-view .entry:hover, .tree-view .directory .header:hover .name, -.tree-view .directory .header:hover .disclosure-arrow { +.tree-view .directory .header:hover .disclosure-arrow, +.tree-view .selected > .directory > .header .disclosure-arrow, +.tree-view .selected > .directory > .header:hover .disclosure-arrow { color: #ebebeb; }