Permalink
Browse files

Merge branch 'feature/5-grouping-algorithms' into develop

* feature/5-grouping-algorithms:
  Add docs
  Make everything work with the new tree structured targets
  Create function for creating a coord/key lookup dict
  Make sure that the key count list is sorted
  Add grouping algorithms

Closes #5.
  • Loading branch information...
Lokaltog committed Apr 2, 2011
2 parents f5f6ecb + 8ecb882 commit 8ac570be5904d76eed83792b80845ecaa752cc15
Showing with 232 additions and 76 deletions.
  1. +47 −2 doc/easymotion.txt
  2. +185 −74 plugin/EasyMotion.vim
View
@@ -23,7 +23,8 @@ CONTENTS *easymotion-contents*
4.3 EasyMotion_shade_hl ............ |EasyMotion_shade_hl|
4.4 EasyMotion_do_shade ............ |EasyMotion_do_shade|
4.5 EasyMotion_do_mapping .......... |EasyMotion_do_mapping|
- 4.6 Custom mappings ................ |easymotion-custom-mappings|
+ 4.6 EasyMotion_grouping ............ |EasyMotion_grouping|
+ 4.7 Custom mappings ................ |easymotion-custom-mappings|
5. License ............................ |easymotion-license|
6. Known bugs ......................... |easymotion-known-bugs|
7. Contributing ....................... |easymotion-contributing|
@@ -167,7 +168,51 @@ this, see |easymotion-custom-mappings| for customizing the default mappings.
Default: 1
------------------------------------------------------------------------------
-4.6 Custom mappings *easymotion-custom-mappings*
+4.6 EasyMotion_grouping *EasyMotion_grouping*
+
+When there are too many possible targets on the screen, the results have to be
+grouped. This configuration option lets you change which grouping algorithm
+you want to use. There are two grouping algorithms available:
+
+ * Single-key priority (value: 1)
+ -------------------
+
+ This algorithm prioritizes single-key jumps for the targets closest to
+ the cursor and only groups the last jump targets to maximize the amount
+ of single-key jumps.
+
+ This algorithm works recursively and will work with as few keys as two.
+
+ Example (with |EasyMotion_keys| = "abcdef"): >
+
+ x x x x x x x x x
+<
+ The |w| motion is triggered: >
+
+ a b c d e f f f f
+ ^ ^ ^ ^ ^ Direct jump to target
+ ^ ^ ^ ^ Enter group "f"
+<
+ * Original (value: 2)
+ --------
+
+ This is the original algorithm which always groups all targets if there
+ are too many possible motion targets.
+
+ Example (with |EasyMotion_keys| = "abcdef"): >
+
+ x x x x x x x x x
+<
+ The |w| motion is triggered: >
+
+ a a a a a a b b b
+ ^ ^ ^ ^ ^ ^ Enter group "a"
+ ^ ^ ^ Enter group "b"
+
+Default: 1
+
+------------------------------------------------------------------------------
+4.7 Custom mappings *easymotion-custom-mappings*
EasyMotion allows you to customize all default mappings to avoid conflicts
with existing mappings. All custom mappings follow the same variable format: >
View
@@ -46,6 +46,7 @@
\ , 'shade_hl' : 'EasyMotionShade'
\ , 'do_shade' : 1
\ , 'do_mapping' : 1
+ \ , 'grouping' : 1
\ })
" }}}
" Default highlighting {{{
@@ -197,74 +198,200 @@
return char
endfunction " }}}
" }}}
-" Core functions {{{
- " Create key index {{{
- function! s:CreateIndex(chars) " {{{
- let index_to_key = {}
- let key_to_index = {}
+" Grouping algorithms {{{
+ let s:grouping_algorithms = {
+ \ 1: 'SCTree'
+ \ , 2: 'Original'
+ \ }
+ " Single-key/closest target priority tree {{{
+ " This algorithm tries to assign one-key jumps to all the targets closest to the cursor.
+ " It works recursively and will work correctly with as few keys as two.
+ function! s:GroupingAlgorithmSCTree(targets, keys)
+ " Prepare variables for working
+ let targets_len = len(a:targets)
+ let keys_len = len(a:keys)
+
+ let groups = {}
+
+ let keys = reverse(copy(a:keys))
+
+ " Semi-recursively count targets {{{
+ " We need to know exactly how many child nodes (targets) this branch will have
+ " in order to pass the correct amount of targets to the recursive function.
+
+ " Prepare sorted target count list {{{
+ " This is horrible, I know. But dicts aren't sorted in vim, so we need to
+ " work around that. That is done by having one sorted list with key counts,
+ " and a dict which connects the key with the keys_count list.
+
+ let keys_count = []
+ let keys_count_keys = {}
+
+ let i = 0
+ for key in keys
+ call add(keys_count, 0)
+
+ let keys_count_keys[key] = i
+
+ let i += 1
+ endfor
+ " }}}
+
+ let targets_left = targets_len
+ let level = 0
+ let i = 0
- let idx = 0
- for char in split(a:chars, '\zs')
- let index_to_key[idx] = char
- let key_to_index[char] = idx
+ while targets_left > 0
+ " Calculate the amount of child nodes based on the current level
+ let childs_len = (level == 0 ? 1 : (keys_len - 1) )
- let idx += 1
- endfor
+ for key in keys
+ " Add child node count to the keys_count array
+ let keys_count[keys_count_keys[key]] += childs_len
+
+ " Subtract the child node count
+ let targets_left -= childs_len
+
+ if targets_left <= 0
+ " Subtract the targets left if we added too many too
+ " many child nodes to the key count
+ let keys_count[keys_count_keys[key]] += targets_left
+
+ break
+ endif
+
+ let i += 1
+ endfor
+
+ let level += 1
+ endwhile
+ " }}}
+ " Create group tree {{{
+ let i = 0
+ let key = 0
+
+ call reverse(keys_count)
+
+ for key_count in keys_count
+ if key_count > 1
+ " We need to create a subgroup
+ " Recurse one level deeper
+ let groups[a:keys[key]] = s:GroupingAlgorithmSCTree(a:targets[i : i + key_count - 1], a:keys)
+ elseif key_count == 1
+ " Assign single target key
+ let groups[a:keys[key]] = a:targets[i]
+ else
+ " No target
+ continue
+ endif
- return [index_to_key, key_to_index]
- endfunction "}}}
+ let key += 1
+ let i += key_count
+ endfor
+ " }}}
- let [s:index_to_key, s:key_to_index] = s:CreateIndex(g:EasyMotion_keys)
+ " Finally!
+ return groups
+ endfunction
" }}}
- function! s:PromptUser(groups) "{{{
- let single_group = len(a:groups) == 1
- let targets_len = single_group ? len(a:groups[0]) : len(a:groups)
+ " Original {{{
+ function! s:GroupingAlgorithmOriginal(targets, keys)
+ " Split targets into groups (1 level)
+ let targets_len = len(a:targets)
+ let keys_len = len(a:keys)
+
+ let groups = {}
+
+ let i = 0
+ let root_group = 0
+ try
+ while root_group < targets_len
+ let groups[a:keys[root_group]] = {}
+
+ for key in a:keys
+ let groups[a:keys[root_group]][key] = a:targets[i]
+
+ let i += 1
+ endfor
+
+ let root_group += 1
+ endwhile
+ catch | endtry
+
+ " Flatten the group array
+ if len(groups) == 1
+ let groups = groups[a:keys[0]]
+ endif
+
+ return groups
+ endfunction
+ " }}}
+ " Coord/key dictionary creation {{{
+ function! s:CreateCoordKeyDict(groups, ...)
+ " Dict structure:
+ " 1,2 : a
+ " 2,3 : b
+ let coord_keys = {}
+ let group_key = a:0 == 1 ? a:1 : ''
+
+ for [key, item] in items(a:groups)
+ let key = ( ! empty(group_key) ? group_key : key)
+
+ if type(item) == 3
+ " Destination coords
+ let coord_keys[join(item, ',')] = key
+ else
+ " Item is a dict (has children)
+ call extend(coord_keys, s:CreateCoordKeyDict(item, key))
+ endif
+
+ unlet item
+ endfor
+ return coord_keys
+ endfunction
+ " }}}
+" }}}
+" Core functions {{{
+ function! s:PromptUser(groups) "{{{
" If only one possible match, jump directly to it {{{
- if single_group && targets_len == 1
+ let group_values = values(a:groups)
+
+ if len(group_values) == 1
redraw
- return a:groups[0][0]
+ return group_values[0]
endif
" }}}
" Prepare marker lines {{{
let lines = {}
let hl_coords = []
- let current_group = 0
- for group in a:groups
- let element = 0
+ for [coords, target_key] in items(s:CreateCoordKeyDict(a:groups))
+ let [line_num, col_num] = split(coords, ',')
- for [line_num, col_num] in group
- " Add original line and marker line
- if ! has_key(lines, line_num)
- let current_line = getline(line_num)
-
- let lines[line_num] = { 'orig': current_line, 'marker': current_line }
- endif
+ " Add original line and marker line
+ if ! has_key(lines, line_num)
+ let current_line = getline(line_num)
- let marker_char = s:index_to_key[single_group ? element : current_group]
-
- if strlen(lines[line_num]['marker']) > 0
- " Replace hard tab with spaces
- if match(lines[line_num]['marker'], '\%' . col_num . 'c\t') != -1
- let marker_char .= repeat(' ', string(&tabstop) - strlen(marker_char))
- endif
+ let lines[line_num] = { 'orig': current_line, 'marker': current_line }
+ endif
- " Substitute marker character if line length > 0
- let lines[line_num]['marker'] = substitute(lines[line_num]['marker'], '\%' . col_num . 'c.', marker_char, '')
- else
- " Set the line to the marker character if the line is empty
- let lines[line_num]['marker'] = marker_char
+ if strlen(lines[line_num]['marker']) > 0
+ " Replace hard tab with spaces
+ if match(lines[line_num]['marker'], '\%' . col_num . 'c\t') != -1
+ let target_key .= repeat(' ', string(&tabstop) - strlen(target_key))
endif
- " Add highlighting coordinates
- call add(hl_coords, '\%' . line_num . 'l\%' . col_num . 'c')
-
- let element += 1
- endfor
+ " Substitute marker character if line length > 0
+ let lines[line_num]['marker'] = substitute(lines[line_num]['marker'], '\%' . col_num . 'c.', target_key, '')
+ else
+ " Set the line to the marker character if the line is empty
+ let lines[line_num]['marker'] = target_key
+ endif
- let current_group += 1
+ " Add highlighting coordinates
+ call add(hl_coords, '\%' . line_num . 'l\%' . col_num . 'c')
endfor
let lines_items = items(lines)
@@ -279,12 +406,8 @@
redraw
- " Get target/group character {{{
- if single_group
- call s:Prompt('Target character')
- else
- call s:Prompt('Group character')
- endif
+ " Get target character {{{
+ call s:Prompt('Target key')
let char = s:GetChar()
" }}}
@@ -307,17 +430,19 @@
endif
" }}}
" Check if the input char is valid {{{
- if ! has_key(s:key_to_index, char) || s:key_to_index[char] >= targets_len
+ if ! has_key(a:groups, char)
throw 'Invalid target'
endif
" }}}
- if single_group
+ let target = a:groups[char]
+
+ if type(target) == 3
" Return target coordinates
- return a:groups[0][s:key_to_index[char]]
+ return target
else
- " Prompt for target character
- return s:PromptUser([a:groups[s:key_to_index[char]]])
+ " Prompt for new target character
+ return s:PromptUser(target)
endif
endfunction "}}}
function! s:EasyMotion(regexp, direction, visualmode, mode) " {{{
@@ -356,24 +481,10 @@
throw 'No matches'
endif
" }}}
- " Split targets into key groups {{{
- let groups_len = len(s:index_to_key)
- let groups = []
- let i = 0
- while i < targets_len
- call add(groups, targets[i : i + groups_len - 1])
+ let GroupingFn = function('s:GroupingAlgorithm' . s:grouping_algorithms[g:EasyMotion_grouping])
+ let groups = GroupingFn(targets, split(g:EasyMotion_keys, '\zs'))
- let i += groups_len
- endwhile
- " }}}
- " Too many groups; only display the first ones {{{
- if len(groups) > groups_len
- call s:Message('Only displaying the first matches')
-
- let groups = groups[0 : groups_len - 1]
- endif
- " }}}
" Shade inactive source {{{
if g:EasyMotion_do_shade
let shade_hl_pos = '\%' . orig_pos[0] . 'l\%'. orig_pos[1] .'c'

0 comments on commit 8ac570b

Please sign in to comment.