diff --git a/autoload/EasyMotion.vim b/autoload/EasyMotion.vim index a8f563a..0483562 100644 --- a/autoload/EasyMotion.vim +++ b/autoload/EasyMotion.vim @@ -181,13 +181,21 @@ function! EasyMotion#WB(visualmode, direction) " {{{ endfunction " }}} function! EasyMotion#WBW(visualmode, direction) " {{{ let s:current.is_operator = mode(1) ==# 'no' ? 1: 0 - call s:EasyMotion('\(\(^\|\s\)\@<=\S\|^$\)', a:direction, a:visualmode ? visualmode() : '', 0) + let regex_without_file_ends = '\v(^|\s)\zs\S|^$' + let regex = l:regex_without_file_ends + \ . (a:direction == 1 ? '' : '|%$') + \ . (a:direction == 0 ? '' : '|%^') + call s:EasyMotion(l:regex, a:direction, a:visualmode ? visualmode() : '', 0) return s:EasyMotion_is_cancelled endfunction " }}} function! EasyMotion#WBK(visualmode, direction) " {{{ " vim's iskeyword style word motion let s:current.is_operator = mode(1) ==# 'no' ? 1: 0 - call s:EasyMotion('\(\(\<\|\>\|\s\)\@<=\S\|^$\)', a:direction, a:visualmode ? visualmode() : '', 0) + let regex_without_file_ends = '\v<|^\S|\s\zs\S|>\zs\S|^$' + let regex = l:regex_without_file_ends + \ . (a:direction == 1 ? '' : '|%$') + \ . (a:direction == 0 ? '' : '|%^') + call s:EasyMotion(l:regex, a:direction, a:visualmode ? visualmode() : '', 0) return s:EasyMotion_is_cancelled endfunction " }}} function! EasyMotion#E(visualmode, direction) " {{{ @@ -199,14 +207,30 @@ endfunction " }}} function! EasyMotion#EW(visualmode, direction) " {{{ let s:current.is_operator = mode(1) ==# 'no' ? 1: 0 let is_inclusive = mode(1) ==# 'no' ? 1 : 0 - call s:EasyMotion('\(\S\(\s\|$\)\|^$\)', a:direction, a:visualmode ? visualmode() : '', is_inclusive) + " Note: The stopping positions for 'E' and 'gE' differs. Thus, the regex + " for direction==2 cannot be the same in both directions. This will be + " ignored. + let regex_stub = '\v\S(\s|$)' + let regex = l:regex_stub + \ . (a:direction == 0 ? '' : '|^$|%^') + \ . (a:direction == 1 ? '' : '|%$') + call s:EasyMotion(l:regex, a:direction, a:visualmode ? visualmode() : '', 0) return s:EasyMotion_is_cancelled endfunction " }}} function! EasyMotion#EK(visualmode, direction) " {{{ " vim's iskeyword style word motion let s:current.is_operator = mode(1) ==# 'no' ? 1: 0 let is_inclusive = mode(1) ==# 'no' ? 1 : 0 - call s:EasyMotion('\(\S\(\>\|\<\|\s\)\@=\|^$\)', a:direction, a:visualmode ? visualmode() : '', is_inclusive) + " Note: The stopping positions for 'e' and 'ge' differs. Thus, the regex + " for direction==2 cannot be the same in both directions. This will be + " ignored. + let regex_stub = '\v.\ze>|\S\ze\s*$|\S\ze\s|\k\zs>\S\ze|\S<' + let regex = l:regex_stub + \ . (a:direction == 0 ? '' : '|^$|%^') + \ . (a:direction == 1 ? '' : '|%$') + call s:EasyMotion(l:regex, a:direction, a:visualmode ? visualmode() : '', 0) + + return s:EasyMotion_is_cancelled endfunction " }}} " -- JK Motion --------------------------- diff --git a/t/compare_movements_spec.vim b/t/compare_movements_spec.vim new file mode 100644 index 0000000..0ea6341 --- /dev/null +++ b/t/compare_movements_spec.vim @@ -0,0 +1,274 @@ +"============================================================================= +" FILE: t/compare_movements_spec.vim +" AUTHOR: YggdrasiI +" Test: https://github.com/kana/vim-vspec +" Description: EasyMotion keyword movement test with vim-vspec +" License: MIT license {{{ +" Permission is hereby granted, free of charge, to any person obtaining +" a copy of this software and associated documentation files (the +" "Software"), to deal in the Software without restriction, including +" without limitation the rights to use, copy, modify, merge, publish, +" distribute, sublicense, and/or sell copies of the Software, and to +" permit persons to whom the Software is furnished to do so, subject to +" the following conditions: +" +" The above copyright notice and this permission notice shall be included +" in all copies or substantial portions of the Software. +" +" THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +" OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +" MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +" IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +" CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +" TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +" SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +" }}} +"============================================================================= + +" Setup {{{ +let s:root_dir = matchstr(system('git rev-parse --show-cdup'), '[^\n]\+') + +" The consumed time depends from the length of the text and could be really high +" on vimdoc pages. (See it 'Loop through Vim help buffer and compare movements') +" Reduce this value to stop CompareMovements(...) before it reached the end of the +" buffer. +let s:maximal_number_of_compared_movments = 10000 +execute 'set' 'rtp +=./'.s:root_dir +runtime! plugin/EasyMotion.vim +" }}} + +" Functions for Test {{{ +function! AddLine(str) + put =a:str +endfunction + +function! CursorPos() + return [line('.'), col('.'), getline('.')[col('.')-1]] +endfunction + +" Nested normal to avoid throwing readonly errors. They abort the testing. +function TryNormal(str) + try + exec 'normal ' . a:str + catch /^Vim\%((\a\+)\)\=:E21/ + endtry + return 0 +endfunction + +let s:to_cursor = {} +function! s:to_cursor.match(actual, expected) + return a:actual == a:expected +endfunction + +" Add metadata about failure. +function! s:to_cursor.failure_message_for_should(actual, expected) + Expect a:actual[0] > 0 + Expect a:expected[0] > 0 + Expect a:actual[0] <= getpos('$')[1] + Expect a:expected[0] <= getpos('$')[1] + Expect a:actual[1] > 0 + Expect a:expected[1] > 0 + + let line1 = getline(a:actual[0]) + let line2 = getline(a:expected[0]) + " Change char on cursor to '█'. + let line1 = strpart(l:line1, 0, a:actual[1]-1) + \ . '█' + \ . strpart(l:line1, a:actual[1]) + let line2 = strpart(l:line2, 0, a:expected[1]-1) + \ . '█' + \ . strpart(l:line2, a:expected[1]) + " Separation of both cases with \n would be nice, but + " vim-vspec allow oneliners as return string, only. + let msg = 'Line ' . string(a:actual[0]) . ": '" . l:line1 + \ . "',\x09\x09 Line " . string(a:expected[0]) . ": '" . l:line2 . "'\x0a" + return l:msg +endfunction + +function! CompareMovements(movement1, movement2, backward) + let jumpmarks = [ + \ [a:movement1, []], + \ [a:movement2, []], + \ ] + + " Loop through current buffer in both variants {{ + for [l:handler, l:list] in l:jumpmarks + if a:backward == 1 + let last_line = line('$') + let last_char = len(getline(l:last_line)) + call cursor(l:last_line, l:last_char) + else + call cursor([1,1]) + endif + + let lastpos = [0,0] + + " Centralize line. Otherwise, Easymotion functions aborts + " at the end of the (virtual) window. + call TryNormal('zz') + call TryNormal(l:handler) + let curpos = getpos(".")[1:2] + + while l:lastpos != l:curpos + let list += [l:curpos] + let lastpos = l:curpos + call TryNormal('zz') + call TryNormal(l:handler) + let curpos = getpos(".")[1:2] + " Abort after a fixed number of steps. + if len(l:list) > s:maximal_number_of_compared_movments + break + endif + endwhile + endfor + " }} + + " The resulting lists are stored in l:jumpmarks[*][1], now. + let [l:cursor_positions1, l:cursor_positions2] = [ l:jumpmarks[0][1], l:jumpmarks[1][1] ] + + if l:cursor_positions1 == l:cursor_positions2 + return 0 + endif + + " Search for first unmatching position. {{ + let index = 0 + let len = min([len(l:cursor_positions2), len(l:cursor_positions1)]) + while l:index < l:len + Expect l:cursor_positions2[l:index] to_cursor l:cursor_positions1[l:index] + let index += 1 + endwhile + + " Collision with begin or end of file or while loop aborts to early. + if a:backward == 1 + Expect join([a:movement2, ': File begin reached after ', len(l:cursor_positions2), ' steps.']) + \ == join([a:movement1, ': File begin reached after ', len(l:cursor_positions1), ' steps.']) + else + Expect l:cursor_positions2[l:index-1] to_cursor l:cursor_positions1[l:index] + Expect join([a:movement2, ': File end reached after ', len(l:cursor_positions2), ' steps.']) + \ == join([a:movement1, ': File end reached after ', len(l:cursor_positions1), ' steps.']) + endif + " }} + + return -1 +endfunction + +" Hand crafted text with rare cases +function! InsertTestText1() + + " Blanks at document begin + call AddLine('') + call AddLine(' ') + call AddLine('') + + call AddLine('scriptencoding utf-8') + + " '^\s*[not-\k]'-case + call AddLine('!foo') + call AddLine(' !bar') + + call AddLine('s! ') + + " Blanks at document end + call AddLine('') + call AddLine(' ') + call AddLine('') +endfunction + +"}}} + +"Keyword word motion {{{ +describe 'Keyword word motion' + before + new + resize 10 + nmap a + let g:EasyMotion_keys = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' + let g:EasyMotion_maximal_jumpmarks = 2 " Error for value 1 unanalyzed. + nmap w (easymotion-iskeyword-w) + nmap b (easymotion-iskeyword-b) + nmap e (easymotion-iskeyword-e) + nmap ge (easymotion-iskeyword-ge) + nmap W (easymotion-W) + nmap B (easymotion-B) + nmap E (easymotion-E) + nmap gE (easymotion-gE) + call EasyMotion#init() + call vspec#customize_matcher('to_cursor', s:to_cursor) + end + + after + close! + end + + it 'Simple test to check setup of this test' + " Check if a is remapped to to avoid start of insert mode. + normal aa\ + Expect getline(1) == '' + + call AddLine('word') + Expect CompareMovements('w', 'w', 0) == 0 + Expect CompareMovements('w', '\wa', 0) == 0 + Expect CompareMovements('b', '\ba', 1) == 0 + Expect CompareMovements('e', '\ea', 0) == 0 + Expect CompareMovements('ge', '\gea', 1) == 0 + Expect CompareMovements('W', '\Wa', 0) == 0 + Expect CompareMovements('B', '\Ba', 1) == 0 + Expect CompareMovements('E', '\Ea', 0) == 0 + Expect CompareMovements('gE', '\gEa', 1) == 0 + end + + it 'w' + call InsertTestText1() + Expect CompareMovements('w', '\wa', 0) == 0 + end + + it 'b' + call InsertTestText1() + Expect CompareMovements('b', '\ba', 1) == 0 + end + + it 'e' + call InsertTestText1() + Expect CompareMovements('e', '\ea', 0) == 0 + end + + it 'ge' + call InsertTestText1() + Expect CompareMovements('ge', '\gea', 1) == 0 + end + + it 'W' + call InsertTestText1() + Expect CompareMovements('W', 'W', 0) == 0 + end + + it 'B' + call InsertTestText1() + Expect CompareMovements('B', 'B', 1) == 0 + end + + it 'E' + call InsertTestText1() + Expect CompareMovements('E', 'E', 0) == 0 + end + + it 'gE' + call InsertTestText1() + Expect CompareMovements('gE', 'gE', 1) == 0 + end + + " Really time consuming test... + "it 'Loop through Vim help buffer and compare movements' + " help motion.txt + " Expect expand('%:t') ==# 'motion.txt' + " "Optional: Copy text into editable buffer + " exec "normal! Gygg\cP" + " Expect CompareMovements('w', '\wa', 0) == 0 + "end + +end +"}}} + +" __END__ {{{ +" vim: fdm=marker:et:ts=4:sw=4:sts=4 +" }}} diff --git a/t/easymotion_spec.vim b/t/easymotion_spec.vim index 8673f3a..df808a3 100644 --- a/t/easymotion_spec.vim +++ b/t/easymotion_spec.vim @@ -217,7 +217,7 @@ describe 'Default settings' "}}} end - it 'provide default mappings for regrex motion' + it 'provide default mappings for regex motion' "(is_visual, direction) " direction: " - 0: forward @@ -1411,8 +1411,8 @@ describe 'Word motion' close! end - " Default word motion {{ - it 'Default word motion' + " Word motion {{ + it 'Word motion' normal! 0 let l = line('.') Expect CursorPos() == [l,1,'p'] @@ -1430,7 +1430,7 @@ describe 'Word motion' normal bh Expect CursorPos() == [l,1,'p'] end - "}}} + "}} end describe 'Verbose'