Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tree View #93

Open
wants to merge 20 commits into
base: master
Choose a base branch
from
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