From f91e6c2ab9e630e56a5545557d21de614126a10a Mon Sep 17 00:00:00 2001 From: John Wright Date: Fri, 7 Oct 2016 10:33:08 +0100 Subject: [PATCH 01/19] docs: remove reference to broken beacon --- lib/jumpy-view.coffee | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/jumpy-view.coffee b/lib/jumpy-view.coffee index fed019c..3e620f6 100644 --- a/lib/jumpy-view.coffee +++ b/lib/jumpy-view.coffee @@ -1,9 +1,8 @@ -# FIXME: Beacon code (currently broken in shadow). This will probably return -# in the form of a decoration with a "flash", not sure yet. # TODO: Merge in @willdady's code for better accuracy. # TODO: Remove space-pen? ### global atom ### +Labels = require './labels' {CompositeDisposable, Point, Range} = require 'atom' {View, $} = require 'space-pen' _ = require 'lodash' From 192dbc0938fa120007260c492ea80b3069ada394 Mon Sep 17 00:00:00 2001 From: John Wright Date: Fri, 7 Oct 2016 10:35:31 +0100 Subject: [PATCH 02/19] refactor: move label logic in to separate module As the label discovery is about to get more complicated, move it in to a separate module. --- lib/jumpy-view.coffee | 110 +++---------------------------- lib/labels.coffee | 148 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 158 insertions(+), 100 deletions(-) create mode 100644 lib/labels.coffee diff --git a/lib/jumpy-view.coffee b/lib/jumpy-view.coffee index 3e620f6..eead602 100644 --- a/lib/jumpy-view.coffee +++ b/lib/jumpy-view.coffee @@ -33,8 +33,8 @@ class JumpyView extends View initialize: () -> @disposables = new CompositeDisposable() - @decorations = [] @commands = new CompositeDisposable() + @labels = new Labels @disposables @commands.add atom.commands.add 'atom-workspace', 'jumpy:toggle': => @toggle() @@ -67,12 +67,9 @@ class JumpyView extends View @disposables.add atom.workspace.observeTextEditors (editor) => editorView = atom.views.getView(editor) return if $(editorView).is ':not(:visible)' - - for decoration in @decorations - element = decoration.getProperties().item - if element.textContent[labelPosition] == character - found = true - return false + if @labels.findByCharacterAndPosition character, labelPosition + found = true + return false return found # Assert: labelPosition will start at 0! @@ -89,11 +86,7 @@ class JumpyView extends View @disposables.add atom.workspace.observeTextEditors (editor) => editorView = atom.views.getView(editor) return if $(editorView).is ':not(:visible)' - - for decoration in @decorations - element = decoration.getProperties().item - if element.textContent.indexOf(@firstChar) != 0 - element.classList.add 'irrelevant' + @labels.markIrrelevant @firstChar else if not @secondChar @secondChar = character @@ -107,8 +100,7 @@ class JumpyView extends View reset: -> @clearKeys() - for decoration in @decorations - decoration.getProperties().item.classList.remove 'irrelevant' + @labels.unmarkIrrelevant() @statusBarJumpy?.classList.remove 'no-match' @statusBarJumpyStatus?.innerHTML = 'Jump Mode!' @@ -133,12 +125,6 @@ class JumpyView extends View @cleared = false # TODO: Can the following few lines be singleton'd up? ie. instance var? - wordsPattern = new RegExp (atom.config.get 'jumpy.matchPattern'), 'g' - fontSize = atom.config.get 'jumpy.fontSize' - fontSize = .75 if isNaN(fontSize) or fontSize > 1 - fontSize = (fontSize * 100) + '%' - highContrast = atom.config.get 'jumpy.highContrast' - @turnOffSlowKeys() @statusBarJumpy?.classList.remove 'no-match' @statusBarJumpy?.innerHTML = @@ -146,73 +132,10 @@ class JumpyView extends View @statusBarJumpyStatus = document.querySelector '#status-bar-jumpy .status' - @allPositions = {} - nextKeys = _.clone keys + @labels.toggle() + @disposables.add atom.workspace.observeTextEditors (editor) => editorView = atom.views.getView(editor) - $editorView = $(editorView) - return if $editorView.is ':not(:visible)' - - # 'jumpy-jump-mode is for keymaps and utilized by tests - editorView.classList.add 'jumpy-jump-mode' - - getVisibleColumnRange = (editorView) -> - charWidth = editorView.getDefaultCharacterWidth() - # FYI: asserts: - # numberOfVisibleColumns = editorView.getWidth() / charWidth - minColumn = (editorView.getScrollLeft() / charWidth) - 1 - maxColumn = editorView.getScrollRight() / charWidth - - return [ - minColumn - maxColumn - ] - - drawLabels = (lineNumber, column) => - return unless nextKeys.length - - keyLabel = nextKeys.shift() - position = {row: lineNumber, column: column} - # creates a reference: - @allPositions[keyLabel] = - editor: editor.id - position: position - - marker = editor.markScreenRange new Range( - new Point(lineNumber, column), - new Point(lineNumber, column)), - invalidate: 'touch' - - labelElement = document.createElement('div') - labelElement.textContent = keyLabel - labelElement.style.fontSize = fontSize - labelElement.classList.add 'jumpy-label' - if highContrast - labelElement.classList.add 'high-contrast' - - decoration = editor.decorateMarker marker, - type: 'overlay' - item: labelElement - position: 'head' - @decorations.push decoration - - [minColumn, maxColumn] = getVisibleColumnRange editorView - rows = editor.getVisibleRowRange() - if rows - [firstVisibleRow, lastVisibleRow] = rows - # TODO: Right now there are issues with lastVisbleRow - for lineNumber in [firstVisibleRow...lastVisibleRow] - lineContents = editor.lineTextForScreenRow(lineNumber) - if editor.isFoldedAtScreenRow(lineNumber) - drawLabels lineNumber, 0 - else - while ((word = wordsPattern.exec(lineContents)) != null) - column = word.index - # Do not do anything... markers etc. - # if the columns are out of bounds... - if column > minColumn && column < maxColumn - drawLabels lineNumber, column - @initializeClearEvents(editorView) clearJumpModeHandler: => @@ -228,12 +151,6 @@ class JumpyView extends View editorView.addEventListener e, @clearJumpModeHandler, true clearJumpMode: -> - clearAllMarkers = => - for decoration in @decorations - decoration.getMarker().destroy() - @decorations = [] # Very important for GC. - # Verifiable in Dev Tools -> Timeline -> Nodes. - if @cleared return @@ -247,12 +164,12 @@ class JumpyView extends View for e in ['blur', 'click'] editorView.removeEventListener e, @clearJumpModeHandler, true atom.keymaps.keyBindings = @backedUpKeyBindings - clearAllMarkers() + @labels.destroy() @disposables?.dispose() @detach() jump: -> - location = @findLocation() + location = @labels.findLocation @firstChar, @secondChar if location == null return @disposables.add atom.workspace.observeTextEditors (currentEditor) => @@ -288,13 +205,6 @@ class JumpyView extends View marker.destroy() , 150 - findLocation: -> - label = "#{@firstChar}#{@secondChar}" - if label of @allPositions - return @allPositions[label] - - return null - # Returns an object that can be retrieved when package is activated serialize: -> diff --git a/lib/labels.coffee b/lib/labels.coffee new file mode 100644 index 0000000..9168b87 --- /dev/null +++ b/lib/labels.coffee @@ -0,0 +1,148 @@ +{CompositeDisposable, Point, Range} = require 'atom' +{$} = require 'space-pen' +_ = require 'lodash' + +lowerCharacters = + (String.fromCharCode(a) for a in ['a'.charCodeAt()..'z'.charCodeAt()]) +upperCharacters = + (String.fromCharCode(a) for a in ['A'.charCodeAt()..'Z'.charCodeAt()]) +keys = [] + +# A little ugly. +# I used itertools.permutation in python. +# Couldn't find a good one in npm. Don't worry this takes < 1ms once. +for c1 in lowerCharacters + for c2 in lowerCharacters + keys.push c1 + c2 +for c1 in upperCharacters + for c2 in lowerCharacters + keys.push c1 + c2 +for c1 in lowerCharacters + for c2 in upperCharacters + keys.push c1 + c2 + +class Labels + constructor: (@disposables = new CompositeDisposable()) -> + @allPositions = {} + @decorations = [] + atom.config.observe 'jumpy.fontSize', @setFontSize + atom.config.observe 'jumpy.matchPattern', @setWordsPattern + atom.config.observe 'jumpy.highContrast', @setHighContrast + + setHighContrast: (value) => + @highContrast = value + + setWordsPattern: (value) => + @matchPattern = new RegExp value, 'g' + + setFontSize: (value) => + value = .75 if isNaN(value) or value > 1 + @fontSize = (value * 100) + '%' + + createLabel: (text) -> + labelElement = document.createElement('span') + labelElement.textContent = text + labelElement.style.fontSize = @fontSize + labelElement.classList.add 'jumpy-label' + labelElement.classList.add 'high-contrast' if @highContrast + labelElement + + toggleTreeView: (keys) -> + elements = document.querySelectorAll( + '.tree-view li.file, .tree-view li.directory.collapsed') + for element in elements + return unless keys.length + label = @createLabel keys.shift() + element.parentNode.insertBefore label, element + + toggleInTextEditors: (keys) -> + @disposables.add atom.workspace.observeTextEditors (editor) => + editorView = atom.views.getView(editor) + $editorView = $(editorView) + return if $editorView.is ':not(:visible)' + + # 'jumpy-jump-mode is for keymaps and utilized by tests + editorView.classList.add 'jumpy-jump-mode' + + getVisibleColumnRange = (editorView) -> + charWidth = editorView.getDefaultCharacterWidth() + # FYI: asserts: + # numberOfVisibleColumns = editorView.getWidth() / charWidth + minColumn = (editorView.getScrollLeft() / charWidth) - 1 + maxColumn = editorView.getScrollRight() / charWidth + + return [ + minColumn + maxColumn + ] + + drawLabels = (lineNumber, column) => + return unless keys.length + + keyLabel = keys.shift() + position = {row: lineNumber, column: column} + # creates a reference: + @allPositions[keyLabel] = + editor: editor.id + position: position + + marker = editor.markScreenRange new Range( + new Point(lineNumber, column), + new Point(lineNumber, column)), + invalidate: 'touch' + + decoration = editor.decorateMarker marker, + type: 'overlay' + item: @createLabel keyLabel + position: 'head' + + @decorations.push decoration + + [minColumn, maxColumn] = getVisibleColumnRange editorView + rows = editor.getVisibleRowRange() + return unless rows + + [firstVisibleRow, lastVisibleRow] = rows + # TODO: Right now there are issues with lastVisbleRow + for lineNumber in [firstVisibleRow...lastVisibleRow] + lineContents = editor.lineTextForScreenRow(lineNumber) + if editor.isFoldedAtScreenRow(lineNumber) + drawLabels lineNumber, 0 + else + while ((word = @matchPattern.exec(lineContents)) != null) + column = word.index + # Do not do anything... markers etc. + # if the columns are out of bounds... + if column > minColumn && column < maxColumn + drawLabels lineNumber, column + + toggle: -> + nextKeys = _.clone keys + @toggleInTextEditors nextKeys + + destroy: -> + decoration.getMarker().destroy() for decoration in @decorations + @decorations = [] # Very important for GC. + # Verifiable in Dev Tools -> Timeline -> Nodes. + + findLocation: (firstChar, secondChar) -> + label = "#{firstChar}#{secondChar}" + return @allPositions[label] if label of @allPositions + null + + markIrrelevant: (firstChar) -> + for decoration in @decorations + element = decoration.getProperties().item + if element.textContent.indexOf(firstChar) != 0 + element.classList.add 'irrelevant' + + unmarkIrrelevant: -> + for decoration in @decorations + decoration.getProperties().item.classList.remove 'irrelevant' + + findByCharacterAndPosition: (character, position) -> + for decoration in @decorations + element = decoration.getProperties().item + return decoration if element.textContent[position] == character + +module.exports = Labels From 473af0b1883be6c2de1d1cf51021706f7af40cde Mon Sep 17 00:00:00 2001 From: John Wright Date: Fri, 7 Oct 2016 13:52:09 +0100 Subject: [PATCH 03/19] feat: extend jumpy to tree-view --- keymaps/jumpy.cson | 5 +- lib/jumpy-view.coffee | 59 +++------- lib/label-manager-iterator.coffee | 73 ++++++++++++ lib/label-manager.coffee | 32 +++++ .../text-editor.coffee} | 110 ++++++++---------- lib/label-managers/tree-view.coffee | 61 ++++++++++ spec/jumpy-spec.coffee | 2 +- styles/.atom-text-editor.less | 5 + 8 files changed, 235 insertions(+), 112 deletions(-) create mode 100644 lib/label-manager-iterator.coffee create mode 100644 lib/label-manager.coffee rename lib/{labels.coffee => label-managers/text-editor.coffee} (61%) create mode 100644 lib/label-managers/tree-view.coffee diff --git a/keymaps/jumpy.cson b/keymaps/jumpy.cson index 874e00c..a44f992 100644 --- a/keymaps/jumpy.cson +++ b/keymaps/jumpy.cson @@ -7,10 +7,11 @@ # For more detailed documentation see # https://atom.io/docs/latest/advanced/keymaps -'atom-workspace atom-text-editor:not(.mini)': +'*:not(.jumpy-jump-mode) atom-workspace atom-text-editor:not(.mini), + *:not(.jumpy-jump-mode) atom-workspace .tree-view': 'shift-enter': 'jumpy:toggle' -'atom-workspace atom-text-editor.jumpy-jump-mode': +'.jumpy-jump-mode atom-workspace': 'backspace': 'jumpy:reset' 'enter': 'jumpy:clear' 'space': 'jumpy:clear' diff --git a/lib/jumpy-view.coffee b/lib/jumpy-view.coffee index eead602..15507bb 100644 --- a/lib/jumpy-view.coffee +++ b/lib/jumpy-view.coffee @@ -2,7 +2,7 @@ # TODO: Remove space-pen? ### global atom ### -Labels = require './labels' +LabelManagerIterator = require './label-manager-iterator' {CompositeDisposable, Point, Range} = require 'atom' {View, $} = require 'space-pen' _ = require 'lodash' @@ -34,7 +34,7 @@ class JumpyView extends View initialize: () -> @disposables = new CompositeDisposable() @commands = new CompositeDisposable() - @labels = new Labels @disposables + @labelManager = new LabelManagerIterator @disposables @commands.add atom.commands.add 'atom-workspace', 'jumpy:toggle': => @toggle() @@ -67,9 +67,9 @@ class JumpyView extends View @disposables.add atom.workspace.observeTextEditors (editor) => editorView = atom.views.getView(editor) return if $(editorView).is ':not(:visible)' - if @labels.findByCharacterAndPosition character, labelPosition - found = true - return false + found = @labelManager.findByCharacterAndPosition( + character, labelPosition) + return false if found return found # Assert: labelPosition will start at 0! @@ -86,7 +86,7 @@ class JumpyView extends View @disposables.add atom.workspace.observeTextEditors (editor) => editorView = atom.views.getView(editor) return if $(editorView).is ':not(:visible)' - @labels.markIrrelevant @firstChar + @labelManager.markIrrelevant @firstChar else if not @secondChar @secondChar = character @@ -100,7 +100,7 @@ class JumpyView extends View reset: -> @clearKeys() - @labels.unmarkIrrelevant() + @labelManager.unmarkIrrelevant() @statusBarJumpy?.classList.remove 'no-match' @statusBarJumpyStatus?.innerHTML = 'Jump Mode!' @@ -124,6 +124,9 @@ class JumpyView extends View # Set dirty for @clearJumpMode @cleared = false + # 'jumpy-jump-mode is for keymaps and utilized by tests + document.body.classList.add 'jumpy-jump-mode' + # TODO: Can the following few lines be singleton'd up? ie. instance var? @turnOffSlowKeys() @statusBarJumpy?.classList.remove 'no-match' @@ -132,7 +135,7 @@ class JumpyView extends View @statusBarJumpyStatus = document.querySelector '#status-bar-jumpy .status' - @labels.toggle() + @labelManager.toggle() @disposables.add atom.workspace.observeTextEditors (editor) => editorView = atom.views.getView(editor) @@ -160,50 +163,16 @@ class JumpyView extends View @disposables.add atom.workspace.observeTextEditors (editor) => editorView = atom.views.getView(editor) - editorView.classList.remove 'jumpy-jump-mode' + document.body.classList.remove 'jumpy-jump-mode' for e in ['blur', 'click'] editorView.removeEventListener e, @clearJumpModeHandler, true atom.keymaps.keyBindings = @backedUpKeyBindings - @labels.destroy() + @labelManager.destroy() @disposables?.dispose() @detach() jump: -> - location = @labels.findLocation @firstChar, @secondChar - if location == null - return - @disposables.add atom.workspace.observeTextEditors (currentEditor) => - editorView = atom.views.getView(currentEditor) - - # Prevent other editors from jumping cursors as well - # TODO: make a test for this return if - return if currentEditor.id != location.editor - - pane = atom.workspace.paneForItem(currentEditor) - pane.activate() - - isVisualMode = editorView.classList.contains 'visual-mode' - isSelected = (currentEditor.getSelections().length == 1 && - currentEditor.getSelectedText() != '') - if (isVisualMode || isSelected) - currentEditor.selectToScreenPosition location.position - else - currentEditor.setCursorScreenPosition location.position - - if atom.config.get 'jumpy.useHomingBeaconEffectOnJumps' - @drawBeacon currentEditor, location - - drawBeacon: (editor, location) -> - range = Range location.position, location.position - marker = editor.markScreenRange range, invalidate: 'never' - beacon = document.createElement 'span' - beacon.classList.add 'beacon' - editor.decorateMarker marker, - item: beacon, - type: 'overlay' - setTimeout -> - marker.destroy() - , 150 + @labelManager.jumpTo @firstChar, @secondChar # Returns an object that can be retrieved when package is activated serialize: -> diff --git a/lib/label-manager-iterator.coffee b/lib/label-manager-iterator.coffee new file mode 100644 index 0000000..c488bc8 --- /dev/null +++ b/lib/label-manager-iterator.coffee @@ -0,0 +1,73 @@ +{CompositeDisposable, Point, Range} = require 'atom' +fs = require 'fs' +pathHelper = require 'path' +_ = require 'lodash' + +LABEL_MANAGER_PATH = pathHelper.join __dirname, 'label-managers' +labelManagers = fs + .readdirSync(LABEL_MANAGER_PATH) + .map((file) -> require(pathHelper.join LABEL_MANAGER_PATH, file)) + +lowerCharacters = + (String.fromCharCode(a) for a in ['a'.charCodeAt()..'z'.charCodeAt()]) +upperCharacters = + (String.fromCharCode(a) for a in ['A'.charCodeAt()..'Z'.charCodeAt()]) +keys = [] + +# A little ugly. +# I used itertools.permutation in python. +# Couldn't find a good one in npm. Don't worry this takes < 1ms once. +for c1 in lowerCharacters + for c2 in lowerCharacters + keys.push c1 + c2 +for c1 in upperCharacters + for c2 in lowerCharacters + keys.push c1 + c2 +for c1 in lowerCharacters + for c2 in upperCharacters + keys.push c1 + c2 + +class LabelManagerIterator + constructor: (disposables) -> + @clickableLabels = [] + @labelManagers = labelManagers.map((Manager) -> new Manager disposables) + atom.config.observe 'jumpy.fontSize', @setFontSize + atom.config.observe 'jumpy.matchPattern', @setWordsPattern + atom.config.observe 'jumpy.highContrast', @setHighContrast + + setHighContrast: (value) => + manager.highContrast = value for manager in @labelManagers + + setWordsPattern: (value) => + value = new RegExp value, 'g' + manager.matchPattern = value for manager in @labelManagers + + setFontSize: (value) => + value = .75 if isNaN(value) or value > 1 + value = (value * 100) + '%' + manager.fontSize = value for manager in @labelManagers + + toggle: -> + nextKeys = _.clone keys + manager.toggle nextKeys for manager in @labelManagers + + jumpTo: (firstChar, secondChar) -> + manager.jumpTo firstChar, secondChar for manager in @labelManagers + + destroy: -> + manager.destroy() for manager in @labelManagers + + markIrrelevant: (firstChar) -> + manager.markIrrelevant firstChar for manager in @labelManagers + + unmarkIrrelevant: -> + manager.unmarkIrrelevant() for manager in @labelManagers + + findByCharacterAndPosition: (character, position) -> + found = null + for manager in @labelManagers + found = manager.findByCharacterAndPosition character, position + break if found + found + +module.exports = LabelManagerIterator diff --git a/lib/label-manager.coffee b/lib/label-manager.coffee new file mode 100644 index 0000000..c827ad1 --- /dev/null +++ b/lib/label-manager.coffee @@ -0,0 +1,32 @@ +abstractMethod = (name) ->-> + throw new Error "The abstract method #{name} needs to be created" + +class LabelManager + constructor: (@disposables) -> + + createLabel: (text) -> + labelElement = document.createElement('span') + labelElement.textContent = text + labelElement.style.fontSize = @fontSize + labelElement.classList.add 'jumpy-label' + labelElement.classList.add 'high-contrast' if @highContrast + labelElement + + createBeacon: -> + beacon = document.createElement 'span' + beacon.classList.add 'beacon' + beacon + + toggle: abstractMethod 'toggle' + + destroy: abstractMethod 'destroy' + + markIrrelevant: abstractMethod 'markIrrelevant' + + unmarkIrrelevant: abstractMethod 'unmarkIrrelevant' + + findByCharacterAndPosition: abstractMethod 'findByCharacterAndPosition' + + jumpTo: abstractMethod 'jumpTo' + +module.exports = LabelManager diff --git a/lib/labels.coffee b/lib/label-managers/text-editor.coffee similarity index 61% rename from lib/labels.coffee rename to lib/label-managers/text-editor.coffee index 9168b87..d55ded0 100644 --- a/lib/labels.coffee +++ b/lib/label-managers/text-editor.coffee @@ -1,69 +1,19 @@ -{CompositeDisposable, Point, Range} = require 'atom' +{Point, Range} = require 'atom' {$} = require 'space-pen' -_ = require 'lodash' - -lowerCharacters = - (String.fromCharCode(a) for a in ['a'.charCodeAt()..'z'.charCodeAt()]) -upperCharacters = - (String.fromCharCode(a) for a in ['A'.charCodeAt()..'Z'.charCodeAt()]) -keys = [] - -# A little ugly. -# I used itertools.permutation in python. -# Couldn't find a good one in npm. Don't worry this takes < 1ms once. -for c1 in lowerCharacters - for c2 in lowerCharacters - keys.push c1 + c2 -for c1 in upperCharacters - for c2 in lowerCharacters - keys.push c1 + c2 -for c1 in lowerCharacters - for c2 in upperCharacters - keys.push c1 + c2 - -class Labels - constructor: (@disposables = new CompositeDisposable()) -> +LabelManager = require '../label-manager' + +class TextEditorLabelManager extends LabelManager + constructor: (args...) -> + super args... @allPositions = {} @decorations = [] - atom.config.observe 'jumpy.fontSize', @setFontSize - atom.config.observe 'jumpy.matchPattern', @setWordsPattern - atom.config.observe 'jumpy.highContrast', @setHighContrast - - setHighContrast: (value) => - @highContrast = value - - setWordsPattern: (value) => - @matchPattern = new RegExp value, 'g' - - setFontSize: (value) => - value = .75 if isNaN(value) or value > 1 - @fontSize = (value * 100) + '%' - - createLabel: (text) -> - labelElement = document.createElement('span') - labelElement.textContent = text - labelElement.style.fontSize = @fontSize - labelElement.classList.add 'jumpy-label' - labelElement.classList.add 'high-contrast' if @highContrast - labelElement - - toggleTreeView: (keys) -> - elements = document.querySelectorAll( - '.tree-view li.file, .tree-view li.directory.collapsed') - for element in elements - return unless keys.length - label = @createLabel keys.shift() - element.parentNode.insertBefore label, element - - toggleInTextEditors: (keys) -> + + toggle: (keys) -> @disposables.add atom.workspace.observeTextEditors (editor) => editorView = atom.views.getView(editor) $editorView = $(editorView) return if $editorView.is ':not(:visible)' - # 'jumpy-jump-mode is for keymaps and utilized by tests - editorView.classList.add 'jumpy-jump-mode' - getVisibleColumnRange = (editorView) -> charWidth = editorView.getDefaultCharacterWidth() # FYI: asserts: @@ -116,9 +66,41 @@ class Labels if column > minColumn && column < maxColumn drawLabels lineNumber, column - toggle: -> - nextKeys = _.clone keys - @toggleInTextEditors nextKeys + jumpTo: (firstChar, secondChar) -> + location = @findLocation firstChar, secondChar + if location == null + return + @disposables.add atom.workspace.observeTextEditors (currentEditor) => + editorView = atom.views.getView(currentEditor) + + # Prevent other editors from jumping cursors as well + # TODO: make a test for this return if + return if currentEditor.id != location.editor + + pane = atom.workspace.paneForItem(currentEditor) + pane.activate() + + isVisualMode = editorView.classList.contains 'visual-mode' + isSelected = (currentEditor.getSelections().length == 1 && + currentEditor.getSelectedText() != '') + if (isVisualMode || isSelected) + currentEditor.selectToScreenPosition location.position + else + currentEditor.setCursorScreenPosition location.position + + if atom.config.get 'jumpy.useHomingBeaconEffectOnJumps' + @drawBeacon currentEditor, location + + drawBeacon: (editor, location) -> + range = Range location.position, location.position + marker = editor.markScreenRange range, invalidate: 'never' + beacon = @createBeacon() + editor.decorateMarker marker, + item: beacon, + type: 'overlay' + setTimeout -> + marker.destroy() + , 150 destroy: -> decoration.getMarker().destroy() for decoration in @decorations @@ -127,8 +109,7 @@ class Labels findLocation: (firstChar, secondChar) -> label = "#{firstChar}#{secondChar}" - return @allPositions[label] if label of @allPositions - null + @allPositions[label] || null markIrrelevant: (firstChar) -> for decoration in @decorations @@ -144,5 +125,6 @@ class Labels for decoration in @decorations element = decoration.getProperties().item return decoration if element.textContent[position] == character + null -module.exports = Labels +module.exports = TextEditorLabelManager diff --git a/lib/label-managers/tree-view.coffee b/lib/label-managers/tree-view.coffee new file mode 100644 index 0000000..0ad12ac --- /dev/null +++ b/lib/label-managers/tree-view.coffee @@ -0,0 +1,61 @@ +LabelManager = require '../label-manager' + +triggerMouseEvent = (element, eventType) -> + clickEvent = document.createEvent 'MouseEvents' + clickEvent.initEvent eventType, true, true + element.dispatchEvent clickEvent + +class TreeViewManager extends LabelManager + constructor: (args...) -> + super args... + @locations = [] + + toggle: (keys) -> + elements = document.querySelectorAll '.tree-view *[data-path]' + for element in elements + return unless keys.length + label = @createLabel keys.shift() + @locations.push {label, element} + element.parentNode.insertBefore label, element + + destroy: -> + while location = @locations.shift() + location.label.parentNode.removeChild location.label + + drawBeacon: ({element}) -> + beacon = @createBeacon() + parent = element.parentNode + parent.insertBefore beacon, element + setTimeout -> + parent.removeChild beacon + , 150 + + jumpTo: (firstChar, secondChar) -> + match = "#{firstChar}#{secondChar}" + location = @locations.find(({label}) -> label.textContent is match) + return unless location + @drawBeacon location + @select location + + select: ({element}) -> + atom.commands.dispatch( + document.querySelector('atom-workspace'), + 'tree-view:show' + ) + triggerMouseEvent element, 'mousedown' + atom.commands.dispatch element, 'tree-view:open-selected-entry' + + markIrrelevant: (firstChar) -> + @locations + .filter(({label}) -> not label.textContent.includes firstChar) + .forEach(({label}) -> label.classList.add 'irrelevant') + + unmarkIrrelevant: -> + label.classList.remove 'irrelevant' for {label} in @locations + + findByCharacterAndPosition: (character, position) -> + for {label} in @locations + return label if label.textContent[position] is character + null + +module.exports = TreeViewManager diff --git a/spec/jumpy-spec.coffee b/spec/jumpy-spec.coffee index 1e71837..bf0ac6a 100644 --- a/spec/jumpy-spec.coffee +++ b/spec/jumpy-spec.coffee @@ -169,7 +169,7 @@ describe "Jumpy", -> expect(cursorPosition.column).toBe 6 expect(textEditor.getSelectedText()).toBe '' it "clears jumpy mode", -> - expect(textEditorElement + expect(atom.document.body .classList.contains('jumpy-jump-mode')).toBeTruthy() atom.commands.dispatch workspaceElement, 'jumpy:a' atom.commands.dispatch workspaceElement, 'jumpy:c' diff --git a/styles/.atom-text-editor.less b/styles/.atom-text-editor.less index 227909d..cd72a9d 100644 --- a/styles/.atom-text-editor.less +++ b/styles/.atom-text-editor.less @@ -23,6 +23,11 @@ &.high-contrast { background-color: @background-color-success; } + + .tree-view & { + transform: translate(0, 0); + z-index: 11; + } } @import "beacon.less"; From 53e1a0a95e37bedb49e7e8f1c44578f236f2c902 Mon Sep 17 00:00:00 2001 From: John Wright Date: Fri, 7 Oct 2016 14:20:29 +0100 Subject: [PATCH 04/19] style: unused vars --- lib/label-manager-iterator.coffee | 2 +- lib/label-managers/text-editor.coffee | 4 ++-- lib/label-managers/tree-view.coffee | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/label-manager-iterator.coffee b/lib/label-manager-iterator.coffee index c488bc8..734f1ea 100644 --- a/lib/label-manager-iterator.coffee +++ b/lib/label-manager-iterator.coffee @@ -1,4 +1,4 @@ -{CompositeDisposable, Point, Range} = require 'atom' +{Point, Range} = require 'atom' fs = require 'fs' pathHelper = require 'path' _ = require 'lodash' diff --git a/lib/label-managers/text-editor.coffee b/lib/label-managers/text-editor.coffee index d55ded0..2de6072 100644 --- a/lib/label-managers/text-editor.coffee +++ b/lib/label-managers/text-editor.coffee @@ -3,8 +3,8 @@ LabelManager = require '../label-manager' class TextEditorLabelManager extends LabelManager - constructor: (args...) -> - super args... + constructor: -> + super @allPositions = {} @decorations = [] diff --git a/lib/label-managers/tree-view.coffee b/lib/label-managers/tree-view.coffee index 0ad12ac..ae6975a 100644 --- a/lib/label-managers/tree-view.coffee +++ b/lib/label-managers/tree-view.coffee @@ -6,8 +6,8 @@ triggerMouseEvent = (element, eventType) -> element.dispatchEvent clickEvent class TreeViewManager extends LabelManager - constructor: (args...) -> - super args... + constructor: -> + super @locations = [] toggle: (keys) -> From c26ea79d29676b0e27574fac4a6858032d64d8a0 Mon Sep 17 00:00:00 2001 From: John Wright Date: Fri, 7 Oct 2016 14:21:07 +0100 Subject: [PATCH 05/19] fix: check that labels **start with** chars --- lib/label-managers/tree-view.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/label-managers/tree-view.coffee b/lib/label-managers/tree-view.coffee index ae6975a..8d69f6b 100644 --- a/lib/label-managers/tree-view.coffee +++ b/lib/label-managers/tree-view.coffee @@ -47,7 +47,7 @@ class TreeViewManager extends LabelManager markIrrelevant: (firstChar) -> @locations - .filter(({label}) -> not label.textContent.includes firstChar) + .filter(({label}) -> not label.textContent.startsWith firstChar) .forEach(({label}) -> label.classList.add 'irrelevant') unmarkIrrelevant: -> From 568adf91d70105648685445bb1fd6cc8b9774540 Mon Sep 17 00:00:00 2001 From: John Wright Date: Fri, 7 Oct 2016 14:25:10 +0100 Subject: [PATCH 06/19] refactor: dispatch tree-view:show on any element --- lib/label-managers/tree-view.coffee | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/label-managers/tree-view.coffee b/lib/label-managers/tree-view.coffee index 8d69f6b..40da71d 100644 --- a/lib/label-managers/tree-view.coffee +++ b/lib/label-managers/tree-view.coffee @@ -38,10 +38,7 @@ class TreeViewManager extends LabelManager @select location select: ({element}) -> - atom.commands.dispatch( - document.querySelector('atom-workspace'), - 'tree-view:show' - ) + atom.commands.dispatch element, 'tree-view:show' triggerMouseEvent element, 'mousedown' atom.commands.dispatch element, 'tree-view:open-selected-entry' From 948cf518a583285b7c910293f92bd71d5fde1eb2 Mon Sep 17 00:00:00 2001 From: John Wright Date: Fri, 7 Oct 2016 16:48:25 +0100 Subject: [PATCH 07/19] style: :art: --- lib/label-managers/tree-view.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/label-managers/tree-view.coffee b/lib/label-managers/tree-view.coffee index 40da71d..99a65ca 100644 --- a/lib/label-managers/tree-view.coffee +++ b/lib/label-managers/tree-view.coffee @@ -32,7 +32,7 @@ class TreeViewManager extends LabelManager jumpTo: (firstChar, secondChar) -> match = "#{firstChar}#{secondChar}" - location = @locations.find(({label}) -> label.textContent is match) + location = @locations.find ({label}) -> label.textContent is match return unless location @drawBeacon location @select location From af2767e6f95ce5fe85697ad2c6b23869df1eb27b Mon Sep 17 00:00:00 2001 From: John Wright Date: Fri, 7 Oct 2016 16:49:10 +0100 Subject: [PATCH 08/19] refactor: keeping all the key references in one place --- lib/jumpy-view.coffee | 31 ++++++------------------------- lib/label-manager-iterator.coffee | 5 ++++- 2 files changed, 10 insertions(+), 26 deletions(-) diff --git a/lib/jumpy-view.coffee b/lib/jumpy-view.coffee index 15507bb..d49bbd1 100644 --- a/lib/jumpy-view.coffee +++ b/lib/jumpy-view.coffee @@ -3,29 +3,10 @@ ### global atom ### LabelManagerIterator = require './label-manager-iterator' -{CompositeDisposable, Point, Range} = require 'atom' +{CompositeDisposable} = require 'atom' {View, $} = require 'space-pen' _ = require 'lodash' -lowerCharacters = - (String.fromCharCode(a) for a in ['a'.charCodeAt()..'z'.charCodeAt()]) -upperCharacters = - (String.fromCharCode(a) for a in ['A'.charCodeAt()..'Z'.charCodeAt()]) -keys = [] - -# A little ugly. -# I used itertools.permutation in python. -# Couldn't find a good one in npm. Don't worry this takes < 1ms once. -for c1 in lowerCharacters - for c2 in lowerCharacters - keys.push c1 + c2 -for c1 in upperCharacters - for c2 in lowerCharacters - keys.push c1 + c2 -for c1 in lowerCharacters - for c2 in upperCharacters - keys.push c1 + c2 - class JumpyView extends View @content: -> @@ -34,17 +15,17 @@ class JumpyView extends View initialize: () -> @disposables = new CompositeDisposable() @commands = new CompositeDisposable() - @labelManager = new LabelManagerIterator @disposables + @labelManager = new LabelManagerIterator @disposables, @commands @commands.add atom.commands.add 'atom-workspace', 'jumpy:toggle': => @toggle() 'jumpy:reset': => @reset() 'jumpy:clear': => @clearJumpMode() - commands = {} - for characterSet in [lowerCharacters, upperCharacters] - for c in characterSet - do (c) => commands['jumpy:' + c] = => @getKey(c) + commands = LabelManagerIterator.chars.reduce( + (commands, c) => _.set(commands, "jumpy:#{c}", => @getKey c), + {} + ) @commands.add atom.commands.add 'atom-workspace', commands # TODO: consider moving this into toggle for new bindings. diff --git a/lib/label-manager-iterator.coffee b/lib/label-manager-iterator.coffee index 734f1ea..180b287 100644 --- a/lib/label-manager-iterator.coffee +++ b/lib/label-manager-iterator.coffee @@ -28,7 +28,10 @@ for c1 in lowerCharacters keys.push c1 + c2 class LabelManagerIterator - constructor: (disposables) -> + @keys: keys + @chars: lowerCharacters.concat upperCharacters + + constructor: (disposables, commands) -> @clickableLabels = [] @labelManagers = labelManagers.map((Manager) -> new Manager disposables) atom.config.observe 'jumpy.fontSize', @setFontSize From b3594c14992a6c6a0604a9c80f47c3988aabfaa8 Mon Sep 17 00:00:00 2001 From: John Wright Date: Fri, 7 Oct 2016 16:49:41 +0100 Subject: [PATCH 09/19] fix: :arrow_up: dependencies for last commit --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c14eaeb..0bb5ec6 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "atom": ">0.50.0" }, "dependencies": { - "lodash": "^2.4.1", + "lodash": "^4.16.4", "space-pen": "^4.2.2" } } From 4dd52112ab0b2bde4f8c233700cce0bbba948d5a Mon Sep 17 00:00:00 2001 From: John Wright Date: Fri, 7 Oct 2016 16:52:06 +0100 Subject: [PATCH 10/19] fix: clear up labels after they're no longer needed --- lib/jumpy-view.coffee | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/lib/jumpy-view.coffee b/lib/jumpy-view.coffee index d49bbd1..3b8c293 100644 --- a/lib/jumpy-view.coffee +++ b/lib/jumpy-view.coffee @@ -72,8 +72,8 @@ class JumpyView extends View @secondChar = character if @secondChar - @jump() # Jump first. Currently need the placement of the labels. - @clearJumpMode() + @jump() # Jump first. Currently need the placement of the labels. + _.defer @clearJumpModeHandler clearKeys: -> @firstChar = null @@ -126,13 +126,12 @@ class JumpyView extends View @clearJumpMode() initializeClearEvents: (editorView) -> - @disposables.add editorView.onDidChangeScrollTop => - @clearJumpModeHandler() - @disposables.add editorView.onDidChangeScrollLeft => - @clearJumpModeHandler() + @disposables.add editorView.onDidChangeScrollTop @clearJumpModeHandler + @disposables.add editorView.onDidChangeScrollLeft @clearJumpModeHandler for e in ['blur', 'click'] - editorView.addEventListener e, @clearJumpModeHandler, true + editorView.addEventListener(e, _.debounce(@clearJumpModeHandler), + true) clearJumpMode: -> if @cleared From 61e5a9e1368115d571dab96f6ac281f9c5ff87de Mon Sep 17 00:00:00 2001 From: John Wright Date: Fri, 7 Oct 2016 17:13:08 +0100 Subject: [PATCH 11/19] fix: tree-view jumping without editor --- keymaps/jumpy.cson | 2 +- lib/jumpy-view.coffee | 12 +----------- lib/label-manager-iterator.coffee | 4 ++-- lib/label-manager.coffee | 2 +- lib/label-managers/text-editor.coffee | 9 +++++++++ lib/label-managers/tree-view.coffee | 2 +- 6 files changed, 15 insertions(+), 16 deletions(-) diff --git a/keymaps/jumpy.cson b/keymaps/jumpy.cson index a44f992..12d6069 100644 --- a/keymaps/jumpy.cson +++ b/keymaps/jumpy.cson @@ -8,7 +8,7 @@ # For more detailed documentation see # https://atom.io/docs/latest/advanced/keymaps '*:not(.jumpy-jump-mode) atom-workspace atom-text-editor:not(.mini), - *:not(.jumpy-jump-mode) atom-workspace .tree-view': + *:not(.jumpy-jump-mode) atom-workspace': 'shift-enter': 'jumpy:toggle' '.jumpy-jump-mode atom-workspace': diff --git a/lib/jumpy-view.coffee b/lib/jumpy-view.coffee index 3b8c293..e4b8eeb 100644 --- a/lib/jumpy-view.coffee +++ b/lib/jumpy-view.coffee @@ -43,19 +43,9 @@ class JumpyView extends View getKey: (character) -> @statusBarJumpy?.classList.remove 'no-match' - isMatchOfCurrentLabels = (character, labelPosition) => - found = false - @disposables.add atom.workspace.observeTextEditors (editor) => - editorView = atom.views.getView(editor) - return if $(editorView).is ':not(:visible)' - found = @labelManager.findByCharacterAndPosition( - character, labelPosition) - return false if found - return found - # Assert: labelPosition will start at 0! labelPosition = (if not @firstChar then 0 else 1) - if !isMatchOfCurrentLabels character, labelPosition + if not @labelManager.isMatchOfCurrentLabels character, labelPosition @statusBarJumpy?.classList.add 'no-match' @statusBarJumpyStatus?.innerHTML = 'No match!' return diff --git a/lib/label-manager-iterator.coffee b/lib/label-manager-iterator.coffee index 180b287..e000f61 100644 --- a/lib/label-manager-iterator.coffee +++ b/lib/label-manager-iterator.coffee @@ -66,10 +66,10 @@ class LabelManagerIterator unmarkIrrelevant: -> manager.unmarkIrrelevant() for manager in @labelManagers - findByCharacterAndPosition: (character, position) -> + isMatchOfCurrentLabels: (character, position) -> found = null for manager in @labelManagers - found = manager.findByCharacterAndPosition character, position + found = manager.isMatchOfCurrentLabels character, position break if found found diff --git a/lib/label-manager.coffee b/lib/label-manager.coffee index c827ad1..07124d4 100644 --- a/lib/label-manager.coffee +++ b/lib/label-manager.coffee @@ -25,7 +25,7 @@ class LabelManager unmarkIrrelevant: abstractMethod 'unmarkIrrelevant' - findByCharacterAndPosition: abstractMethod 'findByCharacterAndPosition' + isMatchOfCurrentLabels: abstractMethod 'isMatchOfCurrentLabels' jumpTo: abstractMethod 'jumpTo' diff --git a/lib/label-managers/text-editor.coffee b/lib/label-managers/text-editor.coffee index 2de6072..8580b99 100644 --- a/lib/label-managers/text-editor.coffee +++ b/lib/label-managers/text-editor.coffee @@ -127,4 +127,13 @@ class TextEditorLabelManager extends LabelManager return decoration if element.textContent[position] == character null + isMatchOfCurrentLabels: (character, labelPosition) => + found = false + @disposables.add atom.workspace.observeTextEditors (editor) => + editorView = atom.views.getView(editor) + return if $(editorView).is ':not(:visible)' + found = @findByCharacterAndPosition character, labelPosition + return false if found + return !!found + module.exports = TextEditorLabelManager diff --git a/lib/label-managers/tree-view.coffee b/lib/label-managers/tree-view.coffee index 99a65ca..47fab9f 100644 --- a/lib/label-managers/tree-view.coffee +++ b/lib/label-managers/tree-view.coffee @@ -50,7 +50,7 @@ class TreeViewManager extends LabelManager unmarkIrrelevant: -> label.classList.remove 'irrelevant' for {label} in @locations - findByCharacterAndPosition: (character, position) -> + isMatchOfCurrentLabels: (character, position) -> for {label} in @locations return label if label.textContent[position] is character null From 87249604652d41362b494219ebb7b3a9ed3b4086 Mon Sep 17 00:00:00 2001 From: John Wright Date: Tue, 11 Oct 2016 16:30:39 +0100 Subject: [PATCH 12/19] test: started tests for tree-view jumping --- package.json | 2 +- spec/jumpy-tree-view-spec.coffee | 48 ++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 spec/jumpy-tree-view-spec.coffee diff --git a/package.json b/package.json index 0bb5ec6..20cc6d9 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "extension" ], "activationCommands": { - "atom-text-editor": [ + "atom-workspace": [ "jumpy:toggle" ] }, diff --git a/spec/jumpy-tree-view-spec.coffee b/spec/jumpy-tree-view-spec.coffee new file mode 100644 index 0000000..4cb3a16 --- /dev/null +++ b/spec/jumpy-tree-view-spec.coffee @@ -0,0 +1,48 @@ +### global +atom jasmine describe xdescribe beforeEach it runs waitsForPromise +### + +path = require 'path' + +NUM_FILES = 3 +NUM_DIRS = 1 +DIR = path.join __dirname, 'fixtures' + +fdescribe 'jumpy-tree-view', -> + + {workspaceElement} = {} + + beforeEach -> + atom.project.setPaths [DIR] + workspaceElement = atom.views.getView atom.workspace + workspaceElement.style.height = '5000px' + workspaceElement.style.width = '5000px' + jasmine.attachToDOM workspaceElement + waitsForPromise -> atom.packages.activatePackage 'tree-view' + runs -> atom.commands.dispatch 'tree-view:show' + waitsForPromise -> + promise = atom.packages.activatePackage 'jumpy' + atom.commands.dispatch workspaceElement, 'jumpy:toggle' + promise + + afterEach -> + atom.commands.dispatch workspaceElement, 'jumpy:clear' + + it 'adds labels to each element in the tree', -> + labels = atom.document.querySelectorAll '.tree-view .jumpy-label' + expect(labels.length).toBe NUM_DIRS + NUM_FILES + + xit 'will open a file when selected with jumpy', -> + # TODO: This doesn't work. Maybe use spies & stubs instead. + atom.commands.dispatch workspaceElement, 'jump:a' + atom.commands.dispatch workspaceElement, 'jump:c' + editor = atom.workspace.getActivePaneItem() + file = editor?.buffer.file.path + expect(file).toBe path.join DIR, 'test_text.md' + + it 'will open/close directories when selected with jumpy', -> + dir = atom.document.querySelector '.tree-view .directory' + expect(dir.classList.contains 'expanded').toBe true + atom.commands.dispatch workspaceElement, 'jumpy:a' + atom.commands.dispatch workspaceElement, 'jumpy:a' + expect(dir.classList.contains 'collapsed').toBe true From 26f5edf651e838ef80d01fd31b19c6eabc7c7509 Mon Sep 17 00:00:00 2001 From: John Wright Date: Tue, 18 Oct 2016 10:07:55 +0100 Subject: [PATCH 13/19] :art: --- lib/label-managers/tree-view.coffee | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/lib/label-managers/tree-view.coffee b/lib/label-managers/tree-view.coffee index 47fab9f..3f1432c 100644 --- a/lib/label-managers/tree-view.coffee +++ b/lib/label-managers/tree-view.coffee @@ -12,23 +12,18 @@ class TreeViewManager extends LabelManager toggle: (keys) -> elements = document.querySelectorAll '.tree-view *[data-path]' - for element in elements - return unless keys.length + for element in elements when keys.length label = @createLabel keys.shift() @locations.push {label, element} element.parentNode.insertBefore label, element destroy: -> - while location = @locations.shift() - location.label.parentNode.removeChild location.label + location.label.remove() while location = @locations.shift() drawBeacon: ({element}) -> beacon = @createBeacon() - parent = element.parentNode - parent.insertBefore beacon, element - setTimeout -> - parent.removeChild beacon - , 150 + element.parentNode.insertBefore beacon, element + setTimeout beacon.remove.bind(beacon), 2000 jumpTo: (firstChar, secondChar) -> match = "#{firstChar}#{secondChar}" @@ -51,8 +46,6 @@ class TreeViewManager extends LabelManager label.classList.remove 'irrelevant' for {label} in @locations isMatchOfCurrentLabels: (character, position) -> - for {label} in @locations - return label if label.textContent[position] is character - null + @locations.find ({label}) -> label.textContent[position] is character module.exports = TreeViewManager From 7ae9a9f6e230f13803db338722e8c46896ca37db Mon Sep 17 00:00:00 2001 From: John Wright Date: Tue, 18 Oct 2016 10:08:17 +0100 Subject: [PATCH 14/19] Better beacon styling --- lib/label-managers/tree-view.coffee | 2 +- styles/beacon.less | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/label-managers/tree-view.coffee b/lib/label-managers/tree-view.coffee index 3f1432c..73886de 100644 --- a/lib/label-managers/tree-view.coffee +++ b/lib/label-managers/tree-view.coffee @@ -29,8 +29,8 @@ class TreeViewManager extends LabelManager match = "#{firstChar}#{secondChar}" location = @locations.find ({label}) -> label.textContent is match return unless location - @drawBeacon location @select location + @drawBeacon location select: ({element}) -> atom.commands.dispatch element, 'tree-view:show' diff --git a/styles/beacon.less b/styles/beacon.less index 23c8f70..1c9bc30 100644 --- a/styles/beacon.less +++ b/styles/beacon.less @@ -13,7 +13,13 @@ -webkit-animation: rip .085s; content: ''; top: -6px; + + .tree-view & { + position: absolute; + z-index: 11; + } } + @-webkit-keyframes rip { 0% { box-shadow:0 0 0 0 transparent, From d4a82fc4fce5bcb6977a97a043ca9080dcc27ca4 Mon Sep 17 00:00:00 2001 From: John Wright Date: Tue, 18 Oct 2016 12:47:09 +0100 Subject: [PATCH 15/19] Ensure blur and click events are handled in the tree --- lib/jumpy-view.coffee | 44 +++++---------------------- lib/label-manager-iterator.coffee | 15 +++++---- lib/label-manager.coffee | 27 ++++++++++------ lib/label-managers/text-editor.coffee | 10 ++++++ lib/label-managers/tree-view.coffee | 12 ++++++++ 5 files changed, 55 insertions(+), 53 deletions(-) diff --git a/lib/jumpy-view.coffee b/lib/jumpy-view.coffee index e4b8eeb..a084839 100644 --- a/lib/jumpy-view.coffee +++ b/lib/jumpy-view.coffee @@ -13,14 +13,12 @@ class JumpyView extends View @div '' initialize: () -> - @disposables = new CompositeDisposable() + @labelManager = new LabelManagerIterator @commands = new CompositeDisposable() - @labelManager = new LabelManagerIterator @disposables, @commands - @commands.add atom.commands.add 'atom-workspace', 'jumpy:toggle': => @toggle() 'jumpy:reset': => @reset() - 'jumpy:clear': => @clearJumpMode() + 'jumpy:clear': @clearJumpMode commands = LabelManagerIterator.chars.reduce( (commands, c) => _.set(commands, "jumpy:#{c}", => @getKey c), @@ -53,17 +51,13 @@ class JumpyView extends View if not @firstChar @firstChar = character @statusBarJumpyStatus?.innerHTML = @firstChar - # TODO: Refactor this so not 2 calls to observeTextEditors - @disposables.add atom.workspace.observeTextEditors (editor) => - editorView = atom.views.getView(editor) - return if $(editorView).is ':not(:visible)' - @labelManager.markIrrelevant @firstChar + @labelManager.markIrrelevant @firstChar else if not @secondChar @secondChar = character if @secondChar @jump() # Jump first. Currently need the placement of the labels. - _.defer @clearJumpModeHandler + _.defer @clearJumpMode clearKeys: -> @firstChar = null @@ -107,38 +101,16 @@ class JumpyView extends View document.querySelector '#status-bar-jumpy .status' @labelManager.toggle() + @labelManager.initializeClearEvents @clearJumpMode - @disposables.add atom.workspace.observeTextEditors (editor) => - editorView = atom.views.getView(editor) - @initializeClearEvents(editorView) - - clearJumpModeHandler: => - @clearJumpMode() - - initializeClearEvents: (editorView) -> - @disposables.add editorView.onDidChangeScrollTop @clearJumpModeHandler - @disposables.add editorView.onDidChangeScrollLeft @clearJumpModeHandler - - for e in ['blur', 'click'] - editorView.addEventListener(e, _.debounce(@clearJumpModeHandler), - true) - - clearJumpMode: -> - if @cleared - return - + clearJumpMode: => + return if @cleared @cleared = true @clearKeys() @statusBarJumpy?.innerHTML = '' - @disposables.add atom.workspace.observeTextEditors (editor) => - editorView = atom.views.getView(editor) - - document.body.classList.remove 'jumpy-jump-mode' - for e in ['blur', 'click'] - editorView.removeEventListener e, @clearJumpModeHandler, true + document.body.classList.remove 'jumpy-jump-mode' atom.keymaps.keyBindings = @backedUpKeyBindings @labelManager.destroy() - @disposables?.dispose() @detach() jump: -> diff --git a/lib/label-manager-iterator.coffee b/lib/label-manager-iterator.coffee index e000f61..14f25c7 100644 --- a/lib/label-manager-iterator.coffee +++ b/lib/label-manager-iterator.coffee @@ -31,9 +31,8 @@ class LabelManagerIterator @keys: keys @chars: lowerCharacters.concat upperCharacters - constructor: (disposables, commands) -> - @clickableLabels = [] - @labelManagers = labelManagers.map((Manager) -> new Manager disposables) + constructor: -> + @labelManagers = labelManagers.map((Manager) -> new Manager) atom.config.observe 'jumpy.fontSize', @setFontSize atom.config.observe 'jumpy.matchPattern', @setWordsPattern atom.config.observe 'jumpy.highContrast', @setHighContrast @@ -67,10 +66,10 @@ class LabelManagerIterator manager.unmarkIrrelevant() for manager in @labelManagers isMatchOfCurrentLabels: (character, position) -> - found = null - for manager in @labelManagers - found = manager.isMatchOfCurrentLabels character, position - break if found - found + @labelManagers.find (manager) -> + manager.isMatchOfCurrentLabels character, position + + initializeClearEvents: (clear) -> + manager.initializeClearEvents clear for manager in @labelManagers module.exports = LabelManagerIterator diff --git a/lib/label-manager.coffee b/lib/label-manager.coffee index 07124d4..80a9811 100644 --- a/lib/label-manager.coffee +++ b/lib/label-manager.coffee @@ -1,8 +1,13 @@ -abstractMethod = (name) ->-> - throw new Error "The abstract method #{name} needs to be created" +{CompositeDisposable} = require 'atom' + +abstractMethod = (cls, methodName) -> + cls::[methodName] = -> + throw new Error( + "The abstract method #{cls.name}::#{methodName} needs to exist") class LabelManager - constructor: (@disposables) -> + constructor: -> + @disposables = new CompositeDisposable createLabel: (text) -> labelElement = document.createElement('span') @@ -17,16 +22,20 @@ class LabelManager beacon.classList.add 'beacon' beacon - toggle: abstractMethod 'toggle' + destroy: -> + @disposables.dispose() + @disposables = new CompositeDisposable + + abstractMethod @, 'toggle' - destroy: abstractMethod 'destroy' + abstractMethod @, 'markIrrelevant' - markIrrelevant: abstractMethod 'markIrrelevant' + abstractMethod @, 'unmarkIrrelevant' - unmarkIrrelevant: abstractMethod 'unmarkIrrelevant' + abstractMethod @, 'isMatchOfCurrentLabels' - isMatchOfCurrentLabels: abstractMethod 'isMatchOfCurrentLabels' + abstractMethod @, 'jumpTo' - jumpTo: abstractMethod 'jumpTo' + abstractMethod @, 'initializeClearEvents' module.exports = LabelManager diff --git a/lib/label-managers/text-editor.coffee b/lib/label-managers/text-editor.coffee index 8580b99..abf8184 100644 --- a/lib/label-managers/text-editor.coffee +++ b/lib/label-managers/text-editor.coffee @@ -1,6 +1,7 @@ {Point, Range} = require 'atom' {$} = require 'space-pen' LabelManager = require '../label-manager' +{debounce} = require 'lodash' class TextEditorLabelManager extends LabelManager constructor: -> @@ -106,6 +107,7 @@ class TextEditorLabelManager extends LabelManager decoration.getMarker().destroy() for decoration in @decorations @decorations = [] # Very important for GC. # Verifiable in Dev Tools -> Timeline -> Nodes. + super findLocation: (firstChar, secondChar) -> label = "#{firstChar}#{secondChar}" @@ -136,4 +138,12 @@ class TextEditorLabelManager extends LabelManager return false if found return !!found + initializeClearEvents: (clear) -> + @disposables.add atom.workspace.observeTextEditors (editor) => + editorView = atom.views.getView(editor) + @disposables.add editorView.onDidChangeScrollTop clear + @disposables.add editorView.onDidChangeScrollLeft clear + for e in ['blur', 'click'] + editorView.addEventListener e, debounce(clear), true + module.exports = TextEditorLabelManager diff --git a/lib/label-managers/tree-view.coffee b/lib/label-managers/tree-view.coffee index 73886de..40700fc 100644 --- a/lib/label-managers/tree-view.coffee +++ b/lib/label-managers/tree-view.coffee @@ -1,4 +1,6 @@ LabelManager = require '../label-manager' +{debounce} = require 'lodash' +{Disposable} = require 'atom' triggerMouseEvent = (element, eventType) -> clickEvent = document.createEvent 'MouseEvents' @@ -19,6 +21,7 @@ class TreeViewManager extends LabelManager destroy: -> location.label.remove() while location = @locations.shift() + super drawBeacon: ({element}) -> beacon = @createBeacon() @@ -48,4 +51,13 @@ class TreeViewManager extends LabelManager isMatchOfCurrentLabels: (character, position) -> @locations.find ({label}) -> label.textContent[position] is character + initializeClearEvents: (clear) -> + clear = debounce clear + for treeView in document.getElementsByClassName('tree-view') + for e in ['blur', 'click'] + do (treeView, e) => + treeView.addEventListener e, clear + @disposables.add new Disposable -> + treeView.removeEventListener e, clear + module.exports = TreeViewManager From c278e7952bc44fe695ecf2326f4faeeaa2a692cc Mon Sep 17 00:00:00 2001 From: John Wright Date: Tue, 18 Oct 2016 12:47:49 +0100 Subject: [PATCH 16/19] fix: use the mocked clock --- spec/jumpy-spec.coffee | 59 +++++++++++++++----------------- spec/jumpy-tree-view-spec.coffee | 2 +- 2 files changed, 28 insertions(+), 33 deletions(-) diff --git a/spec/jumpy-spec.coffee b/spec/jumpy-spec.coffee index bf0ac6a..539ccba 100644 --- a/spec/jumpy-spec.coffee +++ b/spec/jumpy-spec.coffee @@ -140,15 +140,15 @@ describe "Jumpy", -> describe "when the jumpy:toggle event is triggered and hotkeys are entered", -> - it "jumpy is cleared", (done) -> + beforeEach -> atom.commands.dispatch workspaceElement, 'jumpy:a' atom.commands.dispatch workspaceElement, 'jumpy:c' + advanceClock() + + it "jumpy is cleared", -> expect(textEditorElement.classList .contains('jumpy-jump-mode')).toBe false - setTimeout -> - expect(textEditor.getOverlayDecorations()).toHaveLength 0 - done() - , 160 + expect(textEditor.getOverlayDecorations()).toHaveLength 1 describe "when the jumpy:toggle event is triggered and invalid hotkeys are entered", -> @@ -367,40 +367,35 @@ describe "Jumpy", -> .toHaveLength expectedTotalNumberWith2TabsOpenInOnePane describe "when a jump mode is enabled", -> - activationPromise = [] beforeEach -> - activationPromise = atom.packages.activatePackage 'find-and-replace' - - it "clears when a find-and-replace mini pane is opened", -> - atom.commands.dispatch textEditorElement, 'find-and-replace:show' - waitsForPromise -> - activationPromise + promise = atom.packages.activatePackage 'find-and-replace' + atom.commands.dispatch(textEditorElement, + 'find-and-replace:show') + promise + runs advanceClock - runs -> - expect(textEditorElement - .classList.contains('jumpy-jump-mode')).toBe false - expect(textEditor.getOverlayDecorations()).toHaveLength 0 - expect(workspaceElement - .querySelectorAll('.find-and-replace')).toHaveLength 1 + it "clears when a find-and-replace mini pane is opened", -> + expect(workspaceElement + .querySelectorAll('.find-and-replace')).toHaveLength 1 + expect(textEditorElement + .classList.contains('jumpy-jump-mode')).toBe false + expect(textEditor.getOverlayDecorations()).toHaveLength 0 describe "when a jump mode is enabled", -> - activationPromise = [] beforeEach -> - activationPromise = atom.packages.activatePackage 'fuzzy-finder' + waitsForPromise -> + activationPromise = atom.packages.activatePackage 'fuzzy-finder' + atom.commands.dispatch textEditorElement, + 'fuzzy-finder:toggle-file-finder' + activationPromise + runs advanceClock it "clears when a fuzzy-finder mini pane is opened", -> atom.commands.dispatch textEditorElement, 'fuzzy-finder:toggle-file-finder' - - waitsForPromise -> - activationPromise - - runs -> - atom.commands.dispatch textEditorElement, - 'fuzzy-finder:toggle-file-finder' - expect(textEditorElement - .classList.contains('jumpy-jump-mode')).toBe false - expect(textEditor.getOverlayDecorations()).toHaveLength 0 - expect(workspaceElement - .querySelectorAll('.fuzzy-finder')).toHaveLength 1 + expect(textEditorElement + .classList.contains('jumpy-jump-mode')).toBe false + expect(textEditor.getOverlayDecorations()).toHaveLength 0 + expect(workspaceElement + .querySelectorAll('.fuzzy-finder')).toHaveLength 1 diff --git a/spec/jumpy-tree-view-spec.coffee b/spec/jumpy-tree-view-spec.coffee index 4cb3a16..c021017 100644 --- a/spec/jumpy-tree-view-spec.coffee +++ b/spec/jumpy-tree-view-spec.coffee @@ -8,7 +8,7 @@ NUM_FILES = 3 NUM_DIRS = 1 DIR = path.join __dirname, 'fixtures' -fdescribe 'jumpy-tree-view', -> +describe 'jumpy-tree-view', -> {workspaceElement} = {} From ba209f25bd1c27057ae0c19e12034879167f4a4a Mon Sep 17 00:00:00 2001 From: John Wright Date: Tue, 18 Oct 2016 22:29:40 +0100 Subject: [PATCH 17/19] docs: remove unneccessary todo --- lib/jumpy-view.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/jumpy-view.coffee b/lib/jumpy-view.coffee index 7d84d10..91de7b5 100644 --- a/lib/jumpy-view.coffee +++ b/lib/jumpy-view.coffee @@ -84,7 +84,6 @@ class JumpyView extends View # 'jumpy-jump-mode is for keymaps and utilized by tests document.body.classList.add 'jumpy-jump-mode' - # TODO: Can the following few lines be singleton'd up? ie. instance var? @turnOffSlowKeys() @statusBarJumpy?.classList.remove 'no-match' @statusBarJumpy?.innerHTML = From be1d2f1395afb6b6e37d8a6b25f6f426c057b04b Mon Sep 17 00:00:00 2001 From: John Wright Date: Sun, 23 Oct 2016 22:14:11 +0100 Subject: [PATCH 18/19] :art: syntactical sugar' --- lib/jumpy-view.coffee | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/jumpy-view.coffee b/lib/jumpy-view.coffee index 91de7b5..65ff619 100644 --- a/lib/jumpy-view.coffee +++ b/lib/jumpy-view.coffee @@ -42,16 +42,16 @@ class JumpyView extends View # Assert: labelPosition will start at 0! labelPosition = (if not @firstChar then 0 else 1) - if not @labelManager.isMatchOfCurrentLabels character, labelPosition + unless @labelManager.isMatchOfCurrentLabels character, labelPosition @statusBarJumpy?.classList.add 'no-match' @statusBarJumpyStatus?.innerHTML = 'No match!' return - if not @firstChar + unless @firstChar @firstChar = character @statusBarJumpyStatus?.innerHTML = @firstChar @labelManager.markIrrelevant @firstChar - else if not @secondChar + else unless @secondChar @secondChar = character if @secondChar From 788d1496c0a69b3d36643d77692deb33ad112188 Mon Sep 17 00:00:00 2001 From: John Wright Date: Sun, 23 Oct 2016 22:14:35 +0100 Subject: [PATCH 19/19] test: selecting tree view elements --- spec/jumpy-tree-view-spec.coffee | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/spec/jumpy-tree-view-spec.coffee b/spec/jumpy-tree-view-spec.coffee index c021017..3689684 100644 --- a/spec/jumpy-tree-view-spec.coffee +++ b/spec/jumpy-tree-view-spec.coffee @@ -32,13 +32,16 @@ describe 'jumpy-tree-view', -> labels = atom.document.querySelectorAll '.tree-view .jumpy-label' expect(labels.length).toBe NUM_DIRS + NUM_FILES - xit 'will open a file when selected with jumpy', -> - # TODO: This doesn't work. Maybe use spies & stubs instead. - atom.commands.dispatch workspaceElement, 'jump:a' - atom.commands.dispatch workspaceElement, 'jump:c' - editor = atom.workspace.getActivePaneItem() - file = editor?.buffer.file.path - expect(file).toBe path.join DIR, 'test_text.md' + it 'will open a file when selected with jumpy', -> + file = path.join DIR, 'test_text.md' + element = atom.document.querySelector "[data-path=\"#{file}\"]" + spyOn element, 'dispatchEvent' + atom.commands.dispatch workspaceElement, 'jumpy:a' + atom.commands.dispatch workspaceElement, 'jumpy:c' + expect(element.dispatchEvent).toHaveBeenCalled() + arg = element.dispatchEvent.mostRecentCall.args[0] + expect(arg instanceof MouseEvent).toBe yes + expect(arg.type).toEqual 'mousedown' it 'will open/close directories when selected with jumpy', -> dir = atom.document.querySelector '.tree-view .directory'