Skip to content

Commit

Permalink
refactor: create MotionGenerator classes
Browse files Browse the repository at this point in the history
BREAKING CHANGE: `g:pf_motions` has been removed. Descriptions are now
stored in `g:pf_descriptions`. Weights are inferred from the number of
characters in the motion (configurable weights may return with #41). It
is no longer possible to disable motions individually.
  • Loading branch information
danth committed Aug 11, 2020
1 parent 184889a commit 41343f0
Show file tree
Hide file tree
Showing 12 changed files with 206 additions and 258 deletions.
50 changes: 14 additions & 36 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,17 +80,15 @@ you get two commands instead:
*pathfinder.vim works out-of-the box with the default configuration. You don't
need to read this section if you don't want to.*

### General settings

#### `g:pf_popup_time`
### `g:pf_popup_time`
Milliseconds to display the popup for. *Default: 3000*

#### `g:pf_autorun_delay`
### `g:pf_autorun_delay`
When this number of seconds have elapsed with no motions being made, the
pathfinder will run. It also runs for other events such as changing modes.
A negative value will disable automatic suggestions. *Default: 2*

#### `g:pf_explore_scale`
### `g:pf_explore_scale`
Multiplier which determines the range of lines to be explored around the start
and target positions. This is calculated as (lines between start and target
× multiplier) and added to both sides. *Default: 0.5*
Expand All @@ -104,41 +102,21 @@ If you have a powerful computer, you can increase this option to a high value
to allow exploring more of the file. You can also disable it completely by
setting a negative value.

#### `g:pf_max_explore`
### `g:pf_max_explore`
Cap the number of surrounding lines explored (see above) to a maximum value.
As usual, this can be disabled by making it negative. *Default: 10*

### Motions

A global variable is used to set the available motions:
### `g:pf_descriptions`
Dictionary of descriptions, used for `:PathfinderExplain`.

```vim
let g:pf_motions = [
\ {'motion': 'h', 'weight': 1, 'description': 'Left {count} columns'},
\ {'motion': 'l', 'weight': 1, 'description': 'Right {count} columns'},
\ {'motion': 'j', 'weight': 1, 'description': 'Down {count} lines'},
\ {'motion': 'k', 'weight': 1, 'description': 'Up {count} lines'},
\ ...
\ ]
let g:pf_descriptions['k'] = 'Up {count} lines'
let g:pf_descriptions['f'] = 'To occurence {count} of "{argument}", to the right'
```

This contains all the supported motions by default. If you do decide to change
it, you will need to copy the entire list from [defaults.vim](plugin/defaults.vim).
There is no way to edit a single motion without doing that.

The higher a motion's `weight`, the less the pathfinding algorithm wants to use
that motion. The path with the lowest total weight wins. The default settings
use the number of characters in the motion as its weight (excluding modifier
keys).

Repeating a motion will not use its predefined weight. Instead, the cost is
calculated based on the effect adding another repetition will have on the
count. This is easier to explain with examples:

| Motion | Cost of adding the repetition |
| --- | --- |
| `j` | (uses configured weight) |
| `j` -> `2j` | 1, since the `2` has been added |
| `2j` -> `3j` | 0, because `3j` is no longer than `2j` |
| `9j` -> `10j` | 1, since `10j` is a character longer than `9j` |
| `1j` -> `100j` | 2, since `100j` is 2 characters longer than `1j` |
Ensure the plugin is loaded before trying to override keys. Otherwise, the
default dictionary will not exist and you'll get an error.

Re-defining the entire dictionary is not recommended since it could cause
problems if support for a new motion is added and you don't have a description
for it.
3 changes: 0 additions & 3 deletions pathfinder/client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,9 +157,6 @@ def pathfind(self, buffer_contents, start_view, target_view, callback):
"target": target_view,
"min_line": min_line,
"max_line": max_line,
# Using vim.vars would return a vim.list object which we cannot send
# because it can't be pickled
"motions": vim.eval("g:pf_motions"),
"size": (
# WindowTextWidth() - see plugin/dimensions.vim
vim.eval("WindowTextWidth()"),
Expand Down
36 changes: 20 additions & 16 deletions pathfinder/client/output.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,23 @@

import vim

from pathfinder.debytes import debytes

last_output = None


def get_count(motion, count):
"""Build a string like 'k', 'hh', '15w'"""
motion_str = motion.motion + (motion.argument or "")
if count == 1:
return motion.name
return motion_str

elif count == 2 and len(motion.name) == 1:
elif count == 2 and len(motion_str) == 1:
# It's easier to press a single-character motion twice
# than to type a 2 before it
return motion.name + motion.name
return (motion_str) * 2

return str(count) + motion.name
return str(count) + motion_str


def compact_motions(motions):
Expand All @@ -32,26 +35,27 @@ def compact_motions(motions):
)


def get_description(motion, repetitions):
description = debytes(vim.vars["pf_descriptions"][motion.motion])
description = description.replace("{count}", str(repetitions))
if motion.argument is not None:
description = description.replace("{argument}", motion.argument)
return description


def explained_motions(motions):
"""
Yield each motion in the form "motion <padding> help"
e.g. ['5j Down 5 lines', '$ To the end of the line']
"""
# List of tuples of (count, count combined with motion, Motion instance)
counted_motions = list()
for motion, group in itertools.groupby(motions):
repetitions = len(list(group))
counted = get_count(motion, repetitions)
counted_motions.append((repetitions, counted, motion))

# Maximum length of the '5j', '$' etc. strings
max_counted_len = max(len(c[1]) for c in counted_motions)

for repetitions, counted, motion in counted_motions:
padding = " " * (max_counted_len - len(counted))
description = motion.description_template.replace("{count}", str(repetitions))
yield padding + counted + " " + description
yield (
get_count(motion, repetitions)
+ " "
+ get_description(motion, repetitions)
)


def show_output(motions):
Expand Down
19 changes: 0 additions & 19 deletions pathfinder/motion.py

This file was deleted.

102 changes: 0 additions & 102 deletions pathfinder/server/child_views.py

This file was deleted.

5 changes: 3 additions & 2 deletions pathfinder/server/dijkstra.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from heapdict import heapdict

from pathfinder.motion import motions
from pathfinder.server.motions.simple import SimpleMotionGenerator
from pathfinder.server.motions.find import FindMotionGenerator
from pathfinder.server.node import Node


Expand All @@ -20,7 +21,7 @@ def __init__(self, from_view, target_view, min_line, max_line):
self.min_line = min_line
self.max_line = max_line

self.available_motions = list(motions())
self.motion_generators = {SimpleMotionGenerator(self), FindMotionGenerator(self)}

self._open_queue = heapdict() # Min-priority queue: Key -> Distance
self._open_nodes = dict() # Key -> Node
Expand Down
22 changes: 22 additions & 0 deletions pathfinder/server/motions/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from collections import namedtuple
import abc

from pathfinder.server.node import Node


# motion - The motion such as h,j,k,f,T,gM
# argument - Used for the additional argument to f,t,/ etc
Motion = namedtuple("Motion", "motion argument")


class MotionGenerator(abc.ABC):
def __init__(self, dijkstra):
self.dijkstra = dijkstra

@abc.abstractmethod
def generate(self, view):
"""Yield all neighbouring nodes found from the given view."""
pass

def _create_node(self, *args, **kwargs):
return Node(self.dijkstra, *args, **kwargs)
45 changes: 45 additions & 0 deletions pathfinder/server/motions/find.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import vim

from pathfinder.server.motions import MotionGenerator, Motion


class FindMotionGenerator(MotionGenerator):
MOTIONS = {"f", "t", "F", "T"}

def generate(self, view):
for motion in self.MOTIONS:
yield from self._find(view, motion)

def _find(self, view, motion):
line_text = vim.current.buffer[view.lnum - 1]
seen_characters = set()

# characters = string of characters which may be accessible using this motion
# column = lambda function which converts index in `characters` to a column number
if motion == "f" and view.col < len(line_text):
column = lambda i: view.col + i + 1
characters = line_text[view.col + 1 :]
elif motion == "t" and view.col < len(line_text) - 1:
column = lambda i: view.col + i + 1
characters = line_text[view.col + 2 :]
seen_characters.add(line_text[view.col + 1])
elif motion == "F" and view.col > 0 and len(line_text) > view.col:
column = lambda i: view.col - i - 1
# Characters are reversed because we are looking backwards
characters = line_text[: view.col][::-1]
elif motion == "T" and view.col > 1 and len(line_text) > view.col:
column = lambda i: view.col - i - 1
characters = line_text[: view.col - 1][::-1]
seen_characters.add(line_text[view.col - 1])
else:
return

for i, character in enumerate(characters):
# Only use each unique character once
if character in seen_characters:
continue
seen_characters.add(character)

new_col = column(i)
new_view = view._replace(col=new_col, curswant=new_col)
yield self._create_node(new_view, Motion(motion, character))
31 changes: 31 additions & 0 deletions pathfinder/server/motions/simple.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import vim

from pathfinder.window import winsaveview, winrestview
from pathfinder.server.motions import MotionGenerator, Motion


class SimpleMotionGenerator(MotionGenerator):
MOTIONS = {"h", "j", "k", "l", "gj", "gk", "gg", "G", "H", "M", "L", "",
"", "", "", "", "", "zt", "z\", "z.", "zb", "z-",
"0", "^", "g^", "$", "g$", "g_", "gm", "gM", "W", "E", "B",
"gE", "w", "e", "b", "ge", "(", ")", "{", "}", "]]", "][","[[",
"[]", "]m", "[m", "]M", "[M", "*", "#", "g*", "g#", "%"}

def generate(self, view):
for motion in self.MOTIONS:
result_view = self._try_motion(view, motion)
if result_view is not None and result_view != view:
yield self._create_node(result_view, Motion(motion, None))

def _try_motion(self, view, motion):
"""
Use a motion inside Vim, starting from the given view.
If the motion causes an error, return None.
"""
winrestview(view)
try:
vim.command(f"silent! normal! {motion}")
except:
return None
return winsaveview()
Expand Down
Loading

0 comments on commit 41343f0

Please sign in to comment.