Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions keymaps/jumpy.cson
Original file line number Diff line number Diff line change
Expand Up @@ -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':
'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'
Expand Down
227 changes: 24 additions & 203 deletions lib/jumpy-view.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -3,48 +3,28 @@
# TODO: Remove space-pen?

### global atom ###
{CompositeDisposable, Point, Range} = require 'atom'
LabelManagerIterator = require './label-manager-iterator'
{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: ->
@div ''

initialize: () ->
@disposables = new CompositeDisposable()
@decorations = []
@labelManager = new LabelManagerIterator
@commands = new CompositeDisposable()

@commands.add atom.commands.add 'atom-workspace',
'jumpy:toggle': => @toggle()
'jumpy:reset': => @reset()
'jumpy:clear': => @clearJumpMode()
'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.
Expand All @@ -60,53 +40,31 @@ 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)'

for decoration in @decorations
element = decoration.getProperties().item
if element.textContent[labelPosition] == character
found = true
return false
return found

# Assert: labelPosition will start at 0!
labelPosition = (if not @firstChar then 0 else 1)
if !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
# 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)'

for decoration in @decorations
element = decoration.getProperties().item
if element.textContent.indexOf(@firstChar) != 0
element.classList.add 'irrelevant'
else if not @secondChar
@labelManager.markIrrelevant @firstChar
else unless @secondChar
@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 @clearJumpMode

clearKeys: ->
@firstChar = null
@secondChar = null

reset: ->
@clearKeys()
for decoration in @decorations
decoration.getProperties().item.classList.remove 'irrelevant'
@labelManager.unmarkIrrelevant()
@statusBarJumpy?.classList.remove 'no-match'
@statusBarJumpyStatus?.innerHTML = 'Jump Mode!'

Expand All @@ -123,12 +81,8 @@ class JumpyView extends View
# Set dirty for @clearJumpMode
@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'
# 'jumpy-jump-mode is for keymaps and utilized by tests
document.body.classList.add 'jumpy-jump-mode'

@turnOffSlowKeys()
@statusBarJumpy?.classList.remove 'no-match'
Expand All @@ -137,154 +91,21 @@ class JumpyView extends View
@statusBarJumpyStatus =
document.querySelector '#status-bar-jumpy .status'

@allPositions = {}
nextKeys = _.clone 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 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: =>
@clearJumpMode()

initializeClearEvents: (editorView) ->
@disposables.add editorView.onDidChangeScrollTop =>
@clearJumpModeHandler()
@disposables.add editorView.onDidChangeScrollLeft =>
@clearJumpModeHandler()

for e in ['blur', 'click']
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
@labelManager.toggle()
@labelManager.initializeClearEvents @clearJumpMode

clearJumpMode: =>
return if @cleared
@cleared = true
@clearKeys()
@statusBarJumpy?.innerHTML = ''
@disposables.add atom.workspace.observeTextEditors (editor) =>
editorView = atom.views.getView(editor)

editorView.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
clearAllMarkers()
@disposables?.dispose()
@labelManager.destroy()
@detach()

jump: ->
location = @findLocation()
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

findLocation: ->
label = "#{@firstChar}#{@secondChar}"
if label of @allPositions
return @allPositions[label]

return null
@labelManager.jumpTo @firstChar, @secondChar

# Returns an object that can be retrieved when package is activated
serialize: ->
Expand Down
75 changes: 75 additions & 0 deletions lib/label-manager-iterator.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
{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
@keys: keys
@chars: lowerCharacters.concat upperCharacters

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

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

isMatchOfCurrentLabels: (character, position) ->
@labelManagers.find (manager) ->
manager.isMatchOfCurrentLabels character, position

initializeClearEvents: (clear) ->
manager.initializeClearEvents clear for manager in @labelManagers

module.exports = LabelManagerIterator
Loading