From f126bf53ffd0471edbce97521a80548e161a0531 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kim=20Silkeb=C3=A6kken?= Date: Sat, 2 Apr 2011 15:10:17 +0200 Subject: [PATCH 1/5] Add grouping algorithms Two algorithms are defined: - Original: The original grouping - SCTree: Single-key/closest target priority tree --- plugin/EasyMotion.vim | 117 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/plugin/EasyMotion.vim b/plugin/EasyMotion.vim index 3092418..b1d0d1b 100644 --- a/plugin/EasyMotion.vim +++ b/plugin/EasyMotion.vim @@ -197,6 +197,123 @@ return char endfunction " }}} " }}} +" 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 target count array {{{ + let keys_count = {} + + for key in keys + let keys_count[key] = 0 + endfor + " }}} + + let targets_left = targets_len + let level = 0 + let i = 0 + + while targets_left > 0 + " Calculate the amount of child nodes based on the current level + let childs_len = (level == 0 ? 1 : (keys_len - 1) ) + + for key in keys + " Add child node count to the keys_count array + let keys_count[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[key] += targets_left + + break + endif + + let i += 1 + endfor + + let level += 1 + endwhile + " }}} + " Create group tree {{{ + let i = 0 + let key = 0 + + for [key_key, key_count] in items(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 + + let key += 1 + let i += key_count + endfor + " }}} + + " Finally! + return groups + endfunction + " }}} + " 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 + " }}} +" }}} " Core functions {{{ " Create key index {{{ function! s:CreateIndex(chars) " {{{ From 9e69b61e6a2ee8488d8fc3e955bb668d3c0f3993 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kim=20Silkeb=C3=A6kken?= Date: Sat, 2 Apr 2011 16:27:54 +0200 Subject: [PATCH 2/5] Make sure that the key count list is sorted This is a horrible workaround for unsorted dicts, but it works fine. --- plugin/EasyMotion.vim | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/plugin/EasyMotion.vim b/plugin/EasyMotion.vim index b1d0d1b..9e18944 100644 --- a/plugin/EasyMotion.vim +++ b/plugin/EasyMotion.vim @@ -218,11 +218,21 @@ " 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 target count array {{{ - let keys_count = {} + " 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 - let keys_count[key] = 0 + call add(keys_count, 0) + + let keys_count_keys[key] = i + + let i += 1 endfor " }}} @@ -236,7 +246,7 @@ for key in keys " Add child node count to the keys_count array - let keys_count[key] += childs_len + let keys_count[keys_count_keys[key]] += childs_len " Subtract the child node count let targets_left -= childs_len @@ -244,7 +254,7 @@ if targets_left <= 0 " Subtract the targets left if we added too many too " many child nodes to the key count - let keys_count[key] += targets_left + let keys_count[keys_count_keys[key]] += targets_left break endif @@ -259,7 +269,9 @@ let i = 0 let key = 0 - for [key_key, key_count] in items(keys_count) + call reverse(keys_count) + + for key_count in keys_count if key_count > 1 " We need to create a subgroup " Recurse one level deeper From e68d5b08cd85b65a0c94b76595af6b3dd8b09970 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kim=20Silkeb=C3=A6kken?= Date: Sat, 2 Apr 2011 16:30:10 +0200 Subject: [PATCH 3/5] Create function for creating a coord/key lookup dict --- plugin/EasyMotion.vim | 37 ++++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/plugin/EasyMotion.vim b/plugin/EasyMotion.vim index 9e18944..715118f 100644 --- a/plugin/EasyMotion.vim +++ b/plugin/EasyMotion.vim @@ -325,26 +325,33 @@ return groups endfunction " }}} -" }}} -" Core functions {{{ - " Create key index {{{ - function! s:CreateIndex(chars) " {{{ - let index_to_key = {} - let key_to_index = {} + " 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 : '' - let idx = 0 - for char in split(a:chars, '\zs') - let index_to_key[idx] = char - let key_to_index[char] = idx + for [key, item] in items(a:groups) + let key = ( ! empty(group_key) ? group_key : key) - let idx += 1 + 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 [index_to_key, key_to_index] - endfunction "}}} - - let [s:index_to_key, s:key_to_index] = s:CreateIndex(g:EasyMotion_keys) + return coord_keys + endfunction " }}} +" }}} +" Core functions {{{ function! s:PromptUser(groups) "{{{ let single_group = len(a:groups) == 1 let targets_len = single_group ? len(a:groups[0]) : len(a:groups) From e267452b43bf333a95410d3f7a7333cec7d168a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kim=20Silkeb=C3=A6kken?= Date: Sat, 2 Apr 2011 16:44:02 +0200 Subject: [PATCH 4/5] Make everything work with the new tree structured targets --- plugin/EasyMotion.vim | 97 ++++++++++++++++--------------------------- 1 file changed, 36 insertions(+), 61 deletions(-) diff --git a/plugin/EasyMotion.vim b/plugin/EasyMotion.vim index 715118f..82816df 100644 --- a/plugin/EasyMotion.vim +++ b/plugin/EasyMotion.vim @@ -46,6 +46,7 @@ \ , 'shade_hl' : 'EasyMotionShade' \ , 'do_shade' : 1 \ , 'do_mapping' : 1 + \ , 'grouping' : 1 \ }) " }}} " Default highlighting {{{ @@ -353,54 +354,44 @@ " }}} " Core functions {{{ function! s:PromptUser(groups) "{{{ - let single_group = len(a:groups) == 1 - let targets_len = single_group ? len(a:groups[0]) : len(a: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) + " 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 } + let lines[line_num] = { 'orig': current_line, 'marker': current_line } + endif + + 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 - let marker_char = s:index_to_key[single_group ? element : current_group] + " 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 - 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 - - " 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 - endif - - " Add highlighting coordinates - call add(hl_coords, '\%' . line_num . 'l\%' . col_num . 'c') - - let element += 1 - endfor - - let current_group += 1 + " Add highlighting coordinates + call add(hl_coords, '\%' . line_num . 'l\%' . col_num . 'c') endfor let lines_items = items(lines) @@ -415,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() " }}} @@ -443,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) " {{{ @@ -492,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' From 8ecb8829233885feb4cacc9a8bf66f9ad986163b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kim=20Silkeb=C3=A6kken?= Date: Sat, 2 Apr 2011 17:14:38 +0200 Subject: [PATCH 5/5] Add docs --- doc/easymotion.txt | 49 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/doc/easymotion.txt b/doc/easymotion.txt index 5d4e69a..74a3a25 100644 --- a/doc/easymotion.txt +++ b/doc/easymotion.txt @@ -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: >