From f455379a1747b79a043278aecc52c5dc994882fe Mon Sep 17 00:00:00 2001 From: Cheng Zhao Date: Thu, 4 Apr 2013 15:31:06 +0800 Subject: [PATCH 01/49] No drag-drop by default. --- src/app/window.coffee | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/app/window.coffee b/src/app/window.coffee index 272f91029..d2d70894c 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' From f37265978ef5102e066c265fb8d691ea6d9a11b8 Mon Sep 17 00:00:00 2001 From: Cheng Zhao Date: Thu, 4 Apr 2013 16:23:46 +0800 Subject: [PATCH 02/49] Only respond to drag-drop of tabs in tab bar. --- src/packages/tabs/lib/tab-bar-view.coffee | 12 ++++++++++++ src/packages/tabs/spec/tabs-spec.coffee | 16 ++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/src/packages/tabs/lib/tab-bar-view.coffee b/src/packages/tabs/lib/tab-bar-view.coffee index 926e5c3ae..2cafac5d4 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') == 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') == 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() + From a048d2994e2a24c05070e7caafc3fe7c8871ad86 Mon Sep 17 00:00:00 2001 From: probablycorey Date: Thu, 4 Apr 2013 12:22:22 -0700 Subject: [PATCH 03/49] Use [ -t 1 ] to check if we should output progress bar. --- script/cibuild | 2 +- script/update-cefode | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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..d1f31a41f 100755 --- a/script/update-cefode +++ b/script/update-cefode @@ -30,7 +30,7 @@ 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 + if [ -t 1 ] ; then # If run from the terminal CURL_ARGS="--progress-bar" else CURL_ARGS="-fsS" From bb2ab15753d866180f8288f55d7e1a1a5922b1ac Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Thu, 4 Apr 2013 13:32:06 -0700 Subject: [PATCH 04/49] Open files specified via CLI in existing window Activate the window and open an editor when a path is specified that is already present in an existing window's project. Closes #357 --- native/atom_application.mm | 34 ++++++++++++++++++++++++++------ native/atom_window_controller.h | 1 + native/atom_window_controller.mm | 10 ++++++++++ src/app/atom.coffee | 10 +++++++--- 4 files changed, 46 insertions(+), 9 deletions(-) diff --git a/native/atom_application.mm b/native/atom_application.mm index ab26df02d..55d7f971d 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; + } } } } 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..5650637ec 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]; } diff --git a/src/app/atom.coffee b/src/app/atom.coffee index debc9b02f..938539cad 100644 --- a/src/app/atom.coffee +++ b/src/app/atom.coffee @@ -208,9 +208,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) if fsUtils.isFile(path) setWindowState: (keyPath, value) -> windowState = @getWindowState() From 6166f8e68140cbb9b65b4968c8ac6bc3250d56dc Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Thu, 4 Apr 2013 13:40:28 -0700 Subject: [PATCH 05/49] Remove unused require --- src/app/atom.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/atom.coffee b/src/app/atom.coffee index 938539cad..06155e10a 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 From 062d07ada0baf51f257dad5601af8b8021f77217 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Thu, 4 Apr 2013 15:52:53 -0700 Subject: [PATCH 06/49] :lipstick: --- native/atom_cef_render_process_handler.mm | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/native/atom_cef_render_process_handler.mm b/native/atom_cef_render_process_handler.mm index b3147478c..47af29dae 100644 --- a/native/atom_cef_render_process_handler.mm +++ b/native/atom_cef_render_process_handler.mm @@ -55,14 +55,14 @@ void AtomCefRenderProcessHandler::Reload(CefRefPtr browser) { } void AtomCefRenderProcessHandler::Shutdown(CefRefPtr browser) { - CefRefPtr context = browser->GetMainFrame()->GetV8Context(); - CefRefPtr global = context->GetGlobal(); + CefRefPtr context = browser->GetMainFrame()->GetV8Context(); + CefRefPtr global = context->GetGlobal(); - context->Enter(); - CefV8ValueList arguments; - CefRefPtr shutdownFunction = global->GetValue("shutdown"); - shutdownFunction->ExecuteFunction(global, arguments); - context->Exit(); + context->Enter(); + CefV8ValueList arguments; + CefRefPtr shutdownFunction = global->GetValue("shutdown"); + shutdownFunction->ExecuteFunction(global, arguments); + context->Exit(); } bool AtomCefRenderProcessHandler::CallMessageReceivedHandler(CefRefPtr context, CefRefPtr message) { From 8745ef87236b027cab51f7abdfdc39ad02782709 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Thu, 4 Apr 2013 16:29:15 -0700 Subject: [PATCH 07/49] :lipstick: --- src/app/root-view.coffee | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) 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) From 931ae677c19ff54d3884465bf73453f7ffc1ab10 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Thu, 4 Apr 2013 16:37:52 -0700 Subject: [PATCH 08/49] :lipstick: --- src/app/text-buffer.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/text-buffer.coffee b/src/app/text-buffer.coffee index d403c44fd..34c428a0b 100644 --- a/src/app/text-buffer.coffee +++ b/src/app/text-buffer.coffee @@ -47,7 +47,6 @@ class Buffer else @setText(initialText ? '') - @undoManager = new UndoManager(this) destroy: -> From 84107317bb5f7a4ae5a1c276210772c5ef7c8d71 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Thu, 4 Apr 2013 17:08:05 -0700 Subject: [PATCH 09/49] Support opening non-existent files from the CLI This required changing text-buffer to support having a path but not underlying file that exists yet. Now calling RootView.open() with a non-existed path will open a dirty empty editor to the path and the file will be created on first save. --- spec/app/text-buffer-spec.coffee | 44 ++++++++++++++++++++++++++------ src/app/atom.coffee | 2 +- src/app/text-buffer.coffee | 5 ++-- 3 files changed, 40 insertions(+), 11 deletions(-) diff --git a/spec/app/text-buffer-spec.coffee b/spec/app/text-buffer-spec.coffee index 8181e2ab6..2683cc7a7 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", -> @@ -264,18 +266,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/src/app/atom.coffee b/src/app/atom.coffee index 06155e10a..7807af1c3 100644 --- a/src/app/atom.coffee +++ b/src/app/atom.coffee @@ -213,7 +213,7 @@ _.extend atom, @pendingBrowserProcessCallbacks[messageId]?[callbackIndex]?(data...) when 'openPath' path = data[0] - rootView?.open(path) if fsUtils.isFile(path) + rootView?.open(path) setWindowState: (keyPath, value) -> windowState = @getWindowState() diff --git a/src/app/text-buffer.coffee b/src/app/text-buffer.coffee index 34c428a0b..7fa3bb32a 100644 --- a/src/app/text-buffer.coffee +++ b/src/app/text-buffer.coffee @@ -37,13 +37,14 @@ 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 ? '') From 33de90a0b6af72f9e315fae51aebdccb80a2da71 Mon Sep 17 00:00:00 2001 From: Cheng Zhao Date: Fri, 5 Apr 2013 10:30:18 +0800 Subject: [PATCH 10/49] dataTransfer.setData only accepts string. --- src/packages/tabs/lib/tab-bar-view.coffee | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/packages/tabs/lib/tab-bar-view.coffee b/src/packages/tabs/lib/tab-bar-view.coffee index 2cafac5d4..ea8abd24b 100644 --- a/src/packages/tabs/lib/tab-bar-view.coffee +++ b/src/packages/tabs/lib/tab-bar-view.coffee @@ -81,7 +81,7 @@ class TabBarView extends View event.preventDefault() return - event.originalEvent.dataTransfer.setData 'atom-event', true + event.originalEvent.dataTransfer.setData 'atom-event', 'true' el = $(event.target).closest('.sortable') el.addClass 'is-dragging' @@ -95,7 +95,7 @@ class TabBarView extends View @find(".is-dragging").removeClass 'is-dragging' onDragOver: (event) => - unless event.originalEvent.dataTransfer.getData('atom-event') == true + unless event.originalEvent.dataTransfer.getData('atom-event') is 'true' event.preventDefault() event.stopPropagation() return @@ -114,7 +114,7 @@ class TabBarView extends View onDrop: (event) => - unless event.originalEvent.dataTransfer.getData('atom-event') == true + unless event.originalEvent.dataTransfer.getData('atom-event') is 'true' event.preventDefault() event.stopPropagation() return From 1fcc0adf6cc66d51f683dc3d6df4284d396c5fcc Mon Sep 17 00:00:00 2001 From: Cheng Zhao Date: Fri, 5 Apr 2013 22:15:24 +0800 Subject: [PATCH 11/49] Update cefode2 to cefode3. --- script/update-cefode | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/script/update-cefode b/script/update-cefode index d1f31a41f..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,7 +27,7 @@ fi CURRENT_VERSION=`cat cef/version 2>&1` if [[ $LATEST_VERSION != $CURRENT_VERSION ]]; then - echo "Downloading/extracting cefode2 u${LATEST_VERSION}..." + echo "Downloading/extracting cefode3 u${LATEST_VERSION}..." if [ -t 1 ] ; then # If run from the terminal CURL_ARGS="--progress-bar" else @@ -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" From 6e4d9508aa13c0666de7feeb875b9219de1b8cd8 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Fri, 5 Apr 2013 08:34:04 -0700 Subject: [PATCH 12/49] Close browser instead of sending shutdown message CEF now supports calling the beforeunload handler when closed so we no longer need to send a shutdown message on the native side. --- native/atom_cef_render_process_handler.h | 1 - native/atom_cef_render_process_handler.mm | 15 --------------- native/atom_window_controller.mm | 2 +- 3 files changed, 1 insertion(+), 17 deletions(-) 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 47af29dae..afd2f4177 100644 --- a/native/atom_cef_render_process_handler.mm +++ b/native/atom_cef_render_process_handler.mm @@ -30,10 +30,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 +50,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.mm b/native/atom_window_controller.mm index 5650637ec..efb838b3e 100644 --- a/native/atom_window_controller.mm +++ b/native/atom_window_controller.mm @@ -246,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); From 30fb637f863022e0b108e4b6896201e0ace092a2 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Fri, 5 Apr 2013 08:45:00 -0700 Subject: [PATCH 13/49] Add spec for excluding ignored files from Project.scan() --- spec/app/project-spec.coffee | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) 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()) From 9843147c3da8c75f7500c038dd79b8a73315c004 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Fri, 5 Apr 2013 08:52:07 -0700 Subject: [PATCH 14/49] Ignore tag files in any directory --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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/ From 1e3d157f49d0183dff113894ffa850e08add83da Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Fri, 5 Apr 2013 09:01:02 -0700 Subject: [PATCH 15/49] :lipstick: --- src/app/editor.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/editor.coffee b/src/app/editor.coffee index db183a383..7f57fe9b7 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' From c8dd9f8d8252d85d43bdc4d0f1fcdb7201489995 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Fri, 5 Apr 2013 09:07:31 -0700 Subject: [PATCH 16/49] Use indexOf instead of substring to check path --- src/packages/tree-view/lib/directory-view.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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() From f8fbfc8624c522cee14868e0d71a1ffbb871e7ce Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Fri, 5 Apr 2013 09:34:25 -0700 Subject: [PATCH 17/49] Don't show status color on arrow when selected --- themes/atom-dark-ui/tree-view.less | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/themes/atom-dark-ui/tree-view.less b/themes/atom-dark-ui/tree-view.less index f1d8a9796..fd0a39c71 100644 --- a/themes/atom-dark-ui/tree-view.less +++ b/themes/atom-dark-ui/tree-view.less @@ -39,7 +39,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; } From 9633677bcc77df0fcb1bb384eaa7bbe7a1ea1b1f Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Fri, 5 Apr 2013 10:16:56 -0700 Subject: [PATCH 18/49] Only check for updates on code signed builds --- atom.gyp | 7 +++++++ native/atom_application.mm | 14 ++++++++------ 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/atom.gyp b/atom.gyp index c586fc011..6213a4eaa 100644 --- a/atom.gyp +++ b/atom.gyp @@ -269,6 +269,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 55d7f971d..9c9824579 100644 --- a/native/atom_application.mm +++ b/native/atom_application.mm @@ -254,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 + } } From 26e53584c10741bad4418b62cd6768caf150ba14 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 4 Apr 2013 16:46:06 -0600 Subject: [PATCH 19/49] Add 'editor:add-selection-below' command It still needs work, but the basic idea is for every selection to add another another selection over the same column range of the line below. --- spec/app/edit-session-spec.coffee | 12 ++++++++++++ src/app/edit-session.coffee | 3 +++ src/app/editor.coffee | 2 ++ src/app/keymaps/editor.cson | 1 + src/app/selection.coffee | 6 ++++++ 5 files changed, 24 insertions(+) diff --git a/spec/app/edit-session-spec.coffee b/spec/app/edit-session-spec.coffee index bd4537617..2ab1f69ce 100644 --- a/spec/app/edit-session-spec.coffee +++ b/spec/app/edit-session-spec.coffee @@ -708,6 +708,18 @@ describe "EditSession", -> expect(editSession.selectMarker('bogus')).toBeFalsy() expect(editSession.getSelectedBufferRange()).toEqual rangeBefore + describe ".addSelectionBelow()", -> + 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]] + ] + describe "when the cursor is moved while there is a selection", -> makeSelection = -> selection.setBufferRange [[1, 2], [1, 5]] diff --git a/src/app/edit-session.coffee b/src/app/edit-session.coffee index a17c914f0..a24420e48 100644 --- a/src/app/edit-session.coffee +++ b/src/app/edit-session.coffee @@ -761,6 +761,9 @@ class EditSession selectLine: -> @expandSelectionsForward (selection) => selection.selectLine() + addSelectionBelow: -> + @expandSelectionsForward (selection) => selection.addSelectionBelow() + transpose: -> @mutateSelectedText (selection) => if selection.isEmpty() diff --git a/src/app/editor.coffee b/src/app/editor.coffee index 7f57fe9b7..026a00e49 100644 --- a/src/app/editor.coffee +++ b/src/app/editor.coffee @@ -122,6 +122,7 @@ class Editor extends View 'editor:select-to-beginning-of-line': @selectToBeginningOfLine 'editor:select-to-end-of-word': @selectToEndOfWord 'editor:select-to-beginning-of-word': @selectToBeginningOfWord + 'editor:add-selection-below': @addSelectionBelow 'editor:select-line': @selectLine 'editor:transpose': @transpose 'editor:upper-case': @upperCase @@ -211,6 +212,7 @@ class Editor extends View selectAll: -> @activeEditSession.selectAll() selectToBeginningOfLine: -> @activeEditSession.selectToBeginningOfLine() selectToEndOfLine: -> @activeEditSession.selectToEndOfLine() + addSelectionBelow: -> @activeEditSession.addSelectionBelow() selectToBeginningOfWord: -> @activeEditSession.selectToBeginningOfWord() selectToEndOfWord: -> @activeEditSession.selectToEndOfWord() selectWord: -> @activeEditSession.selectWord() diff --git a/src/app/keymaps/editor.cson b/src/app/keymaps/editor.cson index 52522ce73..1fcbbd6a6 100644 --- a/src/app/keymaps/editor.cson +++ b/src/app/keymaps/editor.cson @@ -9,6 +9,7 @@ 'ctrl-]': 'editor:unfold-current-row' 'ctrl-{': 'editor:fold-all' 'ctrl-}': 'editor:unfold-all' + 'ctrl-shift-down': 'editor:add-selection-below' 'alt-meta-ctrl-f': 'editor:fold-selection' 'shift-tab': 'editor:outdent-selected-rows' 'meta-[': 'editor:outdent-selected-rows' diff --git a/src/app/selection.coffee b/src/app/selection.coffee index 817769281..0fa7ae295 100644 --- a/src/app/selection.coffee +++ b/src/app/selection.coffee @@ -148,6 +148,12 @@ class Selection selectToEndOfWord: -> @modifySelection => @cursor.moveToEndOfWord() + addSelectionBelow: -> + range = @getBufferRange().copy() + range.start.row++ + range.end.row++ + @editSession.addSelectionForBufferRange(range) + insertText: (text, options={}) -> oldBufferRange = @getBufferRange() @editSession.destroyFoldsContainingBufferRow(oldBufferRange.end.row) From af923cca9b3a491d06c486e2b03c9ea1328519db Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 4 Apr 2013 17:33:23 -0600 Subject: [PATCH 20/49] Preserve original selection's range when adding selection's below Just like the cursor tries to stay in its "goal column" when moving vertically, here we try to keep the same selection even when adding across shorter lines. --- spec/app/edit-session-spec.coffee | 12 ++++++++++++ src/app/edit-session.coffee | 4 ++-- src/app/selection.coffee | 8 +++++--- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/spec/app/edit-session-spec.coffee b/spec/app/edit-session-spec.coffee index 2ab1f69ce..e5e9fdf26 100644 --- a/spec/app/edit-session-spec.coffee +++ b/spec/app/edit-session-spec.coffee @@ -720,6 +720,18 @@ describe "EditSession", -> [[4, 25], [4, 29]] ] + it "honors the original selection's region 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]] + ] + describe "when the cursor is moved while there is a selection", -> makeSelection = -> selection.setBufferRange [[1, 2], [1, 5]] diff --git a/src/app/edit-session.coffee b/src/app/edit-session.coffee index a24420e48..f5f85c650 100644 --- a/src/app/edit-session.coffee +++ b/src/app/edit-session.coffee @@ -585,7 +585,7 @@ 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() @@ -600,7 +600,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) diff --git a/src/app/selection.coffee b/src/app/selection.coffee index 0fa7ae295..5a141e75b 100644 --- a/src/app/selection.coffee +++ b/src/app/selection.coffee @@ -5,10 +5,12 @@ _ = require 'underscore' module.exports = class Selection wordwise: false + editSession: null initialScreenRange: null + goalBufferRange: null needsAutoscroll: null - constructor: ({@cursor, @marker, @editSession}) -> + constructor: ({@cursor, @marker, @editSession, @goalBufferRange}) -> @cursor.selection = this @editSession.observeMarker @marker, => @screenRangeChanged() @cursor.on 'destroyed.selection', => @@ -149,10 +151,10 @@ class Selection @modifySelection => @cursor.moveToEndOfWord() addSelectionBelow: -> - range = @getBufferRange().copy() + range = (@goalBufferRange ? @getBufferRange()).copy() range.start.row++ range.end.row++ - @editSession.addSelectionForBufferRange(range) + @editSession.addSelectionForBufferRange(range, goalBufferRange: range) insertText: (text, options={}) -> oldBufferRange = @getBufferRange() From 34019951d3e5b06342f7e521c923c30e4d0269e4 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 4 Apr 2013 17:33:29 -0600 Subject: [PATCH 21/49] :lipstick: --- src/app/selection.coffee | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/app/selection.coffee b/src/app/selection.coffee index 5a141e75b..79e678b2c 100644 --- a/src/app/selection.coffee +++ b/src/app/selection.coffee @@ -4,10 +4,12 @@ _ = 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, @goalBufferRange}) -> From 31579703f0bd409d6bf3accd58101a77cebb27d0 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 4 Apr 2013 17:48:02 -0600 Subject: [PATCH 22/49] Ensure new non-empty selections have an invisible cursor. --- spec/app/edit-session-spec.coffee | 2 ++ src/app/buffer-marker.coffee | 3 +++ src/app/cursor.coffee | 7 +++++-- src/app/display-buffer.coffee | 3 +++ src/app/edit-session.coffee | 3 +++ src/app/text-buffer.coffee | 3 +++ 6 files changed, 19 insertions(+), 2 deletions(-) diff --git a/spec/app/edit-session-spec.coffee b/spec/app/edit-session-spec.coffee index e5e9fdf26..b77ddebcd 100644 --- a/spec/app/edit-session-spec.coffee +++ b/spec/app/edit-session-spec.coffee @@ -719,6 +719,8 @@ describe "EditSession", -> [[4, 16], [4, 21]] [[4, 25], [4, 29]] ] + for cursor in editSession.getCursors() + expect(cursor.isVisible()).toBeFalsy() it "honors the original selection's region when adding across shorter lines", -> editSession.setSelectedBufferRange([[3, 22], [3, 38]]) diff --git a/src/app/buffer-marker.coffee b/src/app/buffer-marker.coffee index 199171595..13d0ec8a7 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) + hasTail: -> + @tailPosition? + getRange: -> if @tailPosition new Range(@tailPosition, @headPosition) diff --git a/src/app/cursor.coffee b/src/app/cursor.coffee index 8f1a21d67..4832b74d8 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 @@ -59,6 +59,9 @@ class Cursor unless fn() @trigger 'autoscrolled' if @needsAutoscroll + updateVisibility: -> + @setVisible(not @editSession.doesMarkerHaveTail(@marker)) + setVisible: (visible) -> if @visible != visible @visible = visible diff --git a/src/app/display-buffer.coffee b/src/app/display-buffer.coffee index 0f24a235d..ed4772742 100644 --- a/src/app/display-buffer.coffee +++ b/src/app/display-buffer.coffee @@ -400,6 +400,9 @@ class DisplayBuffer isMarkerReversed: (id) -> @buffer.isMarkerReversed(id) + doesMarkerHaveTail: (id) -> + @buffer.doesMarkerHaveTail(id) + observeMarker: (id, callback) -> @getMarker(id).observe(callback) diff --git a/src/app/edit-session.coffee b/src/app/edit-session.coffee index f5f85c650..83163f894 100644 --- a/src/app/edit-session.coffee +++ b/src/app/edit-session.coffee @@ -556,6 +556,9 @@ class EditSession isMarkerReversed: (args...) -> @displayBuffer.isMarkerReversed(args...) + doesMarkerHaveTail: (args...) -> + @displayBuffer.doesMarkerHaveTail(args...) + hasMultipleCursors: -> @getCursors().length > 1 diff --git a/src/app/text-buffer.coffee b/src/app/text-buffer.coffee index 7fa3bb32a..62dbedc1c 100644 --- a/src/app/text-buffer.coffee +++ b/src/app/text-buffer.coffee @@ -339,6 +339,9 @@ class Buffer isMarkerReversed: (id) -> @validMarkers[id]?.isReversed() + doesMarkerHaveTail: (id) -> + @validMarkers[id]?.hasTail() + observeMarker: (id, callback) -> @validMarkers[id]?.observe(callback) From f6bfab5dd72ec1ad913a1d2a226afa42fdfc5073 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 4 Apr 2013 19:05:22 -0600 Subject: [PATCH 23/49] Don't freak when selections are added & removed before display update Previously, if a selection was added and removed before the editor got a chance to update its display, it would try to add a selection view for the destroyed selection. Now we check the new selections and cursors to make sure they aren't destroyed before we add views for them. --- spec/app/editor-spec.coffee | 12 ++++++++++++ src/app/cursor.coffee | 1 + src/app/editor.coffee | 4 ++-- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/spec/app/editor-spec.coffee b/spec/app/editor-spec.coffee index f5c7612c4..bfee6598b 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) diff --git a/src/app/cursor.coffee b/src/app/cursor.coffee index 4832b74d8..a30f77f93 100644 --- a/src/app/cursor.coffee +++ b/src/app/cursor.coffee @@ -34,6 +34,7 @@ class Cursor @needsAutoscroll = true destroy: -> + @destroyed = true @editSession.destroyMarker(@marker) @editSession.removeCursor(this) @trigger 'destroyed' diff --git a/src/app/editor.coffee b/src/app/editor.coffee index 026a00e49..335523c49 100644 --- a/src/app/editor.coffee +++ b/src/app/editor.coffee @@ -780,7 +780,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 = [] @@ -792,7 +792,7 @@ 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() From 7b7c77645ce17c08a339ecbf0d7d99db8fa43b97 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 4 Apr 2013 19:06:28 -0600 Subject: [PATCH 24/49] Rename SelectionView.destroyed to .needsRemoval to match CursorView --- src/app/editor.coffee | 2 +- src/app/selection-view.coffee | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/editor.coffee b/src/app/editor.coffee index 335523c49..778f56c43 100644 --- a/src/app/editor.coffee +++ b/src/app/editor.coffee @@ -796,7 +796,7 @@ class Editor extends View @newSelections = [] for selectionView in @getSelectionViews() - if selectionView.destroyed + if selectionView.needsRemoval selectionView.remove() else selectionView.updateDisplay() 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: -> From f22461e5e8eb1f0fcf10044cf63ee49b43d4e74d Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 4 Apr 2013 19:18:42 -0600 Subject: [PATCH 25/49] Clear goal range when selection is modified Just like the cursor clears its goal column when it is moved in any way other than vertically, the selection clears its goal range (the range it will attempt to use when adding a selection below) when it is changed in any way. --- spec/app/edit-session-spec.coffee | 17 ++++++++++++++++- src/app/cursor.coffee | 1 + src/app/edit-session.coffee | 2 +- src/app/selection.coffee | 2 +- 4 files changed, 19 insertions(+), 3 deletions(-) diff --git a/spec/app/edit-session-spec.coffee b/spec/app/edit-session-spec.coffee index b77ddebcd..90eca8369 100644 --- a/spec/app/edit-session-spec.coffee +++ b/spec/app/edit-session-spec.coffee @@ -722,7 +722,7 @@ describe "EditSession", -> for cursor in editSession.getCursors() expect(cursor.isVisible()).toBeFalsy() - it "honors the original selection's region when adding across shorter lines", -> + it "honors the original selection's range (goal range) when adding across shorter lines", -> editSession.setSelectedBufferRange([[3, 22], [3, 38]]) editSession.addSelectionBelow() editSession.addSelectionBelow() @@ -734,6 +734,21 @@ describe "EditSession", -> [[6, 22], [6, 38]] ] + it "clears selection goal ranges when the selection changes", -> + editSession.setSelectedBufferRange([[3, 22], [3, 38]]) + console.log "1" + editSession.addSelectionBelow() + editSession.selectLeft() + console.log "2" + editSession.addSelectionBelow() + editSession.addSelectionBelow() + expect(editSession.getSelectedBufferRanges()).toEqual [ + [[3, 22], [3, 37]] + [[4, 22], [4, 29]] + [[5, 22], [5, 29]] + [[6, 22], [6, 28]] + ] + describe "when the cursor is moved while there is a selection", -> makeSelection = -> selection.setBufferRange [[1, 2], [1, 5]] diff --git a/src/app/cursor.coffee b/src/app/cursor.coffee index a30f77f93..92a899460 100644 --- a/src/app/cursor.coffee +++ b/src/app/cursor.coffee @@ -88,6 +88,7 @@ class Cursor clearSelection: -> if @selection + @selection.goalBufferRange = null @selection.clear() unless @selection.retainSelection getScreenRow: -> diff --git a/src/app/edit-session.coffee b/src/app/edit-session.coffee index 83163f894..26796842f 100644 --- a/src/app/edit-session.coffee +++ b/src/app/edit-session.coffee @@ -591,7 +591,7 @@ class EditSession 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) diff --git a/src/app/selection.coffee b/src/app/selection.coffee index 79e678b2c..bcc7e93f2 100644 --- a/src/app/selection.coffee +++ b/src/app/selection.coffee @@ -156,7 +156,7 @@ class Selection range = (@goalBufferRange ? @getBufferRange()).copy() range.start.row++ range.end.row++ - @editSession.addSelectionForBufferRange(range, goalBufferRange: range) + @editSession.addSelectionForBufferRange(range, goalBufferRange: range, suppressMerge: true) insertText: (text, options={}) -> oldBufferRange = @getBufferRange() From ff8491f8d391b873e93c6c586ae3ea099a989501 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 4 Apr 2013 19:22:34 -0600 Subject: [PATCH 26/49] Base cursor visibility on marker emptiness instead of tail status --- src/app/buffer-marker.coffee | 4 ++-- src/app/cursor.coffee | 2 +- src/app/display-buffer.coffee | 4 ++-- src/app/edit-session.coffee | 4 ++-- src/app/text-buffer.coffee | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/app/buffer-marker.coffee b/src/app/buffer-marker.coffee index 13d0ec8a7..ebc843546 100644 --- a/src/app/buffer-marker.coffee +++ b/src/app/buffer-marker.coffee @@ -27,8 +27,8 @@ class BufferMarker isReversed: -> @tailPosition? and @headPosition.isLessThan(@tailPosition) - hasTail: -> - @tailPosition? + isRangeEmpty: -> + @getHeadPosition().isEqual(@getTailPosition()) getRange: -> if @tailPosition diff --git a/src/app/cursor.coffee b/src/app/cursor.coffee index 92a899460..52760c63b 100644 --- a/src/app/cursor.coffee +++ b/src/app/cursor.coffee @@ -61,7 +61,7 @@ class Cursor @trigger 'autoscrolled' if @needsAutoscroll updateVisibility: -> - @setVisible(not @editSession.doesMarkerHaveTail(@marker)) + @setVisible(@editSession.isMarkerRangeEmpty(@marker)) setVisible: (visible) -> if @visible != visible diff --git a/src/app/display-buffer.coffee b/src/app/display-buffer.coffee index ed4772742..37aba8bff 100644 --- a/src/app/display-buffer.coffee +++ b/src/app/display-buffer.coffee @@ -400,8 +400,8 @@ class DisplayBuffer isMarkerReversed: (id) -> @buffer.isMarkerReversed(id) - doesMarkerHaveTail: (id) -> - @buffer.doesMarkerHaveTail(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 26796842f..9d7d2e228 100644 --- a/src/app/edit-session.coffee +++ b/src/app/edit-session.coffee @@ -556,8 +556,8 @@ class EditSession isMarkerReversed: (args...) -> @displayBuffer.isMarkerReversed(args...) - doesMarkerHaveTail: (args...) -> - @displayBuffer.doesMarkerHaveTail(args...) + isMarkerRangeEmpty: (args...) -> + @displayBuffer.isMarkerRangeEmpty(args...) hasMultipleCursors: -> @getCursors().length > 1 diff --git a/src/app/text-buffer.coffee b/src/app/text-buffer.coffee index 62dbedc1c..0d77dc914 100644 --- a/src/app/text-buffer.coffee +++ b/src/app/text-buffer.coffee @@ -339,8 +339,8 @@ class Buffer isMarkerReversed: (id) -> @validMarkers[id]?.isReversed() - doesMarkerHaveTail: (id) -> - @validMarkers[id]?.hasTail() + isMarkerRangeEmpty: (id) -> + @validMarkers[id]?.isRangeEmpty() observeMarker: (id, callback) -> @validMarkers[id]?.observe(callback) From 40d7fcf32c40d68bf1224da1abc8d2c553334a6a Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 5 Apr 2013 11:06:51 -0600 Subject: [PATCH 27/49] :speak_no_evil: --- spec/app/edit-session-spec.coffee | 2 -- 1 file changed, 2 deletions(-) diff --git a/spec/app/edit-session-spec.coffee b/spec/app/edit-session-spec.coffee index 90eca8369..5aef58baf 100644 --- a/spec/app/edit-session-spec.coffee +++ b/spec/app/edit-session-spec.coffee @@ -736,10 +736,8 @@ describe "EditSession", -> it "clears selection goal ranges when the selection changes", -> editSession.setSelectedBufferRange([[3, 22], [3, 38]]) - console.log "1" editSession.addSelectionBelow() editSession.selectLeft() - console.log "2" editSession.addSelectionBelow() editSession.addSelectionBelow() expect(editSession.getSelectedBufferRanges()).toEqual [ From 131df22c112bbb5364dac89c6abc678213f80408 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 5 Apr 2013 11:18:50 -0600 Subject: [PATCH 28/49] Skip lines that are too-short when adding non-empty selection below --- spec/app/edit-session-spec.coffee | 79 +++++++++++++++++-------------- src/app/edit-session.coffee | 4 +- src/app/selection.coffee | 10 ++-- 3 files changed, 53 insertions(+), 40 deletions(-) diff --git a/spec/app/edit-session-spec.coffee b/spec/app/edit-session-spec.coffee index 5aef58baf..b8cb46d7b 100644 --- a/spec/app/edit-session-spec.coffee +++ b/spec/app/edit-session-spec.coffee @@ -709,43 +709,52 @@ describe "EditSession", -> expect(editSession.getSelectedBufferRange()).toEqual rangeBefore describe ".addSelectionBelow()", -> - 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() + 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 "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 "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 "clears selection goal ranges when the selection changes", -> - editSession.setSelectedBufferRange([[3, 22], [3, 38]]) - editSession.addSelectionBelow() - editSession.selectLeft() - editSession.addSelectionBelow() - editSession.addSelectionBelow() - expect(editSession.getSelectedBufferRanges()).toEqual [ - [[3, 22], [3, 37]] - [[4, 22], [4, 29]] - [[5, 22], [5, 29]] - [[6, 22], [6, 28]] - ] + 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() + editSession.addSelectionBelow() + expect(editSession.getSelectedBufferRanges()).toEqual [ + [[3, 22], [3, 37]] + [[4, 22], [4, 29]] + [[5, 22], [5, 29]] + [[6, 22], [6, 28]] + ] describe "when the cursor is moved while there is a selection", -> makeSelection = -> selection.setBufferRange [[1, 2], [1, 5]] diff --git a/src/app/edit-session.coffee b/src/app/edit-session.coffee index 9d7d2e228..2846412d2 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)) diff --git a/src/app/selection.coffee b/src/app/selection.coffee index bcc7e93f2..40f1ad389 100644 --- a/src/app/selection.coffee +++ b/src/app/selection.coffee @@ -154,9 +154,13 @@ class Selection addSelectionBelow: -> range = (@goalBufferRange ? @getBufferRange()).copy() - range.start.row++ - range.end.row++ - @editSession.addSelectionForBufferRange(range, goalBufferRange: range, suppressMerge: true) + nextRow = range.end.row + 1 + for row in [nextRow..@editSession.getLastBufferRow()] + range.start.row = row + range.end.row = row + unless @editSession.clipBufferRange(range).isEmpty() + @editSession.addSelectionForBufferRange(range, goalBufferRange: range, suppressMerge: true) + break insertText: (text, options={}) -> oldBufferRange = @getBufferRange() From 393cba4d42a0fdba71b95ff2e7f14722a3d96ed9 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 5 Apr 2013 11:40:03 -0600 Subject: [PATCH 29/49] Don't skip shorter lines when the adding empty selection below --- spec/app/edit-session-spec.coffee | 13 +++++++++++++ src/app/selection.coffee | 18 ++++++++++++------ 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/spec/app/edit-session-spec.coffee b/spec/app/edit-session-spec.coffee index b8cb46d7b..6092dd662 100644 --- a/spec/app/edit-session-spec.coffee +++ b/spec/app/edit-session-spec.coffee @@ -756,6 +756,19 @@ describe "EditSession", -> [[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]] + ] + describe "when the cursor is moved while there is a selection", -> makeSelection = -> selection.setBufferRange [[1, 2], [1, 5]] diff --git a/src/app/selection.coffee b/src/app/selection.coffee index 40f1ad389..6c37b2291 100644 --- a/src/app/selection.coffee +++ b/src/app/selection.coffee @@ -155,12 +155,18 @@ class Selection addSelectionBelow: -> range = (@goalBufferRange ? @getBufferRange()).copy() nextRow = range.end.row + 1 - for row in [nextRow..@editSession.getLastBufferRow()] - range.start.row = row - range.end.row = row - unless @editSession.clipBufferRange(range).isEmpty() - @editSession.addSelectionForBufferRange(range, goalBufferRange: range, suppressMerge: true) - break + + if range.isEmpty() + range.start.row = nextRow + range.end.row = nextRow + @editSession.addSelectionForBufferRange(range, goalBufferRange: range, suppressMerge: true) + else + for row in [nextRow..@editSession.getLastBufferRow()] + range.start.row = row + range.end.row = row + unless @editSession.clipBufferRange(range).isEmpty() + @editSession.addSelectionForBufferRange(range, goalBufferRange: range, suppressMerge: true) + break insertText: (text, options={}) -> oldBufferRange = @getBufferRange() From be009e87c29c353f69f6893aa881914f6df3dfcd Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 5 Apr 2013 11:55:51 -0600 Subject: [PATCH 30/49] Skip empty lines when adding selections below empty selections Unless the selection's column is 0 --- spec/app/edit-session-spec.coffee | 16 ++++++++++++++++ src/app/selection.coffee | 21 +++++++++++---------- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/spec/app/edit-session-spec.coffee b/spec/app/edit-session-spec.coffee index 6092dd662..c1f791dc7 100644 --- a/spec/app/edit-session-spec.coffee +++ b/spec/app/edit-session-spec.coffee @@ -769,6 +769,22 @@ describe "EditSession", -> [[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 "when the cursor is moved while there is a selection", -> makeSelection = -> selection.setBufferRange [[1, 2], [1, 5]] diff --git a/src/app/selection.coffee b/src/app/selection.coffee index 6c37b2291..844ee0242 100644 --- a/src/app/selection.coffee +++ b/src/app/selection.coffee @@ -156,17 +156,18 @@ class Selection range = (@goalBufferRange ? @getBufferRange()).copy() nextRow = range.end.row + 1 - if range.isEmpty() - range.start.row = nextRow - range.end.row = nextRow + 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) - else - for row in [nextRow..@editSession.getLastBufferRow()] - range.start.row = row - range.end.row = row - unless @editSession.clipBufferRange(range).isEmpty() - @editSession.addSelectionForBufferRange(range, goalBufferRange: range, suppressMerge: true) - break + break insertText: (text, options={}) -> oldBufferRange = @getBufferRange() From bd58834e7d1589be4ba6174db1e227f68212f056 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 5 Apr 2013 12:15:21 -0600 Subject: [PATCH 31/49] Merge goal ranges when merging selections --- spec/app/edit-session-spec.coffee | 9 ++++++++- src/app/selection.coffee | 4 ++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/spec/app/edit-session-spec.coffee b/spec/app/edit-session-spec.coffee index c1f791dc7..e5bd91b82 100644 --- a/spec/app/edit-session-spec.coffee +++ b/spec/app/edit-session-spec.coffee @@ -748,11 +748,18 @@ describe "EditSession", -> 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, 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]] ] diff --git a/src/app/selection.coffee b/src/app/selection.coffee index 844ee0242..5d6e06f2e 100644 --- a/src/app/selection.coffee +++ b/src/app/selection.coffee @@ -394,6 +394,10 @@ class Selection 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 From 2efed9f42c73ba816d20847a44228c77f2f1121f Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 5 Apr 2013 12:33:27 -0600 Subject: [PATCH 32/49] Add EditSession.consolidateSelections() --- spec/app/edit-session-spec.coffee | 14 ++++++++++++++ src/app/edit-session.coffee | 15 +++++++++------ 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/spec/app/edit-session-spec.coffee b/spec/app/edit-session-spec.coffee index e5bd91b82..2fb732910 100644 --- a/spec/app/edit-session-spec.coffee +++ b/spec/app/edit-session-spec.coffee @@ -792,6 +792,20 @@ describe "EditSession", -> [[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/src/app/edit-session.coffee b/src/app/edit-session.coffee index 2846412d2..cef8da34c 100644 --- a/src/app/edit-session.coffee +++ b/src/app/edit-session.coffee @@ -626,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...) From 7018f33ad77a8fc83731244616314149a9c193b2 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 5 Apr 2013 13:22:13 -0600 Subject: [PATCH 33/49] Allow !important flag in keymap selectors --- spec/app/keymap-spec.coffee | 3 ++- src/app/binding-set.coffee | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) 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/src/app/binding-set.coffee b/src/app/binding-set.coffee index e9017d1f9..1fea03c22 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(/\s*!important\s*$/, '') @commandsByKeystrokes = @normalizeCommandsByKeystrokes(commandsByKeystrokes) commandForEvent: (event) -> From 37e16bb1637383bd6c4f0eda8e2ff33f1c21c0b5 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 5 Apr 2013 13:23:45 -0600 Subject: [PATCH 34/49] Clear multiple selections on escape The binding uses the `!important` selector to ensure that the editor always gets a chance to clear multiple selections before other bindings for escape are processed. --- spec/app/editor-spec.coffee | 16 ++++++++++++++++ src/app/editor.coffee | 4 +++- src/app/keymaps/editor.cson | 3 +++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/spec/app/editor-spec.coffee b/spec/app/editor-spec.coffee index bfee6598b..6e3de12a0 100644 --- a/spec/app/editor-spec.coffee +++ b/spec/app/editor-spec.coffee @@ -2472,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/src/app/editor.coffee b/src/app/editor.coffee index 778f56c43..70f4e5b5b 100644 --- a/src/app/editor.coffee +++ b/src/app/editor.coffee @@ -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 @@ -164,7 +165,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() @@ -232,6 +233,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) diff --git a/src/app/keymaps/editor.cson b/src/app/keymaps/editor.cson index 1fcbbd6a6..f79b8310e 100644 --- a/src/app/keymaps/editor.cson +++ b/src/app/keymaps/editor.cson @@ -31,3 +31,6 @@ 'enter': 'core:confirm', 'escape': 'core:cancel' 'meta-w': 'core:cancel' + +'.editor !important': + 'escape': 'editor:consolidate-selections' From a7091c8d943ae0a79b3996e376463ccc0bea3b1a Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 5 Apr 2013 13:26:24 -0600 Subject: [PATCH 35/49] Remove *all* !important expressions once specificity is calculated --- src/app/binding-set.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/binding-set.coffee b/src/app/binding-set.coffee index 1fea03c22..9fadc8756 100644 --- a/src/app/binding-set.coffee +++ b/src/app/binding-set.coffee @@ -19,7 +19,7 @@ class BindingSet constructor: (selector, commandsByKeystrokes, @index, @name) -> BindingSet.parser ?= PEG.buildParser(fsUtils.read(require.resolve 'keystroke-pattern.pegjs')) @specificity = Specificity(selector) - @selector = selector.replace(/\s*!important\s*$/, '') + @selector = selector.replace(/!important/g, '') @commandsByKeystrokes = @normalizeCommandsByKeystrokes(commandsByKeystrokes) commandForEvent: (event) -> From 3e073515522f305dbc92860e194f723a38b720b4 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 5 Apr 2013 13:26:47 -0600 Subject: [PATCH 36/49] Also bind escape to consolidate selections in mini editors --- src/app/keymaps/editor.cson | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/keymaps/editor.cson b/src/app/keymaps/editor.cson index f79b8310e..432fe6b30 100644 --- a/src/app/keymaps/editor.cson +++ b/src/app/keymaps/editor.cson @@ -32,5 +32,5 @@ 'escape': 'core:cancel' 'meta-w': 'core:cancel' -'.editor !important': +'.editor !important, .editor.mini !important': 'escape': 'editor:consolidate-selections' From abc5ed5190d9aff775dcefb6fabd3aff05d55616 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 5 Apr 2013 14:11:08 -0600 Subject: [PATCH 37/49] Add editor:add-selection-above command --- spec/app/edit-session-spec.coffee | 64 +++++++++++++++++++++++++++++++ src/app/edit-session.coffee | 3 ++ src/app/editor.coffee | 2 + src/app/keymaps/editor.cson | 1 + src/app/selection.coffee | 17 ++++++++ 5 files changed, 87 insertions(+) diff --git a/spec/app/edit-session-spec.coffee b/spec/app/edit-session-spec.coffee index 2fb732910..3327b9f06 100644 --- a/spec/app/edit-session-spec.coffee +++ b/spec/app/edit-session-spec.coffee @@ -792,6 +792,70 @@ describe "EditSession", -> [[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]]) diff --git a/src/app/edit-session.coffee b/src/app/edit-session.coffee index cef8da34c..e3365b9bf 100644 --- a/src/app/edit-session.coffee +++ b/src/app/edit-session.coffee @@ -770,6 +770,9 @@ class EditSession 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 70f4e5b5b..8259fd967 100644 --- a/src/app/editor.coffee +++ b/src/app/editor.coffee @@ -124,6 +124,7 @@ class Editor extends View 'editor:select-to-end-of-word': @selectToEndOfWord 'editor:select-to-beginning-of-word': @selectToBeginningOfWord 'editor:add-selection-below': @addSelectionBelow + 'editor:add-selection-above': @addSelectionAbove 'editor:select-line': @selectLine 'editor:transpose': @transpose 'editor:upper-case': @upperCase @@ -214,6 +215,7 @@ class Editor extends View selectToBeginningOfLine: -> @activeEditSession.selectToBeginningOfLine() selectToEndOfLine: -> @activeEditSession.selectToEndOfLine() addSelectionBelow: -> @activeEditSession.addSelectionBelow() + addSelectionAbove: -> @activeEditSession.addSelectionAbove() selectToBeginningOfWord: -> @activeEditSession.selectToBeginningOfWord() selectToEndOfWord: -> @activeEditSession.selectToEndOfWord() selectWord: -> @activeEditSession.selectWord() diff --git a/src/app/keymaps/editor.cson b/src/app/keymaps/editor.cson index 432fe6b30..7b95d6c53 100644 --- a/src/app/keymaps/editor.cson +++ b/src/app/keymaps/editor.cson @@ -10,6 +10,7 @@ 'ctrl-{': 'editor:fold-all' 'ctrl-}': 'editor:unfold-all' 'ctrl-shift-down': 'editor:add-selection-below' + 'ctrl-shift-up': 'editor:add-selection-above' 'alt-meta-ctrl-f': 'editor:fold-selection' 'shift-tab': 'editor:outdent-selected-rows' 'meta-[': 'editor:outdent-selected-rows' diff --git a/src/app/selection.coffee b/src/app/selection.coffee index 5d6e06f2e..2bde79cfd 100644 --- a/src/app/selection.coffee +++ b/src/app/selection.coffee @@ -169,6 +169,23 @@ class Selection @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) From 987f80aeb3c58a3d03e7ef2a02f83769bf80c2f2 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 5 Apr 2013 14:14:11 -0600 Subject: [PATCH 38/49] Bind add selection commands to alt-shift-up/down --- src/app/keymaps/editor.cson | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/keymaps/editor.cson b/src/app/keymaps/editor.cson index 7b95d6c53..335be5587 100644 --- a/src/app/keymaps/editor.cson +++ b/src/app/keymaps/editor.cson @@ -9,8 +9,8 @@ 'ctrl-]': 'editor:unfold-current-row' 'ctrl-{': 'editor:fold-all' 'ctrl-}': 'editor:unfold-all' - 'ctrl-shift-down': 'editor:add-selection-below' - 'ctrl-shift-up': 'editor:add-selection-above' + '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' From 6042439598e19bdd2d703cd69cd3c0ee35bfc67e Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 5 Apr 2013 14:14:33 -0600 Subject: [PATCH 39/49] Add emacs bindings for add selection commands: alt-ctrl-n/p --- src/app/keymaps/emacs.cson | 2 ++ 1 file changed, 2 insertions(+) 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' From 763729d08d35bc68b84c1f96eaa7b753b1aecd0a Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 5 Apr 2013 14:51:34 -0600 Subject: [PATCH 40/49] :horse_racing: Use buffer ranges to see if selections intersect --- src/app/selection.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/selection.coffee b/src/app/selection.coffee index 2bde79cfd..03bf8aca4 100644 --- a/src/app/selection.coffee +++ b/src/app/selection.coffee @@ -407,7 +407,7 @@ class Selection @getBufferRange().intersectsWith(bufferRange) intersectsWith: (otherSelection) -> - @getScreenRange().intersectsWith(otherSelection.getScreenRange()) + @getBufferRange().intersectsWith(otherSelection.getBufferRange()) merge: (otherSelection, options) -> @setBufferRange(@getBufferRange().union(otherSelection.getBufferRange()), options) From b9fcfda9048d76097aeab5ec9e545d9b2d870b03 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Fri, 5 Apr 2013 12:50:46 -0700 Subject: [PATCH 41/49] Only match brackets if underlayer is visible --- src/packages/bracket-matcher/lib/bracket-matcher.coffee | 1 + src/stdlib/jquery-extensions.coffee | 3 +++ 2 files changed, 4 insertions(+) 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/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 From fcb90abfda00a819c38311203512c455ab70c892 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Fri, 5 Apr 2013 15:03:30 -0700 Subject: [PATCH 42/49] Don't show status colors on selected arrow --- themes/atom-dark-ui/tree-view.less | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/themes/atom-dark-ui/tree-view.less b/themes/atom-dark-ui/tree-view.less index fd0a39c71..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; } @@ -40,8 +41,8 @@ .tree-view .entry:hover, .tree-view .directory .header:hover .name, .tree-view .directory .header:hover .disclosure-arrow, -.tree-view .selected .directory .header .disclosure-arrow, -.tree-view .selected .directory .header:hover .disclosure-arrow { +.tree-view .selected > .directory > .header .disclosure-arrow, +.tree-view .selected > .directory > .header:hover .disclosure-arrow { color: #ebebeb; } From e0865e8c38e069e8996eaf6acc3006e615a74f4f Mon Sep 17 00:00:00 2001 From: Cheng Zhao Date: Sun, 7 Apr 2013 16:18:08 +0800 Subject: [PATCH 43/49] Use node-pathwatcher. --- package.json | 1 + spec/app/file-spec.coffee | 2 ++ spec/app/text-buffer-spec.coffee | 2 ++ spec/spec-helper.coffee | 5 +++-- src/app/directory.coffee | 7 ++++--- src/app/file.coffee | 15 ++++++++------- src/stdlib/fs-utils.coffee | 8 -------- 7 files changed, 20 insertions(+), 20 deletions(-) diff --git a/package.json b/package.json index c691b0b27..c31deeab9 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.4", "plist": "git://github.com/nathansobo/node-plist.git", "space-pen": "git://github.com/nathansobo/space-pen.git" }, 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/text-buffer-spec.coffee b/spec/app/text-buffer-spec.coffee index 2683cc7a7..93e39af75 100644 --- a/spec/app/text-buffer-spec.coffee +++ b/spec/app/text-buffer-spec.coffee @@ -65,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) 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/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/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/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) From 7c8e1634b8e2f46340a23cd04eef5e6bf588e51c Mon Sep 17 00:00:00 2001 From: Cheng Zhao Date: Wed, 3 Apr 2013 20:38:39 +0800 Subject: [PATCH 44/49] Remove native path watcher code. --- atom.gyp | 2 - native/atom_cef_render_process_handler.mm | 2 - native/path_watcher.h | 25 -- native/path_watcher.mm | 273 ---------------------- native/v8_extensions/native.mm | 65 +----- 5 files changed, 1 insertion(+), 366 deletions(-) delete mode 100644 native/path_watcher.h delete mode 100644 native/path_watcher.mm diff --git a/atom.gyp b/atom.gyp index 6213a4eaa..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', diff --git a/native/atom_cef_render_process_handler.mm b/native/atom_cef_render_process_handler.mm index afd2f4177..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, 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 From 4636e9ca40b5865592f73213b7cc9e8187ec29a5 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Sun, 7 Apr 2013 14:52:20 -0700 Subject: [PATCH 45/49] Unsubscribe if editor has no session --- src/packages/spell-check/lib/spell-check-view.coffee | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) 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) => From dc94855e1a15fe0872ad8c6329f3eefe72057ca2 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Sun, 7 Apr 2013 15:07:57 -0700 Subject: [PATCH 46/49] Parse emoji start, word, and end into different scopes --- src/packages/gfm/grammars/gfm.cson | 6 +++++- src/packages/gfm/spec/gfm-spec.coffee | 4 +++- 2 files changed, 8 insertions(+), 2 deletions(-) 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"] From 17e9c6ea3b550cf8e20343ca484b88d06d951deb Mon Sep 17 00:00:00 2001 From: Cheng Zhao Date: Sun, 7 Apr 2013 23:38:22 +0800 Subject: [PATCH 47/49] Restart renderer process when reloaded for 4 times. Fix #481. --- native/atom_cef_client.cpp | 11 +++++++++++ native/atom_cef_client.h | 1 + src/app/atom.coffee | 3 +++ src/app/window.coffee | 9 ++++++++- 4 files changed, 23 insertions(+), 1 deletion(-) 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/src/app/atom.coffee b/src/app/atom.coffee index 7807af1c3..ada93c5d4 100644 --- a/src/app/atom.coffee +++ b/src/app/atom.coffee @@ -126,6 +126,9 @@ _.extend atom, newWindow: (args...) -> @sendMessageToBrowserProcess('newWindow', args) + restartRendererProcess: -> + @sendMessageToBrowserProcess('restartRendererProcess') + confirm: (message, detailedMessage, buttonLabelsAndCallbacks...) -> wrapCallback = (callback) => => @dismissModal(callback) @presentModal => diff --git a/src/app/window.coffee b/src/app/window.coffee index d2d70894c..2c2175565 100644 --- a/src/app/window.coffee +++ b/src/app/window.coffee @@ -154,7 +154,14 @@ window.applyStylesheet = (id, text, ttype = 'bundled') -> $("head").append "" window.reload = -> - $native.reload() + timesReloaded = process.global.timesReloaded ? 0 + ++timesReloaded + + if timesReloaded > 3 + atom.restartRendererProcess() + else + $native.reload() + process.global.timesReloaded = timesReloaded window.onerror = -> atom.showDevTools() From f4161a18891093c72b1c3409d2a08d7293fcb66c Mon Sep 17 00:00:00 2001 From: Cheng Zhao Date: Sun, 7 Apr 2013 23:53:02 +0800 Subject: [PATCH 48/49] Restart interval of main window should be longer than specs window. --- src/app/window.coffee | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/app/window.coffee b/src/app/window.coffee index 2c2175565..d78f54caa 100644 --- a/src/app/window.coffee +++ b/src/app/window.coffee @@ -157,7 +157,9 @@ window.reload = -> timesReloaded = process.global.timesReloaded ? 0 ++timesReloaded - if timesReloaded > 3 + restartValue = if window.location.search.indexOf('spec-bootstrap') == -1 then 10 else 3 + + if timesReloaded > restartValue atom.restartRendererProcess() else $native.reload() From 314324562eca14e1d1ba735bf69393faa1ef7555 Mon Sep 17 00:00:00 2001 From: Cheng Zhao Date: Mon, 8 Apr 2013 11:59:54 +0800 Subject: [PATCH 49/49] Update node-pathwatcher to v0.1.5. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c31deeab9..c82ee7ea6 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "async": "0.2.6", "nak": "0.2.12", "spellchecker": "0.2.0", - "pathwatcher": "0.1.4", + "pathwatcher": "0.1.5", "plist": "git://github.com/nathansobo/node-plist.git", "space-pen": "git://github.com/nathansobo/space-pen.git" },