Skip to content

Commit

Permalink
feat: refine the path during backtracking
Browse files Browse the repository at this point in the history
line

para
graph

parag
raph

In the example above, the start point is on `line` and the target is a blank
line after the second paragraph. For simplicity pretend that the only
available motions are j and }.

Preiously, the pathfinder would have returned j2}. This is because j comes
first in the settings and when } is tested, it goes to the same location with
the same cost and was ignored.

Now, both j and } will be stored in a list called `incoming_motions` attached
to that node. When we reach the target, we go back and choose the best
motion from each of those lists, meaning that 3} will be the output.

Fixes #10
  • Loading branch information
danth committed May 27, 2020
1 parent bb2b927 commit a948181
Showing 1 changed file with 94 additions and 24 deletions.
118 changes: 94 additions & 24 deletions plugin/main.vim
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,24 @@ function! s:Backtrack(node, function)
endwhile
endfunction


" Get a list of items which the two lists have in common
function! IntersectLists(list1, list2)
let result = copy(a:list1)
call filter(result, 'index(a:list2, v:val) >= 0')
return result
endfunction


" Calculate how much a:node.g should increase based on information from
" previous nodes
function! s:CalcGIncrement(node)
" Count how many times the reached_by motion has been repeated
" Count how many times the incoming motion has been repeated
let s:repetition_count = 0
let s:ginc_node = a:node
function! s:RepetitionCounter(node)
if a:node.reached_by == s:ginc_node.reached_by
if len(IntersectLists(a:node.incoming_motions,
\ s:ginc_node.incoming_motions))
let s:repetition_count += 1
else | return 1 | endif
endfunction
Expand All @@ -31,10 +43,9 @@ function! s:CalcGIncrement(node)
unlet s:repetition_count s:ginc_node
return g
endfunction

function! s:GetGIncrementFromCount(node, repetition_count)
" If the count is 1 (first time used), return the set weight of the motion
if a:repetition_count == 1 | return a:node.reached_by.weight | endif
if a:repetition_count == 1 | return a:node.incoming_motions[0].weight | endif

" Otherwise, return how many characters the count has increased in length by
" e.g. 2 -> 3 is 0, 9 -> 10 is 1, 1 -> 100 would be 2
Expand All @@ -44,15 +55,19 @@ function! s:GetGIncrementFromCount(node, repetition_count)
return len(a:repetition_count) - len(a:repetition_count - 1)
endfunction


function! s:CreateNode(view, rb, rf)
let node = {'key': a:view.lnum . ',' . a:view.col,
\ 'view': a:view,
\ 'reached_by': a:rb,
\ 'reached_from': a:rf}
\ 'reached_from': a:rf,
\ 'incoming_motions': [a:rb]}
let node.g = a:rf.g + s:CalcGIncrement(node)
return node
endfunction


" Test the a:motion from a:node, and return a new child node if the cursor
" moves
function! s:DoMotion(node, motion)
call winrestview(a:node.view)
try
Expand All @@ -67,27 +82,75 @@ function! s:DoMotion(node, motion)
endif
endfunction

function! s:EchoKeys(final_node)
let s:motions = []
let s:last_motion = ''
let s:c = 0
function! s:AddToMotionString(node)
if s:last_motion !=# a:node.reached_by.motion
call add(s:motions, (s:c > 1 ? s:c : '') . s:last_motion)
let s:last_motion = a:node.reached_by.motion
let s:c = 1
else | let s:c += 1 | endif

" Backtrack and select the best choices from each node.incoming_motions
function! s:RefinePath(final_node)
" Build a 2d list where each item is a list of the possible congruent
" motions which can be used there
let s:motion_options = []
function! s:AddToMotionOptions(node)
call add(s:motion_options, a:node.incoming_motions)
endfunction
call s:Backtrack(a:final_node, function('s:AddToMotionOptions'))
" Backtracking builds the list in reverse order, so we need to flip it
" around
call reverse(s:motion_options)

function! s:GetBestMotionFromTriplet(left, centre, right)
if len(a:centre) == 1 | return a:centre[0] | endif

" Look for motions we have in common with either side
let l_c = IntersectLists(a:left, a:centre)
let c_r = IntersectLists(a:centre, a:right)

if len(l_c) > 1 || len(c_r) > 1
" Look for motions we have in common with *both* sides
let l_c_r = IntersectLists(l_c, c_r)
if len(l_c_r) > 0 | return l_c_r[0] | endif
endif

if len(c_r) > 0 | return c_r[0] | endif
if len(l_c) > 0 | return l_c[0] | endif
return a:centre[0]
endfunction

call s:Backtrack(a:final_node, function('s:AddToMotionString'))
call add(s:motions, (s:c > 1 ? s:c : '') . s:last_motion)
" Select the best motion from each sub-list, such that we can combine as
" many together with counts as possible
" e.g. 3} is preferred over j2} where the first j and } have the same effect
let motions = []
let i = 0
while i < len(s:motion_options)
let left = i > 0 ? s:motion_options[i-1] : []
let right = i < len(s:motion_options)-1 ? s:motion_options[i+1] : []
call add(motions, s:GetBestMotionFromTriplet(left, s:motion_options[i], right))
let i += 1
endwhile

" Reverse to get the motions in the right order
echom join(reverse(s:motions), '')
unlet s:motion_options
return motions
endfunction

unlet s:motions s:last_motion s:c
" Call s:RefinePath and then print it in a human-readable format
function! s:EchoKeys(final_node)
let motions = s:RefinePath(a:final_node)

" Combine repeated motions using counts
let motion_string = ''
let last_motion = ''
let c = 0
for motion in motions
if last_motion !=# motion.motion
let motion_string .= (c > 1 ? c : '') . last_motion
let last_motion = motion.motion
let c = 1
else | let c += 1 | endif
endfor
let motion_string .= (c > 1 ? c : '') . last_motion

echom motion_string
endfunction


function! PathfinderRun()
if !exists('b:pf_start')
echom 'Please run :PathfinderBegin to set a start position first'
Expand Down Expand Up @@ -131,11 +194,18 @@ function! PathfinderRun()
if child_node == {} || has_key(closed_nodes, child_node.key) | continue | endif

if has_key(open_nodes, child_node.key)
" Replace the existing node if this one has a lower g
if child_node.g < open_nodes[child_node.key].g
call extend(open_nodes[child_node.key], child_node)
" There is already a node in the same position, update it
if child_node.g == open_nodes[child_node.key].g
\ && child_node.reached_from == open_nodes[child_node.key].reached_from
" This is an alternative motion with the same g value,
" add it to the list
call add(open_nodes[child_node.key].incoming_motions, motion)
elseif child_node.g < open_nodes[child_node.key].g
" This is a shorter route, discard the old ones
call extend(open_nodes[child_node.key], child_node)
endif
else
" Add new node
let open_nodes[child_node.key] = child_node
endif
endfor
Expand Down

0 comments on commit a948181

Please sign in to comment.