Skip to content

Commit 3965141

Browse files
authored
Merge pull request #852 from t9md/lazy-f
Better `f` as chimera of clever-f and vim-seek.
2 parents 8eac98b + 13935ac commit 3965141

13 files changed

Lines changed: 355 additions & 46 deletions

CHANGELOG.md

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,48 @@
1+
# 1.1.0: This release is all for better `f` by making it tunable
2+
- New: [Summary] Now `f` is **tunable**. #852.
3+
- Inspired pure-vim's plugins: `clever-f`, `vim-seek`, `vim-sneak`.
4+
- Highlighting find-char. It help you to pre-determine consequence of repeat by `;`, `,` and `.`.
5+
- Aiming to get both benefit of two-char-find(`vim-seek`, `vim-sneak`) and one-char-find( vim's default ).
6+
- Even after two-char-find was enabled, you can auto-confirm one-char input by specified timeout.
7+
- Can reuse `f`, `F`, `t`, `T` as `repeat-find` like `clever-f`.
8+
- Config: [Detail] Following configuration option is available to **tune** `f`.
9+
- `keymapSemicolonToConfirmFind`: default `false`.
10+
- See explanation for `findByTwoChars`.
11+
- `ignoreCaseForFind`: default `false`
12+
- `useSmartcaseForFind`: default `false`
13+
- `highlightFindChar`: default `true`
14+
- Highlight find char, fadeout automatically( this auto-disappearing behavior/duration is not configurable ).
15+
- Fadeout in 2 second when used as motion.
16+
- Fadeout in 4 second when used as operator-target.
17+
- `findByTwoChars`: default `false`
18+
- When enabled, `f` accept TWO chars.
19+
- Pros. Greatly reduces possible matches, avoid being stopped at earlier spot than where you aimed.
20+
- Cons. Require explicit **confirmation** by `enter` for single char-input. You might mitigate frustration by.
21+
- Confirm by `;`, easier to type and well blend to forwarding `repeat-find`( `;` ).
22+
- Enable "keymap `;` to confirm `find` motion"( `keymapSemicolonToConfirmFind` ) configuration.
23+
- 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(`;`)).
24+
- Enable auto confirm by timeout( See. `findByTwoCharsAutoConfirmTimeout` )
25+
- `findByTwoCharsAutoConfirmTimeout`: default `0`.
26+
- "When `findByTwoChars` was enabled, automatically confirm single-char input on timeout( msec ).
27+
- `0` means no timeout.
28+
- `reuseFindForRepeatFind`: default `false`
29+
- When `true` you can repeat last-find by `f` and `F`(also `t` and `T`).
30+
- You still can use `,` and `;`.
31+
- e.g. `f a f` move cursor to 2nd `a`.
32+
- My configuration( I'm still in-eval phase, don't take this as recommendation ).
33+
```coffeescript
34+
keymapSemicolonToConfirmFind: true
35+
findByTwoChars: true
36+
findByTwoCharsAutoConfirmTimeout: 500
37+
reuseFindForRepeatFind: true
38+
useSmartcaseForFind: true
39+
```
40+
141
# 1.0.0: New default `stayOn` all `true`.
242
- Version: Decided to bump major version.
343
- Breaking: Default config change/Renamed config name.
444
- Summary:
5-
- Now all `stayOn` prefixed configuration have new default `true`.
45+
- Now all `stayOn` prefixed configuration have new default `false`.
646
- New default behavior is NOT compatible with pure-Vim.
747
- Set all `stayOn` prefixed configuration to `false` to revert to previous behavior.
848
- Some configuration parameter name is renamed to have `stayOn` prefix.

keymaps/vim-mode-plus.cson

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@
105105
'-': 'vim-mode-plus:move-to-first-character-of-line-up'
106106
'+': 'vim-mode-plus:move-to-first-character-of-line-down'
107107
'enter': 'vim-mode-plus:move-to-first-character-of-line-down'
108-
108+
109109
'g 0': 'vim-mode-plus:move-to-beginning-of-screen-line'
110110
'g ^': 'vim-mode-plus:move-to-first-character-of-screen-line'
111111
'g $': 'vim-mode-plus:move-to-last-character-of-screen-line'
@@ -282,7 +282,6 @@
282282
# 'g n': 'vim-mode-plus:move-to-next-number'
283283
# 'g N': 'vim-mode-plus:move-to-previous-number'
284284

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

664-
# Input mini editor e.g. mark, surround.
663+
# Input mini editor e.g. mark, surround, find, till.
665664
# -------------------------
666665
'atom-text-editor.vim-mode-plus-input':
667666
'ctrl-c': 'core:cancel'

lib/base.coffee

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -166,13 +166,11 @@ class Base
166166
selectList.show(@vimState, options)
167167

168168
input: null
169-
focusInput: ({charsMax, hideCursor} = {}) ->
170-
@vimState.focusInput
171-
charsMax: charsMax
172-
hideCursor: hideCursor
173-
onConfirm: (@input) => @processOperation()
174-
onCancel: => @cancelOperation()
175-
onChange: (input) => @vimState.hover.set(input)
169+
focusInput: (options = {}) ->
170+
options.onConfirm ?= (@input) => @processOperation()
171+
options.onCancel ?= => @cancelOperation()
172+
options.onChange ?= (input) => @vimState.hover.set(input)
173+
@vimState.focusInput(options)
176174

177175
readChar: ->
178176
@vimState.readChar

lib/focus-input.js

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,33 @@
1-
module.exports = function focusInput(vimState, {charsMax = 1, hideCursor, onChange, onCancel, onConfirm} = {}) {
2-
vimState.editorElement.classList.add("vim-mode-plus-input-focused")
3-
if (hideCursor) vimState.editorElement.classList.add("hide-cursor")
1+
module.exports = function focusInput(
2+
vimState,
3+
{charsMax = 1, autoConfirmTimeout, hideCursor, onChange, onCancel, onConfirm, purpose} = {}
4+
) {
5+
const classListToAdd = ["vim-mode-plus-input-focused"]
6+
if (hideCursor) classListToAdd.push("hide-cursor")
7+
vimState.editorElement.classList.add(...classListToAdd)
48

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

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

11-
if (atom.inSpecMode()) atom.workspace.getElement().appendChild(editor.element) // I can skip jasmine.attachToDOM.
16+
// So that I can skip jasmine.attachToDOM in test.
17+
if (atom.inSpecMode()) atom.workspace.getElement().appendChild(editor.element)
1218
else vimState.editorElement.parentNode.appendChild(editor.element)
1319

20+
let autoConfirmTimeoutID
21+
const clerAutoConfirmTimer = () => {
22+
if (autoConfirmTimeoutID) clearTimeout(autoConfirmTimeoutID)
23+
autoConfirmTimeoutID = null
24+
}
25+
1426
const unfocus = () => {
27+
clerAutoConfirmTimer()
1528
vimState.editorElement.focus() // focus
1629
vimState.inputEditor = null // unset ref for test
17-
vimState.editorElement.classList.remove("vim-mode-plus-input-focused", "hide-cursor")
30+
vimState.editorElement.classList.remove(...classListToAdd)
1831
editor.element.remove()
1932
editor.destroy()
2033
}
@@ -30,8 +43,23 @@ module.exports = function focusInput(vimState, {charsMax = 1, hideCursor, onChan
3043

3144
vimState.onDidFailToPushToOperationStack(cancel)
3245

33-
if (charsMax === 1) editor.onDidChange(() => confirm(editor.getText()))
34-
else if (onChange) editor.onDidChange(() => onChange(editor.getText()))
46+
if (charsMax === 1) {
47+
editor.onDidChange(() => confirm(editor.getText()))
48+
} else {
49+
editor.onDidChange(() => {
50+
clerAutoConfirmTimer()
51+
52+
const text = editor.getText()
53+
if (text.length >= charsMax) {
54+
confirm(text)
55+
} else {
56+
if (onChange) onChange(text)
57+
if (autoConfirmTimeout) {
58+
autoConfirmTimeoutID = setTimeout(() => confirm(text), autoConfirmTimeout)
59+
}
60+
}
61+
})
62+
}
3563

3664
atom.commands.add(editor.element, {
3765
"core:cancel": event => {

lib/highlight-find-manager.js

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
const DecorationTypes = {
2+
"pre-confirm": {
3+
decorationProps: {
4+
type: "highlight",
5+
class: "vim-mode-plus-find-char pre-confirm",
6+
},
7+
},
8+
"post-confirm": {
9+
timeout: 2000,
10+
decorationProps: {
11+
type: "highlight",
12+
class: "vim-mode-plus-find-char post-confirm",
13+
},
14+
},
15+
"post-confirm-long": {
16+
timeout: 4000,
17+
decorationProps: {
18+
type: "highlight",
19+
class: "vim-mode-plus-find-char post-confirm-long",
20+
},
21+
},
22+
}
23+
24+
module.exports = class HighlightFind {
25+
constructor(vimState) {
26+
this.vimState = vimState
27+
vimState.onDidDestroy(() => this.destroy())
28+
29+
const {editor} = vimState
30+
this.markerLayer = editor.addMarkerLayer()
31+
this.decorationLayer = editor.decorateMarkerLayer(this.markerLayer, {})
32+
}
33+
34+
destroy() {
35+
this.decorationLayer.destroy()
36+
this.markerLayer.destroy()
37+
}
38+
39+
clearMarkers() {
40+
this.markerLayer.clear()
41+
}
42+
43+
highlightRanges(ranges, decorationType) {
44+
if (this.clearMarkerTimeoutID) {
45+
clearTimeout(this.clearMarkerTimeoutID)
46+
this.clearMarkerTimeoutID = null
47+
}
48+
49+
this.clearMarkers()
50+
// We need to force update here to restart(re-trigger) keyframe animation.
51+
this.vimState.editor.component.updateSync()
52+
53+
for (const range of ranges) {
54+
this.markerLayer.markBufferRange(range)
55+
}
56+
57+
const {timeout, decorationProps} = DecorationTypes[decorationType]
58+
this.decorationLayer.setProperties(decorationProps)
59+
if (timeout) {
60+
this.clearMarkerTimeoutID = setTimeout(() => this.clearMarkers(), timeout)
61+
}
62+
}
63+
}

lib/motion-search.coffee

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ class SearchBase extends Motion
99
jump: true
1010
backwards: false
1111
useRegexp: true
12-
configScope: null
12+
caseSensitivityKind: null
1313
landingPoint: null # ['start' or 'end']
1414
defaultLandingPoint: 'start' # ['start' or 'end']
1515
relativeIndex: null
@@ -33,20 +33,6 @@ class SearchBase extends Motion
3333
else
3434
count
3535

36-
getCaseSensitivity: ->
37-
if @getConfig("useSmartcaseFor#{@configScope}")
38-
'smartcase'
39-
else if @getConfig("ignoreCaseFor#{@configScope}")
40-
'insensitive'
41-
else
42-
'sensitive'
43-
44-
isCaseSensitive: (term) ->
45-
switch @getCaseSensitivity()
46-
when 'smartcase' then term.search('[A-Z]') isnt -1
47-
when 'insensitive' then false
48-
when 'sensitive' then true
49-
5036
finish: ->
5137
if @isIncrementalSearch() and @getConfig('showHoverSearchCounter')
5238
@vimState.hoverSearchCounter.reset()
@@ -101,7 +87,7 @@ class SearchBase extends Motion
10187
# -------------------------
10288
class Search extends SearchBase
10389
@extend()
104-
configScope: "Search"
90+
caseSensitivityKind: "Search"
10591
requireInput: true
10692

10793
initialize: ->
@@ -206,7 +192,7 @@ class SearchBackwards extends Search
206192
# -------------------------
207193
class SearchCurrentWord extends SearchBase
208194
@extend()
209-
configScope: "SearchCurrentWord"
195+
caseSensitivityKind: "SearchCurrentWord"
210196

211197
moveCursor: (cursor) ->
212198
@input ?= (

lib/motion.coffee

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,12 @@ class Motion extends Base
116116
state.stop()
117117
oldPosition = newPosition
118118

119+
isCaseSensitive: (term) ->
120+
if @getConfig("useSmartcaseFor#{@caseSensitivityKind}")
121+
term.search(/[A-Z]/) isnt -1
122+
else
123+
not @getConfig("ignoreCaseFor#{@caseSensitivityKind}")
124+
119125
# Used as operator's target in visual-mode.
120126
class CurrentSelection extends Motion
121127
@extend(false)
@@ -890,14 +896,43 @@ class Find extends Motion
890896
inclusive: true
891897
offset: 0
892898
requireInput: true
899+
caseSensitivityKind: "Find"
893900

894901
initialize: ->
895902
super
896-
@focusInput() unless @isComplete()
903+
904+
@repeatIfNecessary()
905+
return if @isComplete()
906+
907+
if @getConfig("findByTwoChars")
908+
options =
909+
charsMax: 2
910+
autoConfirmTimeout: @getConfig("findByTwoCharsAutoConfirmTimeout")
911+
onChange: (char) => @highlightTextInCursorRows(char, "pre-confirm")
912+
onCancel: =>
913+
@vimState.highlightFind.clearMarkers()
914+
@cancelOperation()
915+
916+
options ?= {}
917+
options.purpose = "find"
918+
919+
@focusInput(options)
920+
921+
repeatIfNecessary: ->
922+
if @getConfig("reuseFindForRepeatFind")
923+
if @vimState.operationStack.getLastCommandName() in ["Find", "FindBackwards", "Till", "TillBackwards"]
924+
@input = @vimState.globalState.get("currentFind").input
925+
@repeated = true
897926

898927
isBackwards: ->
899928
@backwards
900929

930+
execute: ->
931+
super
932+
decorationType = "post-confirm"
933+
decorationType += "-long" if @isAsTargetExceptSelect()
934+
@highlightTextInCursorRows(@input, decorationType)
935+
901936
getPoint: (fromPoint) ->
902937
{start, end} = @editor.bufferRangeForBufferRow(fromPoint.row)
903938

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

913948
points = []
914-
@editor[method] ///#{_.escapeRegExp(@input)}///g, scanRange, ({range}) ->
915-
points.push(range.start)
949+
@editor[method] @getRegex(@input), scanRange, ({range}) -> points.push(range.start)
950+
916951
points[@getCount(-1)]?.translate([0, offset])
917952

953+
highlightTextInCursorRows: (text, decorationType) ->
954+
return unless @getConfig("highlightFindChar")
955+
ranges = []
956+
for cursor in @editor.getCursors()
957+
scanRange = cursor.getCurrentLineBufferRange()
958+
@editor.scanInBufferRange(@getRegex(text), scanRange, ({range}) -> ranges.push(range))
959+
@vimState.highlightFind.highlightRanges(ranges, decorationType)
960+
918961
moveCursor: (cursor) ->
919962
point = @getPoint(cursor.getBufferPosition())
920963
@setBufferPositionSafely(cursor, point)
921964
@globalState.set('currentFind', this) unless @repeated
922965

966+
getRegex: (term) ->
967+
modifiers = if @isCaseSensitive(term) then 'g' else 'gi'
968+
new RegExp(_.escapeRegExp(term), modifiers)
969+
923970
# keymap: F
924971
class FindBackwards extends Find
925972
@extend()

lib/operation-stack.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ module.exports = class OperationStack {
3838
return handler // DONT REMOVE
3939
}
4040

41+
getLastCommandName() {
42+
return this.lastCommandName
43+
}
44+
4145
reset() {
4246
this.resetCount()
4347
this.stack = []
@@ -222,7 +226,10 @@ module.exports = class OperationStack {
222226
}
223227

224228
finish(operation) {
225-
if (operation && operation.recordable) this.recordedOperation = operation
229+
if (operation) {
230+
if (operation.recordable) this.recordedOperation = operation
231+
this.lastCommandName = operation.name
232+
}
226233

227234
this.vimState.emitDidFinishOperation()
228235
if (operation && operation.isOperator()) operation.resetState()

0 commit comments

Comments
 (0)