Skip to content

Commit

Permalink
Merge pull request #852 from t9md/lazy-f
Browse files Browse the repository at this point in the history
Better `f` as chimera of clever-f  and vim-seek.
  • Loading branch information
t9md committed Sep 1, 2017
2 parents 8eac98b + 13935ac commit 3965141
Show file tree
Hide file tree
Showing 13 changed files with 355 additions and 46 deletions.
42 changes: 41 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,48 @@
# 1.1.0: This release is all for better `f` by making it tunable
- New: [Summary] Now `f` is **tunable**. #852.
- Inspired pure-vim's plugins: `clever-f`, `vim-seek`, `vim-sneak`.
- Highlighting find-char. It help you to pre-determine consequence of repeat by `;`, `,` and `.`.
- Aiming to get both benefit of two-char-find(`vim-seek`, `vim-sneak`) and one-char-find( vim's default ).
- Even after two-char-find was enabled, you can auto-confirm one-char input by specified timeout.
- Can reuse `f`, `F`, `t`, `T` as `repeat-find` like `clever-f`.
- Config: [Detail] Following configuration option is available to **tune** `f`.
- `keymapSemicolonToConfirmFind`: default `false`.
- See explanation for `findByTwoChars`.
- `ignoreCaseForFind`: default `false`
- `useSmartcaseForFind`: default `false`
- `highlightFindChar`: default `true`
- Highlight find char, fadeout automatically( this auto-disappearing behavior/duration is not configurable ).
- Fadeout in 2 second when used as motion.
- Fadeout in 4 second when used as operator-target.
- `findByTwoChars`: default `false`
- When enabled, `f` accept TWO chars.
- Pros. Greatly reduces possible matches, avoid being stopped at earlier spot than where you aimed.
- Cons. Require explicit **confirmation** by `enter` for single char-input. You might mitigate frustration by.
- Confirm by `;`, easier to type and well blend to forwarding `repeat-find`( `;` ).
- Enable "keymap `;` to confirm `find` motion"( `keymapSemicolonToConfirmFind` ) configuration.
- e.g. `f a ;` to move to `a`( better than `f a enter`?). `f a ; ;` to move to 2nd `a`(well blended to default repeat-find(`;`)).
- Enable auto confirm by timeout( See. `findByTwoCharsAutoConfirmTimeout` )
- `findByTwoCharsAutoConfirmTimeout`: default `0`.
- "When `findByTwoChars` was enabled, automatically confirm single-char input on timeout( msec ).
- `0` means no timeout.
- `reuseFindForRepeatFind`: default `false`
- When `true` you can repeat last-find by `f` and `F`(also `t` and `T`).
- You still can use `,` and `;`.
- e.g. `f a f` move cursor to 2nd `a`.
- My configuration( I'm still in-eval phase, don't take this as recommendation ).
```coffeescript
keymapSemicolonToConfirmFind: true
findByTwoChars: true
findByTwoCharsAutoConfirmTimeout: 500
reuseFindForRepeatFind: true
useSmartcaseForFind: true
```

# 1.0.0: New default `stayOn` all `true`.
- Version: Decided to bump major version.
- Breaking: Default config change/Renamed config name.
- Summary:
- Now all `stayOn` prefixed configuration have new default `true`.
- Now all `stayOn` prefixed configuration have new default `false`.
- New default behavior is NOT compatible with pure-Vim.
- Set all `stayOn` prefixed configuration to `false` to revert to previous behavior.
- Some configuration parameter name is renamed to have `stayOn` prefix.
Expand Down
5 changes: 2 additions & 3 deletions keymaps/vim-mode-plus.cson
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@
'-': 'vim-mode-plus:move-to-first-character-of-line-up'
'+': 'vim-mode-plus:move-to-first-character-of-line-down'
'enter': 'vim-mode-plus:move-to-first-character-of-line-down'

'g 0': 'vim-mode-plus:move-to-beginning-of-screen-line'
'g ^': 'vim-mode-plus:move-to-first-character-of-screen-line'
'g $': 'vim-mode-plus:move-to-last-character-of-screen-line'
Expand Down Expand Up @@ -282,7 +282,6 @@
# 'g n': 'vim-mode-plus:move-to-next-number'
# 'g N': 'vim-mode-plus:move-to-previous-number'


# macOS only
'.platform-darwin atom-text-editor.vim-mode-plus:not(.insert-mode)':
'ctrl-s': 'vim-mode-plus:transform-string-by-select-list'
Expand Down Expand Up @@ -661,7 +660,7 @@
'O': 'vim-mode-plus:reverse-selections'
'D': 'vim-mode-plus:delete-to-last-character-of-line' # To avoid overridden by delete-line in visual-mode

# Input mini editor e.g. mark, surround.
# Input mini editor e.g. mark, surround, find, till.
# -------------------------
'atom-text-editor.vim-mode-plus-input':
'ctrl-c': 'core:cancel'
Expand Down
12 changes: 5 additions & 7 deletions lib/base.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -166,13 +166,11 @@ class Base
selectList.show(@vimState, options)

input: null
focusInput: ({charsMax, hideCursor} = {}) ->
@vimState.focusInput
charsMax: charsMax
hideCursor: hideCursor
onConfirm: (@input) => @processOperation()
onCancel: => @cancelOperation()
onChange: (input) => @vimState.hover.set(input)
focusInput: (options = {}) ->
options.onConfirm ?= (@input) => @processOperation()
options.onCancel ?= => @cancelOperation()
options.onChange ?= (input) => @vimState.hover.set(input)
@vimState.focusInput(options)

readChar: ->
@vimState.readChar
Expand Down
42 changes: 35 additions & 7 deletions lib/focus-input.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,33 @@
module.exports = function focusInput(vimState, {charsMax = 1, hideCursor, onChange, onCancel, onConfirm} = {}) {
vimState.editorElement.classList.add("vim-mode-plus-input-focused")
if (hideCursor) vimState.editorElement.classList.add("hide-cursor")
module.exports = function focusInput(
vimState,
{charsMax = 1, autoConfirmTimeout, hideCursor, onChange, onCancel, onConfirm, purpose} = {}
) {
const classListToAdd = ["vim-mode-plus-input-focused"]
if (hideCursor) classListToAdd.push("hide-cursor")
vimState.editorElement.classList.add(...classListToAdd)

const editor = atom.workspace.buildTextEditor({mini: true})

vimState.inputEditor = editor // set ref for test
editor.element.classList.add("vim-mode-plus-input")
if (purpose) editor.element.classList.add(purpose)
editor.element.setAttribute("mini", "")

if (atom.inSpecMode()) atom.workspace.getElement().appendChild(editor.element) // I can skip jasmine.attachToDOM.
// So that I can skip jasmine.attachToDOM in test.
if (atom.inSpecMode()) atom.workspace.getElement().appendChild(editor.element)
else vimState.editorElement.parentNode.appendChild(editor.element)

let autoConfirmTimeoutID
const clerAutoConfirmTimer = () => {
if (autoConfirmTimeoutID) clearTimeout(autoConfirmTimeoutID)
autoConfirmTimeoutID = null
}

const unfocus = () => {
clerAutoConfirmTimer()
vimState.editorElement.focus() // focus
vimState.inputEditor = null // unset ref for test
vimState.editorElement.classList.remove("vim-mode-plus-input-focused", "hide-cursor")
vimState.editorElement.classList.remove(...classListToAdd)
editor.element.remove()
editor.destroy()
}
Expand All @@ -30,8 +43,23 @@ module.exports = function focusInput(vimState, {charsMax = 1, hideCursor, onChan

vimState.onDidFailToPushToOperationStack(cancel)

if (charsMax === 1) editor.onDidChange(() => confirm(editor.getText()))
else if (onChange) editor.onDidChange(() => onChange(editor.getText()))
if (charsMax === 1) {
editor.onDidChange(() => confirm(editor.getText()))
} else {
editor.onDidChange(() => {
clerAutoConfirmTimer()

const text = editor.getText()
if (text.length >= charsMax) {
confirm(text)
} else {
if (onChange) onChange(text)
if (autoConfirmTimeout) {
autoConfirmTimeoutID = setTimeout(() => confirm(text), autoConfirmTimeout)
}
}
})
}

atom.commands.add(editor.element, {
"core:cancel": event => {
Expand Down
63 changes: 63 additions & 0 deletions lib/highlight-find-manager.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
const DecorationTypes = {
"pre-confirm": {
decorationProps: {
type: "highlight",
class: "vim-mode-plus-find-char pre-confirm",
},
},
"post-confirm": {
timeout: 2000,
decorationProps: {
type: "highlight",
class: "vim-mode-plus-find-char post-confirm",
},
},
"post-confirm-long": {
timeout: 4000,
decorationProps: {
type: "highlight",
class: "vim-mode-plus-find-char post-confirm-long",
},
},
}

module.exports = class HighlightFind {
constructor(vimState) {
this.vimState = vimState
vimState.onDidDestroy(() => this.destroy())

const {editor} = vimState
this.markerLayer = editor.addMarkerLayer()
this.decorationLayer = editor.decorateMarkerLayer(this.markerLayer, {})
}

destroy() {
this.decorationLayer.destroy()
this.markerLayer.destroy()
}

clearMarkers() {
this.markerLayer.clear()
}

highlightRanges(ranges, decorationType) {
if (this.clearMarkerTimeoutID) {
clearTimeout(this.clearMarkerTimeoutID)
this.clearMarkerTimeoutID = null
}

this.clearMarkers()
// We need to force update here to restart(re-trigger) keyframe animation.
this.vimState.editor.component.updateSync()

for (const range of ranges) {
this.markerLayer.markBufferRange(range)
}

const {timeout, decorationProps} = DecorationTypes[decorationType]
this.decorationLayer.setProperties(decorationProps)
if (timeout) {
this.clearMarkerTimeoutID = setTimeout(() => this.clearMarkers(), timeout)
}
}
}
20 changes: 3 additions & 17 deletions lib/motion-search.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class SearchBase extends Motion
jump: true
backwards: false
useRegexp: true
configScope: null
caseSensitivityKind: null
landingPoint: null # ['start' or 'end']
defaultLandingPoint: 'start' # ['start' or 'end']
relativeIndex: null
Expand All @@ -33,20 +33,6 @@ class SearchBase extends Motion
else
count

getCaseSensitivity: ->
if @getConfig("useSmartcaseFor#{@configScope}")
'smartcase'
else if @getConfig("ignoreCaseFor#{@configScope}")
'insensitive'
else
'sensitive'

isCaseSensitive: (term) ->
switch @getCaseSensitivity()
when 'smartcase' then term.search('[A-Z]') isnt -1
when 'insensitive' then false
when 'sensitive' then true

finish: ->
if @isIncrementalSearch() and @getConfig('showHoverSearchCounter')
@vimState.hoverSearchCounter.reset()
Expand Down Expand Up @@ -101,7 +87,7 @@ class SearchBase extends Motion
# -------------------------
class Search extends SearchBase
@extend()
configScope: "Search"
caseSensitivityKind: "Search"
requireInput: true

initialize: ->
Expand Down Expand Up @@ -206,7 +192,7 @@ class SearchBackwards extends Search
# -------------------------
class SearchCurrentWord extends SearchBase
@extend()
configScope: "SearchCurrentWord"
caseSensitivityKind: "SearchCurrentWord"

moveCursor: (cursor) ->
@input ?= (
Expand Down
53 changes: 50 additions & 3 deletions lib/motion.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,12 @@ class Motion extends Base
state.stop()
oldPosition = newPosition

isCaseSensitive: (term) ->
if @getConfig("useSmartcaseFor#{@caseSensitivityKind}")
term.search(/[A-Z]/) isnt -1
else
not @getConfig("ignoreCaseFor#{@caseSensitivityKind}")

# Used as operator's target in visual-mode.
class CurrentSelection extends Motion
@extend(false)
Expand Down Expand Up @@ -890,14 +896,43 @@ class Find extends Motion
inclusive: true
offset: 0
requireInput: true
caseSensitivityKind: "Find"

initialize: ->
super
@focusInput() unless @isComplete()

@repeatIfNecessary()
return if @isComplete()

if @getConfig("findByTwoChars")
options =
charsMax: 2
autoConfirmTimeout: @getConfig("findByTwoCharsAutoConfirmTimeout")
onChange: (char) => @highlightTextInCursorRows(char, "pre-confirm")
onCancel: =>
@vimState.highlightFind.clearMarkers()
@cancelOperation()

options ?= {}
options.purpose = "find"

@focusInput(options)

repeatIfNecessary: ->
if @getConfig("reuseFindForRepeatFind")
if @vimState.operationStack.getLastCommandName() in ["Find", "FindBackwards", "Till", "TillBackwards"]
@input = @vimState.globalState.get("currentFind").input
@repeated = true

isBackwards: ->
@backwards

execute: ->
super
decorationType = "post-confirm"
decorationType += "-long" if @isAsTargetExceptSelect()
@highlightTextInCursorRows(@input, decorationType)

getPoint: (fromPoint) ->
{start, end} = @editor.bufferRangeForBufferRow(fromPoint.row)

Expand All @@ -911,15 +946,27 @@ class Find extends Motion
method = 'scanInBufferRange'

points = []
@editor[method] ///#{_.escapeRegExp(@input)}///g, scanRange, ({range}) ->
points.push(range.start)
@editor[method] @getRegex(@input), scanRange, ({range}) -> points.push(range.start)

points[@getCount(-1)]?.translate([0, offset])

highlightTextInCursorRows: (text, decorationType) ->
return unless @getConfig("highlightFindChar")
ranges = []
for cursor in @editor.getCursors()
scanRange = cursor.getCurrentLineBufferRange()
@editor.scanInBufferRange(@getRegex(text), scanRange, ({range}) -> ranges.push(range))
@vimState.highlightFind.highlightRanges(ranges, decorationType)

moveCursor: (cursor) ->
point = @getPoint(cursor.getBufferPosition())
@setBufferPositionSafely(cursor, point)
@globalState.set('currentFind', this) unless @repeated

getRegex: (term) ->
modifiers = if @isCaseSensitive(term) then 'g' else 'gi'
new RegExp(_.escapeRegExp(term), modifiers)

# keymap: F
class FindBackwards extends Find
@extend()
Expand Down
9 changes: 8 additions & 1 deletion lib/operation-stack.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ module.exports = class OperationStack {
return handler // DONT REMOVE
}

getLastCommandName() {
return this.lastCommandName
}

reset() {
this.resetCount()
this.stack = []
Expand Down Expand Up @@ -222,7 +226,10 @@ module.exports = class OperationStack {
}

finish(operation) {
if (operation && operation.recordable) this.recordedOperation = operation
if (operation) {
if (operation.recordable) this.recordedOperation = operation
this.lastCommandName = operation.name
}

this.vimState.emitDidFinishOperation()
if (operation && operation.isOperator()) operation.resetState()
Expand Down

0 comments on commit 3965141

Please sign in to comment.