Full Code of AlphaMycelium/pathfinder.vim for AI

master 5b6343de1b46 cached
49 files
73.8 KB
19.3k tokens
129 symbols
1 requests
Download .txt
Repository: AlphaMycelium/pathfinder.vim
Branch: master
Commit: 5b6343de1b46
Files: 49
Total size: 73.8 KB

Directory structure:
gitextract_r8990n86/

├── .github/
│   ├── CODEOWNERS
│   └── workflows/
│       ├── pr.yml
│       └── release.yml
├── .gitignore
├── .gitmodules
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE
├── MANIFEST.in
├── README.md
├── pathfinder/
│   ├── __init__.py
│   ├── client/
│   │   ├── __init__.py
│   │   ├── autorun.py
│   │   ├── client.py
│   │   ├── explore_lines.py
│   │   ├── output.py
│   │   ├── plugin.py
│   │   ├── popup.py
│   │   └── state_tracker.py
│   ├── debytes.py
│   ├── server/
│   │   ├── __init__.py
│   │   ├── dijkstra.py
│   │   ├── motions/
│   │   │   ├── __init__.py
│   │   │   ├── find.py
│   │   │   ├── search.py
│   │   │   └── simple.py
│   │   ├── node.py
│   │   └── server.py
│   └── window.py
├── plugin/
│   ├── defaults.vim
│   ├── dimensions.vim
│   └── main.vim
├── serverrc.vim
├── setup.cfg
├── setup.py
├── test_requirements.txt
└── tests/
    ├── __init__.py
    ├── conftest.py
    ├── test_autorun.py
    ├── test_debytes.py
    ├── test_explore_lines.py
    ├── test_motions_find.py
    ├── test_motions_search.py
    ├── test_motions_simple.py
    ├── test_node.py
    ├── test_output.py
    ├── test_popup.py
    ├── test_state_tracker.py
    └── test_window.py

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/CODEOWNERS
================================================
LICENSE @AlphaMycelium
.github/ @AlphaMycelium
setup.cfg @AlphaMycelium


================================================
FILE: .github/workflows/pr.yml
================================================
name: Checks

on: [pull_request]

jobs:
  test:
    name: Test on Python ${{ matrix.python-version }}
    runs-on: ubuntu-latest

    strategy:
      matrix:
        python-version: [3.6, 3.7, 3.8, 3.9]

    steps:
    - name: Set up Python ${{ matrix.python-version }}
      uses: actions/setup-python@v1
      with:
        python-version: ${{ matrix.python-version }}

    - name: Checkout repository
      uses: actions/checkout@v2

    - name: Cache dependencies
      uses: actions/cache@v1
      with:
        path: ~/.cache/pip
        key: ${{ runner.os }}-pip-${{ hashFiles('test_requirements.txt') }}
        restore-keys: |
          ${{ runner.os }}-pip-

    - name: Install dependencies
      run: python -m pip install -r test_requirements.txt

    - name: Install pytest-github-actions-annotate-failures
      run: python -m pip install pytest-github-actions-annotate-failures

    - name: Run pytest
      run: pytest -v


================================================
FILE: .github/workflows/release.yml
================================================
name: Release

on:
  push:
    branches:
      - master

jobs:
  test:
    name: Test on Python ${{ matrix.python-version }}
    runs-on: ubuntu-latest

    strategy:
      matrix:
        python-version: [3.6, 3.7, 3.8, 3.9]

    steps:
    - name: Set up Python ${{ matrix.python-version }}
      uses: actions/setup-python@v1
      with:
        python-version: ${{ matrix.python-version }}

    - name: Checkout repository
      uses: actions/checkout@v2

    - name: Cache dependencies
      uses: actions/cache@v1
      with:
        path: ~/.cache/pip
        key: ${{ runner.os }}-pip-${{ hashFiles('test_requirements.txt') }}
        restore-keys: |
          ${{ runner.os }}-pip-

    - name: Install dependencies
      run: python -m pip install -r test_requirements.txt

    - name: Install pytest-github-actions-annotate-failures
      run: python -m pip install pytest-github-actions-annotate-failures

    - name: Run pytest
      run: pytest -v

  release:
    name: Semantic Release
    runs-on: ubuntu-latest
    needs: [test]
    if: github.repository_owner == 'danth'

    steps:
      - name: Checkout repository
        uses: actions/checkout@v2
        with:
          fetch-depth: 0
          ref: ${{ needs.beautify.outputs.new_sha }}

      - name: Fetch master
        run: git fetch --prune origin +refs/heads/master:refs/remotes/origin/master

      - name: Semantic Release
        uses: relekang/python-semantic-release@v7.15.5
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}


================================================
FILE: .gitignore
================================================
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# PyInstaller
#  Usually these files are written by a python script from a template
#  before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
.pybuilder/
target/

# Jupyter Notebook
.ipynb_checkpoints

# IPython
profile_default/
ipython_config.py

# pyenv
#   For a library or package, you might want to ignore these files since the code is
#   intended to run in multiple environments; otherwise, check them in:
# .python-version

# pipenv
#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
#   However, in case of collaboration, if having platform-specific dependencies or dependencies
#   having no cross-platform support, pipenv may install dependencies that don't work, or not
#   install all needed dependencies.
#Pipfile.lock

# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/

# Celery stuff
celerybeat-schedule
celerybeat.pid

# SageMath parsed files
*.sage.py

# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Spyder project settings
.spyderproject
.spyproject

# Rope project settings
.ropeproject

# mkdocs documentation
/site

# mypy
.mypy_cache/
.dmypy.json
dmypy.json

# Pyre type checker
.pyre/

# pytype static type analyzer
.pytype/

# Cython debug symbols
cython_debug/


================================================
FILE: .gitmodules
================================================
[submodule "heapdict"]
	path = heapdict
	url = https://github.com/DanielStutzbach/heapdict.git


================================================
FILE: CHANGELOG.md
================================================
# Changelog

<!--next-version-placeholder-->

## v3.1.3 (2022-08-24)
| Type | Change |
| --- | --- |
| Fix | Don't error when `g:python3_host_prog` is unset ([`4c01c96`](https://github.com/danth/pathfinder.vim/commit/4c01c9635a54e5669e05c26b9c443c3f86de84d2)) |
| Documentation | Update contributing guidelines ([`c83f926`](https://github.com/danth/pathfinder.vim/commit/c83f9264f859367f2f7998e10907e57a99d414cb)) |


================================================
FILE: CONTRIBUTING.md
================================================
# Contributing to Pathfinder.vim

## Commit messages

**Please follow the
[Angular commit style](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#commits)
in your commit messages so that the automatic version numbering can work.**

## Development environment

The plugin files will be cloned somewhere under `~/.vim` by your plugin manager.
For example Plug places them in `~/.vim/plugged/pathfinder.vim/`.

Some plugin managers also allow you to clone the repository into a directory of
your choice and use the path to that directory to install the plugin.

## Pathfinding

The pathfinding algorithm used is [Dijkstra's algorithm][dijkstra], which is
just a greedy algorithm. Vertices are generated on-the-fly rather than building
the entire graph beforehand.

Each vertex in the graph is a tuple of `(view, most recent motion)`.  `view` is
the result of `winsaveview()` in Vim: this includes both the cursor and scroll
positions. We need to store the most recent motion because it is used to
calculate certain weights - `2fk` should be cheaper than `fkfy`.

[dijkstra]: https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm

## Python to Vim interface

Vim (when compiled with `+python3`) comes with an embedded Python module called
`vim`, which is used to send commands and output from Python.

In Vimscript, the following syntax is used to call Python code:

```vim
python3 <line of code>
```

```vim
python3 << endpython
<multiple lines of code>
endpython
```

## Client and Server

The way we discover connections between nodes (characters) in the pathfinding
graph is:

1. Move the cursor to the node position
2. Run the motion with `silent! normal! <motion>`
3. Check where the cursor moved to and record it as a connection

This causes the cursor to move around while things are being tested. We want to
allow users to continue working while paths are found, hence we start a "server"
which is basically another Vim process using [this barebones vimrc](serverrc.vim).

The user's Vim (the client) sends the contents of the current buffer, the start
and target positions, the window dimensions and some other information to the
server when it wants to request a path. This communication happens through
`multiprocessing.connection` with a temporary file.

The server will then do pathfinding in the background and send the result back
when it is done, which the client receives and displays.


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2020 Daniel Thwaites

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.


================================================
FILE: MANIFEST.in
================================================
include LICENSE README.md
include plugin/* colors/* ftdetect/* ftplugin/* indent/* compiler/* after/* autoload/* doc/*
include heapdict/LICENSE heapdict/heapdict.py


================================================
FILE: README.md
================================================
# pathfinder.vim

A Vim plugin to give suggestions to improve your movements.
It's a bit like [Clippy][office-assistant].

[![Demo](https://asciinema.org/a/CYX4I94GGBsHZqMVc9N8MerFD.svg)](https://asciinema.org/a/CYX4I94GGBsHZqMVc9N8MerFD)

[office-assistant]: https://en.wikipedia.org/wiki/Office_Assistant


## Features

- Find the shortest motion sequence or search query to move the cursor
- Help summaries to explain a suggestion
- Asynchronous - pathfinding runs in a separate process


## Installation

Use your favorite plugin manager. I recommend
[vim-plug](https://github.com/junegunn/vim-plug).

```vim
if has('python3') && has('timers')
  Plug 'danth/pathfinder.vim'
else
  echoerr 'pathfinder.vim is not supported on this Vim installation'
endif
```

You may also need to run `git submodule update --init` from inside the plugin
directory. Most popular plugin managers will do that automatically.


## Usage

1. Move the cursor in normal, visual or visual-line mode.
2. That's it.

Suggestions pop up above the cursor if you have:

- Vim 8.2 or above, with `+popupwin`
- Neovim 0.4 or above

Otherwise, they will appear as a plain `echo` at the bottom of the screen.

### Explanations

If you don't understand how a suggestion works, you should use the
`:PathfinderExplain` command, which will show a short description of each
motion used.

If you find yourself using this a lot, make a mapping for it!

```vim
noremap <leader>pe :PathfinderExplain<CR>
```

### Manual Commands

If you set [`g:pf_autorun_delay`](#gpf_autorun_delay) to a negative value,
you get two commands instead:

- `:PathfinderBegin`: Set the start position. This still happens automatically
  when switching windows/tabs, or loading a new file.
- `:PathfinderRun`: Set the target position and get a suggestion.


## Related Plugins

- [vim-be-good](https://github.com/ThePrimeagen/vim-be-good) - Various training games to practice certain actions
- [vim-hardtime](https://github.com/takac/vim-hardtime) - Prevent yourself from repeating keys like `h`,`j`,`k`,`l`


## Configuration

*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.*

### `highlight PathfinderPopup`
Change the appearance of suggestion popups. *Default: same as cursor*

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

### `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`
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
&times; multiplier) and added to both sides. *Default: 0.5*

This limitation improves performance by disallowing movements outside the area
of interest. It also prevents suggestions which rely on knowing about the exact
text hundreds of lines away. Settings below 1 cause movements within a line to
only use motions inside that line.

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`
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*

### `g:pf_descriptions`
Dictionary of descriptions, used for `:PathfinderExplain`.

```vim
let g:pf_descriptions['k'] = 'Up {count} lines'
let g:pf_descriptions['f'] = 'To occurence {count} of "{argument}", to the right'
```

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.


================================================
FILE: pathfinder/__init__.py
================================================


================================================
FILE: pathfinder/client/__init__.py
================================================


================================================
FILE: pathfinder/client/autorun.py
================================================
import time

import vim


def choose_action(start_state, current_state, update_time):
    """
    Select an action to take automatically.

    This is intended for use with StateTracker.choose_action_using().

    Returns one of:
    "reset" - Set start and target to the current state
    "set_target" - Set target to the current state
    "pathfind" - Start pathfinding, using the target from last time it was set
    None - Do nothing
    """
    if (
        start_state.window != current_state.window
        or start_state.buffer != current_state.buffer
    ):
        # Reset to ensure the start or target view isn't set to a location
        # which is now impossible to access
        return "reset"

    delay = vim.vars["pf_autorun_delay"]
    if delay > 0:  # If delay <= 0, then the user disabled autorun
        if start_state.mode not in {"n", "v", "V", ""}:
            # Motions are not used in this mode, so pathfinding is useless
            return "reset"

        if (
            time.time() >= update_time + delay
            or start_state.mode != current_state.mode
            or start_state.lines != current_state.lines
        ):
            return "pathfind"
        else:
            return "set_target"


================================================
FILE: pathfinder/client/client.py
================================================
import os
import subprocess
import tempfile
from multiprocessing import connection

import vim

from pathfinder.client.explore_lines import get_line_limits


class Client:
    """
    Starts and connects to a separate Vim instance used for testing motions.

    This is used to test motions in the exact same environment as the user (loaded via
    a temporary session file), without moving the user's cursor in the process. This
    allows pathfinding to happen in the background while the user may continue working.

    A custom vimrc is used to only load this plugin, disabling other plugins and user
    settings.
    """

    def __init__(self):
        self.open()

    def open(self):
        """Launch and connect to the server Vim."""
        # Create a file used to communicate with the server
        self.file_path = os.path.join(
            tempfile.gettempdir(), "pathfinder_vim_" + vim.eval("getpid()")
        )

        self.server_process = subprocess.Popen(
            self._build_server_cmd(), stdout=subprocess.PIPE, stderr=subprocess.PIPE
        )

        self.server_connection = None
        self.to_send = None

    def _build_server_cmd(self):
        """Build the command used to launch the server Vim."""
        progpath = vim.eval("v:progpath")

        options = [
            progpath,
            "--clean",
            "--cmd",
            f"let g:pf_server_communication_file='{self.file_path}'",
            "-u",
            os.path.normpath(
                # serverrc.vim in the root of this repository, instead of the user's
                # regular .vimrc or init.vim
                os.path.join(os.path.dirname(__file__), "..", "..", "serverrc.vim")
            ),
        ]

        if progpath.endswith("nvim"):
            options += ["--headless"]
            try:
                python3_host_prog = vim.eval("g:python3_host_prog")
                options += ["--cmd", f"let g:python3_host_prog='{python3_host_prog}'"]
            except:
                pass
        else:
            options += ["-v", "--not-a-term"]

        return options

    def close(self):
        """Shut down the server Vim."""
        if self.server_connection is not None:
            # Server will shut down Vim gracefully when we disconnect
            self.server_connection.close()
        elif self.server_process is not None:
            # Not connected yet, terminate the process instead
            self.server_process.terminate()

    def poll_responses(self):
        if not self.connect():
            return

        # Check if a request is waiting to be sent
        if self.to_send is not None:
            self.server_connection.send(self.to_send)
            self.to_send = None

        # Check if any data is available to be read
        elif self.server_connection.poll():
            # Get response (sent in a tuple of type, data)
            response_type, data = self.server_connection.recv()
            self.handle_response(response_type, data)

    def connect(self):
        """
        Attempt to connect to the server.

        :returns: whether a connection is ready.
        """
        if self.server_connection is not None:
            return True

        if self.server_process is None:
            # Server process has exited but we already raised an exception
            return False

        return_code = self.server_process.poll()
        if return_code is not None:
            # Server process has exited
            stdout, stderr = self.server_process.communicate()
            self.server_process = None
            raise Exception(
                f"Pathfinding server process exited with return code {return_code}:\n"
                + stderr.decode()
            )

        try:
            # Attempt to connect
            self.server_connection = connection.Client(self.file_path)
            return True
        except FileNotFoundError:
            return False

    def handle_response(self, response_type, data):
        """
        Process a response recieved from the server.

        This will be one of:
        - ``RESULT`` - A pathfinding result. Call the first queued callback.
        - ``ERROR`` - An unexpected exception was caught and the server has exited.
          Relay the traceback to the user for debugging.
        """
        if response_type == "RESULT":
            # Get the first callback function and pass the result to it
            self.callback(data)
            del self.callback
        elif response_type == "ERROR":
            raise Exception("Pathfinding server encountered an exception:\n" + data)
        else:
            raise Exception("Received an unexpected response " + response_type)

    def pathfind(self, buffer_contents, start_view, target_view, callback):
        """
        Request a pathfinding result from the server.

        :param buffer_contents: List of lines we are pathfinding inside.
        :param start_view: The start position, in the current window.
        :param target_view: The target position, in the current window.
        :param callback: Function to be called once a path is found. Recieves a list
            of motions as a parameter.
        """
        self.callback = callback

        min_line, max_line = get_line_limits(start_view, target_view)
        self.to_send = {
            "start": start_view,
            "target": target_view,
            "min_line": min_line,
            "max_line": max_line,
            "size": (
                # WindowTextWidth() - see plugin/dimensions.vim
                vim.eval("WindowTextWidth()"),
                vim.eval("winheight(0)"),
            ),
            "buffer": buffer_contents,
            "wrap": vim.current.window.options["wrap"],
            "scrolloff": vim.options["scrolloff"],
            "sidescrolloff": vim.options["sidescrolloff"],
        }


================================================
FILE: pathfinder/client/explore_lines.py
================================================
import vim


def get_explore_lines(search_area_lines):
    """
    :param search_area_lines: Number of lines between, and including, the start and
        target positions.
    :returns: Number of lines to explore either side of the search area.
    """
    # Get setting values from Vim variables
    explore_scale = float(vim.vars["pf_explore_scale"])
    max_explore = int(vim.vars["pf_max_explore"])

    if explore_scale < 0:
        # This filtering is disabled, explore the entire buffer
        return len(vim.current.buffer)

    # Number of lines to explore above and below the search area is scaled based
    # on the length of the area. This setting defaults to 0.5, if the search area
    # was e.g. 6 lines then 3 more lines would be explored on either side.
    explore_lines = search_area_lines * explore_scale
    if max_explore >= 0:
        # Limit to no more than max_explore lines
        return min(max_explore, explore_lines)
    else:
        # Do not limit
        return explore_lines


def get_line_limits(start_view, target_view):
    """
    Return the minimum and maximum line numbers to explore.

    :param start_view: The start position.
    :param target_view: The target position.
    :returns: Tuple of (min line, max line)
    """
    min_line = min(int(start_view.lnum), int(target_view.lnum))
    max_line = max(int(start_view.lnum), int(target_view.lnum))
    explore_lines = get_explore_lines(max_line - min_line)

    return (
        max(1, min_line - explore_lines),
        min(len(vim.current.buffer), max_line + explore_lines),
    )


================================================
FILE: pathfinder/client/output.py
================================================
import itertools

import vim

from pathfinder.debytes import debytes

last_output = None


def strtrans(string):
    """Convert special characters like '' to '^D'."""
    escaped_string = string.replace("'", "\\'").replace("\\", "\\\\")
    return vim.eval(f"strtrans('{escaped_string}')")


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

    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_str) * 2

    return str(count) + motion_str


def compact_motions(motions):
    """
    Return the given motion sequence in single-line form.

    e.g. 2* 5j $
    """
    return " ".join(
        [
            get_count(motion, len(list(group)))
            for motion, group in itertools.groupby(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']
    """
    for motion, group in itertools.groupby(motions):
        repetitions = len(list(group))
        yield (
            get_count(motion, repetitions) + "  " + get_description(motion, repetitions)
        )


================================================
FILE: pathfinder/client/plugin.py
================================================
import time

import vim

import pathfinder.client.output as output
from pathfinder.client.autorun import choose_action
from pathfinder.client.client import Client
from pathfinder.client.popup import open_popup
from pathfinder.client.state_tracker import StateTracker
from pathfinder.window import cursor_in_same_position


class Plugin:
    def __init__(self):
        self.client = Client()
        self.state_tracker = StateTracker()
        self.last_output = None

    def _run(self):
        """Start calculating a path in the background."""
        self.client.pathfind(
            self.state_tracker.start_state.lines,
            self.state_tracker.start_state.view,
            self.state_tracker.target_state.view,
            self.popup,
        )

    def popup(self, motions):
        self.last_output = motions
        open_popup(output.compact_motions(motions))

    def autorun(self):
        """Called on a timer several times per second."""
        if self.state_tracker.choose_action_using(choose_action) == "pathfind":
            if not cursor_in_same_position(
                self.state_tracker.start_state.view,
                self.state_tracker.target_state.view,
            ):
                self._run()
            self.state_tracker.reset()

    def command_begin(self):
        """Called for the :PathfinderBegin command."""
        self.state_tracker.reset()

    def command_run(self):
        """Called for the :PathfinderRun command."""
        self.state_tracker.set_target()

        if cursor_in_same_position(
            self.state_tracker.start_state.view,
            self.state_tracker.target_state.view,
        ):
            print("You must move the cursor to a new location first!")
        else:
            self._run()

    def command_explain(self):
        """Called for the :PathfinderExplain command."""
        if self.last_output is None:
            print("No suggestion to explain.")
        else:
            # explained_motions yields each line
            # sep tells print to put \n between them rather than space
            print(*output.explained_motions(self.last_output), sep="\n")

    def stop(self):
        """Called when Vim is about to shut down."""
        self.client.close()


================================================
FILE: pathfinder/client/popup.py
================================================
import vim

from pathfinder.debytes import debytes


def _neovim_popup(text, line_offset):
    """Create a popup using Neovim 0.4+ floating windows."""
    # Insert text into a scratch buffer
    buffer = vim.api.create_buf(False, True)
    vim.api.buf_set_lines(buffer, 0, -1, True, [f" {text} "])

    # Create a window containing the buffer
    window = vim.api.open_win(
        buffer,
        0,
        {
            "relative": "cursor",
            "row": int(line_offset),
            "col": 0,
            "style": "minimal",
            "focusable": 0,
            "height": 1,
            "width": len(text) + 2,
        },
    )
    # Set the highlight of the window to match the cursor
    vim.api.win_set_option(window, "winhl", "Normal:PathfinderPopup")

    # Create a timer to close the window
    popup_time = int(vim.vars["pf_popup_time"])
    vim.eval(f"timer_start({popup_time}, {{-> nvim_win_close({window.handle}, 1)}})")


def _vim_popup(text, line_offset):
    """Create a popup using Vim +popupwin."""
    vim.Function("popup_create")(
        text,
        {
            "line": f"cursor{line_offset}",
            "col": "cursor",
            "wrap": False,
            "padding": (0, 1, 0, 1),
            "highlight": "PathfinderPopup",
            "time": int(vim.vars["pf_popup_time"]),
            "zindex": 1000,
        },
    )


def open_popup(text):
    line_offset = "+1" if vim.eval("line('.')") == "1" else "-1"

    if vim.eval("has('nvim-0.4')") == "1":
        _neovim_popup(text, line_offset)
    elif vim.eval("has('popupwin')") == "1":
        _vim_popup(text, line_offset)
    else:
        # Not able to create a popup
        print(text)


================================================
FILE: pathfinder/client/state_tracker.py
================================================
import time
from collections import namedtuple

import vim

from pathfinder.window import winsaveview

State = namedtuple("State", "view mode buffer window lines")


class StateTracker:
    def __init__(self):
        self.reset()

    def _set_update_time(self):
        self.update_time = time.time()

    def _record_state(self):
        return State(
            winsaveview(),
            vim.eval("mode()"),
            vim.current.buffer.number,
            vim.current.window.number,
            vim.current.buffer[:],
        )

    def reset(self):
        current_state = self._record_state()
        self._reset(current_state)

    def _reset(self, new_state):
        self.start_state = new_state
        self.target_state = new_state
        self._set_update_time()

    def set_target(self):
        current_state = self._record_state()
        self._set_target(current_state)

    def _set_target(self, new_state):
        if new_state != self.target_state:
            self.target_state = new_state
            self._set_update_time()

    def choose_action_using(self, function):
        """
        Choose an action to take using the given function.

        Function will be called with the arguments (start state, current state,
        time of most recent update). It may return "reset" or "set_target" to
        call the corresponding method, or any other value to do nothing.  The
        function's return value is passed through this method, so can be used
        to take further actions elsewhere.
        """
        current_state = self._record_state()

        result = function(self.start_state, current_state, self.update_time)
        if result == "reset":
            self._reset(current_state)
        elif result == "set_target":
            self._set_target(current_state)

        return result


================================================
FILE: pathfinder/debytes.py
================================================
def debytes(string):
    """
    Decode string if it is a bytes object.

    This is necessary since Neovim, correctly, gives strings as a str, but regular
    Vim leaves them encoded as bytes.
    """
    try:
        return string.decode()
    except AttributeError:
        return string


================================================
FILE: pathfinder/server/__init__.py
================================================


================================================
FILE: pathfinder/server/dijkstra.py
================================================
from heapdict import heapdict

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


class Dijkstra:
    """
    A path between a start and end point in the same window.

    :param from_view: View of the start point
    :param target_view: View of the target point
    :param min_line: Do not explore nodes above this line
    :param max_line: Do not explore nodes below this line
    """

    def __init__(self, from_view, target_view, min_line, max_line):
        self.from_view = from_view
        self.target_view = target_view
        self.min_line = min_line
        self.max_line = max_line

        self.motion_generators = {
            SimpleMotionGenerator(self),
            FindMotionGenerator(self),
            SearchMotionGenerator(self),
        }

        self._open_queue = heapdict()  # Min-priority queue: Key -> Distance
        self._open_nodes = dict()  # Key -> Node
        self._closed_nodes = set()  # Key

        start_node = Node(self, self.from_view, None)
        self._open_queue[start_node.key] = 0
        self._open_nodes[start_node.key] = start_node

    def find_path(self, client_connection):
        """
        Use Dijkstra's algorithm to find the optimal sequence of motions.

        :param client_connection: If another pathfinding request is waiting on this
            connection, exit (returning None) as soon as possible. This cancels the
            pathfinding, moving on to the new request immediately.
        """
        while len(self._open_queue) > 0 and not client_connection.poll():
            current_node_key, current_distance = self._open_queue.popitem()
            current_node = self._open_nodes.pop(current_node_key)
            self._closed_nodes.add(current_node_key)

            if current_node.is_target():
                return current_node.reconstruct_path()

            for node in current_node.get_neighbours():
                if node.key in self._closed_nodes:
                    continue

                new_distance = current_distance + current_node.motion_weight(
                    node.came_by_motion
                )
                if (
                    node.key not in self._open_nodes
                    or new_distance < self._open_queue[node.key]
                ):
                    node.set_came_from(current_node)
                    self._open_nodes[node.key] = node
                    self._open_queue[node.key] = new_distance


================================================
FILE: pathfinder/server/motions/__init__.py
================================================
import abc
from collections import namedtuple

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)


================================================
FILE: pathfinder/server/motions/find.py
================================================
import vim

from pathfinder.server.motions import Motion, MotionGenerator


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))


================================================
FILE: pathfinder/server/motions/search.py
================================================
import re

import vim

from pathfinder.server.motions import Motion, MotionGenerator


class SearchMotionGenerator(MotionGenerator):
    def generate(self, view):
        # Only return results from the starting node
        if view != self.dijkstra.from_view:
            return

        motion = self._search_lines(
            vim.current.buffer[:],
            view.lnum - 1,
            view.col,
            self.dijkstra.target_view.lnum - 1,
            self.dijkstra.target_view.col,
        )
        if motion:
            yield self._create_node(self.dijkstra.target_view, motion)

    def _create_motion(self, search_query, motion="/"):
        return Motion(motion, self._escape_magic(search_query))

    def _escape_magic(self, search_query):
        """Add backslash escapes to any "magic" characters in a query."""
        for char in r"\^$.*[~/":
            search_query = search_query.replace(char, "\\" + char)
        return search_query

    def _search(self, text, start, target):
        """
        Return the simplest possible searching motion to reach the given target.

        :param text: Contents of the file.
        :param start: Index in ``text`` to start the search from.
        :param target: Index of the target position in ``text``.
        """
        search_text = text[target:]

        # ("a", "ab", "abc", "abcd"...) until we reach
        # the end of search_text or find a working query
        for query_length in range(1, len(search_text) + 1):
            query = search_text[:query_length]

            # Get a list of all match positions for this search query
            # query="x" text="x___x_xx" == [0, 4, 6, 7]
            pattern = re.escape(query)
            matches = [m.start() for m in re.finditer(pattern, text)]

            if matches:
                # Sort the list so it begins with matches after `start`, rather
                # than matches at the beginning of the file
                # sorted([True, False]) == [False, True]
                matches.sort(key=lambda position: position <= start)

                if matches[0] == target:
                    return self._create_motion(query)
                if matches[-1] == target:
                    return self._create_motion(query, "?")

    def _search_lines(self, lines, start_line, start_col, target_line, target_col):
        """
        Wrapper around _search which handles 2d coordinates and a list of lines.

        :param lines: List of lines.
        :param start_line: Starting line, indexed from 0.
        :param start_col: Starting column.
        :param target_line: Target line, indexed from 0.
        :param target_col: Target column.
        """
        text = "\n".join(lines)
        start = sum(len(line) + 1 for line in lines[:start_line]) + start_col
        target = sum(len(line) + 1 for line in lines[:target_line]) + target_col
        return self._search(text, start, target)


================================================
FILE: pathfinder/server/motions/simple.py
================================================
import vim

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


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()


================================================
FILE: pathfinder/server/node.py
================================================
from pathfinder.window import cursor_in_same_position


class Node:
    """Graph node linked to a view (cursor+scroll location) within the document."""

    def __init__(self, dijkstra, view, came_by_motion):
        self.dijkstra = dijkstra
        self.view = view
        self.came_by_motion = came_by_motion

        self.key = (view, came_by_motion)

        self.came_from = None
        self.came_by_motion_repetitions = 1

    def get_neighbours(self):
        """Yield all neighbours of this node."""
        for motion_generator in self.dijkstra.motion_generators:
            for node in motion_generator.generate(self.view):
                if (
                    node.view.lnum >= self.dijkstra.min_line
                    and node.view.lnum <= self.dijkstra.max_line
                ):
                    yield node

    def motion_weight(self, motion):
        """Return the weight of using a motion from this node."""
        if motion != self.came_by_motion:
            # First repetition, return number of characters in the motion
            return len(motion.motion) + (
                0 if motion.argument is None else len(motion.argument)
            )
        elif self.came_by_motion_repetitions == 1:
            # Second repetition, adding a "2" is 1 extra character
            return 1
        else:
            # Difference in length of current and future count
            # 2j -> 3j = 0
            # 9j -> 10j = 1
            return len(str(self.came_by_motion_repetitions + 1)) - len(
                str(self.came_by_motion_repetitions)
            )

    def set_came_from(self, node):
        """Set the node this node was reached from."""
        self.came_from = node

        if node.came_by_motion == self.came_by_motion:
            self.came_by_motion_repetitions = node.came_by_motion_repetitions + 1
        else:
            self.came_by_motion_repetitions = 1

    def is_target(self):
        return cursor_in_same_position(self.view, self.dijkstra.target_view)

    def reconstruct_path(self):
        """Return the sequence of motions used to reach this node."""
        motions = list()
        node = self
        while node.came_from is not None:
            motions.insert(0, node.came_by_motion)
            node = node.came_from
        return motions


================================================
FILE: pathfinder/server/server.py
================================================
import traceback
from multiprocessing import connection

import vim

from pathfinder.debytes import debytes
from pathfinder.server.dijkstra import Dijkstra


class Server:
    """
    Local server which runs inside an instance of Vim in a separate process.

    This is used to test motions in the same environment as the user, but without
    blocking their Vim or moving their cursor. This allows pathfinding to happen in
    the background while the user may continue working.

    The server's Vim is quitted as soon as the client (the user's Vim) disconnects,
    which happens when it is closed. It will also exit if an exception is caught,
    relaying the traceback message to the client beforehand so that it can be displayed
    for debugging.
    """

    def __init__(self, file_path):
        self.listener = connection.Listener(debytes(file_path))

    def run(self):
        try:
            # Wait for the user's Vim to connect
            self.client_connection = self.listener.accept()
            # Continuously process jobs until EOFError is raised
            # (when the client disconnects)
            self.message_loop()
        except EOFError:
            pass
        finally:
            self.listener.close()
            # Exit the background Vim since it is no longer needed
            vim.command("qa!")

    def message_loop(self):
        """
        Continuously wait for and handle instructions from the client.

        This waiting blocks Vim, but that does not matter since nobody is looking at
        it. Blocking also prevents CPU resources from being wasted on redrawing.

        :raises EOFError: when the connection is closed.
        """
        while True:
            try:
                data = self.client_connection.recv()

                # If there is still data waiting, then multiple requests were sent,
                # so we skip pathfinding and move on to the next one
                if not self.client_connection.poll():
                    self.do_action(data)
            except:
                # Send any unexpected exceptions back to the client
                # to be displayed for debugging purposes
                self.client_connection.send(("ERROR", traceback.format_exc()))

    def do_action(self, data):
        """Process an instruction from the client."""
        self.start_view = data["start"]
        self.target_view = data["target"]
        self.min_line = data["min_line"]
        self.max_line = data["max_line"]

        vim.current.buffer[:] = data["buffer"]

        vim.current.window.options["wrap"] = data["wrap"]
        vim.options["scrolloff"] = data["scrolloff"]
        vim.options["sidescrolloff"] = data["sidescrolloff"]

        # Set size of the entire Vim display to match the size of the
        # corresponding window in the client
        vim.options["columns"] = int(data["size"][0])
        vim.options["lines"] = vim.options["cmdheight"] + int(data["size"][1])

        self.pathfind()

    def pathfind(self):
        """Run the pathfinder, then send the result back to the client."""
        dijkstra = Dijkstra(
            self.start_view, self.target_view, self.min_line, self.max_line
        )
        motions = dijkstra.find_path(self.client_connection)

        # If motions is None, that means we cancelled pathfinding because a new
        # request was received. We also check for another request now in case one was
        # sent during the last iteration of the pathfinding loop.
        if not (motions is None or self.client_connection.poll()):
            self.client_connection.send(("RESULT", motions))


server = Server(vim.vars["pf_server_communication_file"])
server.run()


================================================
FILE: pathfinder/window.py
================================================
from collections import namedtuple

import vim

View = namedtuple("View", "lnum col curswant leftcol topline")


def winsaveview():
    view_dict = vim.eval("winsaveview()")
    # Any dictionary elements not in View._fields will be discarded
    return View._make(int(view_dict[field]) for field in View._fields)


def winrestview(view):
    view_dict = dict(view._asdict())
    vim.eval(f"winrestview({view_dict})")


def cursor_in_same_position(a, b):
    """
    Check if the given views have the cursor on the same position.

    The scroll position and other properties may differ.
    """
    return a.lnum == b.lnum and a.col == b.col


================================================
FILE: plugin/defaults.vim
================================================
highlight default link PathfinderPopup Cursor

if !exists('g:pf_autorun_delay')
  let g:pf_autorun_delay = 2
endif

if !exists('g:pf_popup_time')
  let g:pf_popup_time = 3000
endif

if !exists('g:pf_explore_scale')
  let g:pf_explore_scale = 0.5
endif
if !exists('g:pf_max_explore')
  let g:pf_max_explore = 10
endif

if !exists('g:pf_descriptions')
  let g:pf_descriptions = {
    \ 'h': 'Left {count} columns',
    \ 'l': 'Right {count} columns',
    \ 'j': 'Down {count} lines',
    \ 'k': 'Up {count} lines',
    \ 'gj': 'Down {count} display lines',
    \ 'gk': 'Up {count} display lines',
    \ 'gg': 'To the start of the buffer',
    \ 'G': 'To the end of the buffer',
    \ 'H': 'To line {count} from the top of the window',
    \ 'M': 'To the middle of the window',
    \ 'L': 'To line {count} from the bottom of the window',
    \ '': 'Scroll downward {count} lines',
    \ '': 'Scroll upward {count} lines',
    \ '': 'Scroll forward {count} pages',
    \ '': 'Scroll backward {count} pages',
    \ '': 'Scroll downward {count} times',
    \ '': 'Scroll upward {count} times',
    \ 'zt': 'Current line to the top of the window',
    \ 'z\
': 'Current line to the top of the window and move to the first non-blank character',
    \ 'zz': 'Current line to the centre of the window',
    \ 'z.': 'Current line to the centre of the window and move to the first non-blank character',
    \ 'zb': 'Current line to the bottom of the window',
    \ 'z-': 'Current line to the bottom of the window and move to the first non-blank character',
    \ '0': 'To the start of the line',
    \ '^': 'To the first non-blank character on the line',
    \ 'g^': 'To the first non-blank character on the display line',
    \ '$': 'To the end of the line',
    \ 'g$': 'To the end of the display line',
    \ 'g_': 'To the last non-blank character on the line',
    \ 'gm': 'To the centre of the screen',
    \ 'gM': 'To the centre of the line',
    \ 'W': '{count} WORDs forward (inclusive)',
    \ 'E': 'Forward to the end of WORD {count} (exclusive)',
    \ 'B': '{count} WORDs backward (inclusive)',
    \ 'gE': 'Backward to the end of WORD {count} (exclusive)',
    \ 'w': '{count} words forward (inclusive)',
    \ 'e': 'Forward to the end of word {count} (exclusive)',
    \ 'b': '{count} words backward (inclusive)',
    \ 'ge': 'Backward to the end of word {count} (exclusive)',
    \ '(': '{count} sentences backward',
    \ ')': '{count} sentences forward',
    \ '{': '{count} paragraphs backward',
    \ '}': '{count} paragraphs forward',
    \ ']]': '{count} sections forward or to the next { in the first column',
    \ '][': '{count} sections forward or to the next } in the first column',
    \ '[[': '{count} sections backward or to the previous { in the first column',
    \ '[]': '{count} sections backward or to the previous } in the first column',
    \ ']m': '{count} next start of a method (Java or similar)',
    \ '[m': '{count} previous start of a method (Java or similar)',
    \ ']M': '{count} next end of a method (Java or similar)',
    \ '[M': '{count} previous end of a method (Java or similar)',
    \ '*': 'Search forward for occurrence {count} of the word nearest to the cursor',
    \ '#': 'Search backward for occurrence {count} of the word nearest to the cursor',
    \ 'g*': 'Search forward for occurrence {count} of the word nearest to the cursor, allowing matches which are not a whole word',
    \ 'g#': 'Search backward for occurrence {count} of the word nearest to the cursor, allowing matches which are not a whole word',
    \ '%': 'Go to the matching bracket',
    \ 'f': 'To occurrence {count} of "{argument}", to the right',
    \ 't': 'Till before occurrence {count} of "{argument}", to the right',
    \ 'F': 'To occurrence {count} of "{argument}", to the left',
    \ 'T': 'Till after occurrence {count} of "{argument}", to the left',
    \ '/': 'Search forwards for occurence {count} of "{argument}"',
    \ '?': 'Search backwards for occurence {count} of "{argument}"',
    \ }
endif


================================================
FILE: plugin/dimensions.vim
================================================
" Get the width of the current window, excluding sign and number columns
function! WindowTextWidth()
  let l:decorationColumns = &number ? &numberwidth : 0

  if has('folding')
    let l:decorationColumns += &foldcolumn
  endif

  if has('signs')
    if &signcolumn ==? 'yes' || &signcolumn ==? 'number'
      " Sign column is always enabled
      let l:decorationColumns += 2
    elseif &signcolumn ==? 'auto'
      redir => l:signs
      silent execute 'sign place buffer=' . bufnr('')
      redir END

      " The output of the command above contains two header lines
      " Any more and that means signs are displayed
      if len(split(l:signs, "\n")) > 2
	  let l:decorationColumns += 2
      endif
    endif
  endif

  return winwidth(0) - l:decorationColumns
endfunction


================================================
FILE: plugin/main.vim
================================================
if exists('g:pf_loaded') | finish | endif
if !has('python3')
  echom 'The +python3 feature is required to run pathfinder.vim'
  finish
endif
if !has('timers')
  echom 'The +timers feature is required to run pathfinder.vim'
  finish
endif


python3 << endpython
import vim
import sys
from os.path import normpath, join

plugin_root = vim.eval("fnamemodify(resolve(expand('<sfile>:p')), ':h')")
sys.path.insert(0, normpath(join(plugin_root, '..')))
sys.path.insert(0, normpath(join(plugin_root, '..', 'heapdict')))
endpython


if exists('g:pf_server_communication_file')
  " Importing this will run the server and connect back to the client
  python3 import pathfinder.server.server
else
  python3 from pathfinder.client.plugin import Plugin; plugin = Plugin()

  command! PathfinderBegin python3 plugin.command_begin()
  command! PathfinderRun python3 plugin.command_run()
  command! PathfinderExplain python3 plugin.command_explain()

  function! PathfinderLoop(timer)
    " Check for responses from the server
    python3 plugin.client.poll_responses()
    " Check if we should take any actions automatically
    python3 plugin.autorun()
  endfunction

  if !exists("s:timer")
    let s:timer = timer_start(100, 'PathfinderLoop', {'repeat': -1})
  endif
  augroup PathfinderStopOnLeave
    autocmd!
    autocmd VimLeave * call timer_stop(s:timer)
    autocmd VimLeave * python3 plugin.stop()
  augroup END
endif


let g:pf_loaded = 1


================================================
FILE: serverrc.vim
================================================
set nocompatible

" Disable unnecessary features
set noruler
set nonumber
set noshowcmd
set signcolumn=no
filetype off
syntax off

" Add pathfinder.vim to runtimepath
let &runtimepath .= ',' . escape(expand('<sfile>:p:h'), '\,')


================================================
FILE: setup.cfg
================================================
[semantic_release]
version_variable=setup.py:__version__
changelog_components=semantic_release.changelog.changelog_table
commit_subject=release: v{version}
upload_to_pypi=false
build_command=python setup.py sdist --formats=gztar,zip

[isort]
default_section=THIRDPARTY
known_third_party=vim
known_first_party=pathfinder
order_by_type=True
combine_star=True
reverse_relative=True
multi_line_output=3
include_trailing_comma=True
force_grid_wrap=0
use_parentheses=True
line_length=88


================================================
FILE: setup.py
================================================
from setuptools import find_packages, setup

__version__ = "3.1.3"


setup(
    name="pathfinder.vim",
    version=__version__,
    author="Daniel Thwaites",
    author_email="danthwaites30@btinternet.com",
    url="https://github.com/danth/pathfinder.vim",
    packages=find_packages(exclude=("tests",)),
)


================================================
FILE: test_requirements.txt
================================================
pytest >=5,<6
pytest-stub >=1,<2


================================================
FILE: tests/__init__.py
================================================


================================================
FILE: tests/conftest.py
================================================
from pytest_stub.toolbox import stub_global

stub_global(
    {
        "vim": "[mock_persist]",
    }
)


================================================
FILE: tests/test_autorun.py
================================================
import time
from unittest import mock

import pytest

from pathfinder.client.autorun import choose_action
from pathfinder.client.state_tracker import State
from pathfinder.window import View


@pytest.fixture
def default_state():
    return State(View(0, 0, 0, 0, 0), "n", 1, 1, ["hello world"])


def test_choose_action_new_window(default_state):
    assert (
        choose_action(
            default_state._replace(window=1),
            default_state._replace(window=2),
            time.time(),
        )
        == "reset"
    )


@mock.patch("pathfinder.client.autorun.vim.vars", {"pf_autorun_delay": 1})
def test_choose_action_non_motion_mode(default_state):
    assert (
        choose_action(
            default_state._replace(mode="i"),
            default_state._replace(mode="i"),
            time.time(),
        )
        == "reset"
    )


@mock.patch("pathfinder.client.autorun.vim.vars", {"pf_autorun_delay": 1})
def test_choose_action_changed_mode(default_state):
    assert (
        choose_action(
            default_state._replace(mode="n"),
            default_state._replace(mode="i"),
            time.time(),
        )
        == "pathfind"
    )


@mock.patch("pathfinder.client.autorun.vim.vars", {"pf_autorun_delay": 1})
def test_choose_action_changed_lines(default_state):
    assert (
        choose_action(
            default_state._replace(lines=["foo"]),
            default_state._replace(lines=["bar"]),
            time.time(),
        )
        == "pathfind"
    )


@mock.patch("pathfinder.client.autorun.vim.vars", {"pf_autorun_delay": 1})
def test_choose_action_cursor_idle(default_state):
    assert (
        choose_action(
            default_state,
            default_state,
            time.time() - 2,
        )
        == "pathfind"
    )


@mock.patch("pathfinder.client.autorun.vim.vars", {"pf_autorun_delay": 1})
def test_choose_action_set_target(default_state):
    assert (
        choose_action(
            default_state,
            default_state,
            time.time(),
        )
        == "set_target"
    )


@mock.patch("pathfinder.client.autorun.vim.vars", {"pf_autorun_delay": 0})
def test_choose_action_do_nothing(default_state):
    assert (
        choose_action(
            default_state,
            default_state,
            time.time(),
        )
        is None
    )


================================================
FILE: tests/test_debytes.py
================================================
from pathfinder.debytes import debytes


def test_debytes_bytes():
    assert debytes(b"hello world") == "hello world"


def test_debytes_str():
    assert debytes("hello world") == "hello world"


================================================
FILE: tests/test_explore_lines.py
================================================
from collections import namedtuple
from unittest import mock

from pathfinder.client.explore_lines import get_explore_lines, get_line_limits


def test_get_explore_lines():
    with mock.patch(
        "pathfinder.client.explore_lines.vim.vars",
        {"pf_explore_scale": 0.5, "pf_max_explore": 10},
    ):
        assert get_explore_lines(10) == 5


def test_get_explore_lines_max_0():
    with mock.patch(
        "pathfinder.client.explore_lines.vim.vars",
        {"pf_explore_scale": 0.5, "pf_max_explore": 0},
    ):
        assert get_explore_lines(10) == 0


def test_get_explore_lines_scale_0():
    with mock.patch(
        "pathfinder.client.explore_lines.vim.vars",
        {"pf_explore_scale": 0, "pf_max_explore": 10},
    ):
        assert get_explore_lines(10) == 0


@mock.patch("pathfinder.client.explore_lines.get_explore_lines", return_value=0)
def test_get_line_limits(mock_get_explore_lines):
    View = namedtuple("View", "lnum")
    with mock.patch(
        "pathfinder.client.explore_lines.vim.current.buffer", ["line"] * 10
    ):
        assert get_line_limits(View(2), View(8)) == (2, 8)
        assert mock_get_explore_lines.called_once_with(6)


@mock.patch("pathfinder.client.explore_lines.get_explore_lines", return_value=2)
def test_get_line_limits_with_explore(mock_get_explore_lines):
    View = namedtuple("View", "lnum")
    with mock.patch(
        "pathfinder.client.explore_lines.vim.current.buffer", ["line"] * 10
    ):
        assert get_line_limits(View(4), View(6)) == (2, 8)
        assert mock_get_explore_lines.called_once_with(4)


================================================
FILE: tests/test_motions_find.py
================================================
from collections import namedtuple
from unittest import mock

from pathfinder.server.motions import Motion
from pathfinder.server.motions.find import FindMotionGenerator

View = namedtuple("View", "lnum col curswant")


@mock.patch("pathfinder.server.motions.find.vim.current.buffer", ["abcdde"])
@mock.patch.object(FindMotionGenerator, "_create_node", new=lambda self, v, m: (v, m))
def test_find_f():
    generator = FindMotionGenerator(None)
    output = list(generator._find(View(1, 0, 0), "f"))
    assert output == [
        (View(1, 1, 1), Motion("f", "b")),
        (View(1, 2, 2), Motion("f", "c")),
        (View(1, 3, 3), Motion("f", "d")),
        (View(1, 5, 5), Motion("f", "e")),
    ]


@mock.patch("pathfinder.server.motions.find.vim.current.buffer", ["abcdde"])
def test_find_f_final_column():
    generator = FindMotionGenerator(None)
    output = list(generator._find(View(1, 5, 5), "f"))
    assert len(output) == 0


@mock.patch("pathfinder.server.motions.find.vim.current.buffer", ["abcdde"])
@mock.patch.object(FindMotionGenerator, "_create_node", new=lambda self, v, m: (v, m))
def test_find_t():
    generator = FindMotionGenerator(None)
    output = list(generator._find(View(1, 0, 0), "t"))
    assert output == [
        (View(1, 1, 1), Motion("t", "c")),
        (View(1, 2, 2), Motion("t", "d")),
        (View(1, 4, 4), Motion("t", "e")),
    ]


@mock.patch("pathfinder.server.motions.find.vim.current.buffer", ["abcdde"])
def test_find_t_penultimate_column():
    generator = FindMotionGenerator(None)
    output = list(generator._find(View(1, 4, 4), "t"))
    assert len(output) == 0


@mock.patch("pathfinder.server.motions.find.vim.current.buffer", ["abcdde"])
@mock.patch.object(FindMotionGenerator, "_create_node", new=lambda self, v, m: (v, m))
def test_find_F():
    generator = FindMotionGenerator(None)
    output = list(generator._find(View(1, 5, 5), "F"))
    assert output == [
        (View(1, 4, 4), Motion("F", "d")),
        (View(1, 2, 2), Motion("F", "c")),
        (View(1, 1, 1), Motion("F", "b")),
        (View(1, 0, 0), Motion("F", "a")),
    ]


@mock.patch("pathfinder.server.motions.find.vim.current.buffer", ["abcdde"])
def test_find_F_first_column():
    generator = FindMotionGenerator(None)
    output = list(generator._find(View(1, 0, 0), "F"))
    assert len(output) == 0


@mock.patch("pathfinder.server.motions.find.vim.current.buffer", ["abcdde"])
@mock.patch.object(FindMotionGenerator, "_create_node", new=lambda self, v, m: (v, m))
def test_find_T():
    generator = FindMotionGenerator(None)
    output = list(generator._find(View(1, 5, 5), "T"))
    assert output == [
        (View(1, 3, 3), Motion("T", "c")),
        (View(1, 2, 2), Motion("T", "b")),
        (View(1, 1, 1), Motion("T", "a")),
    ]


@mock.patch("pathfinder.server.motions.find.vim.current.buffer", ["abcdde"])
def test_find_T_second_column():
    generator = FindMotionGenerator(None)
    output = list(generator._find(View(1, 1, 1), "T"))
    assert len(output) == 0


================================================
FILE: tests/test_motions_search.py
================================================
from unittest import mock

from pathfinder.server.motions import Motion
from pathfinder.server.motions.search import SearchMotionGenerator


def test_escape_magic():
    assert (
        SearchMotionGenerator(None)._escape_magic(r"x\x^x$x.x*x[x~x/x")
        == r"x\\x\^x\$x\.x\*x\[x\~x\/x"
    )


def test_search_finds_shortest_possible_query():
    generator = SearchMotionGenerator(None)
    assert generator._search("abcde", 0, 2) == Motion("/", "c")
    assert generator._search("abcbcdebbc", 0, 3) == Motion("/", "bcd")


def test_search_when_target_is_before_start():
    generator = SearchMotionGenerator(None)
    assert generator._search("abcde", 2, 0) == Motion("/", "a")
    assert generator._search("bcdabc", 3, 0) == Motion("?", "b")


@mock.patch.object(SearchMotionGenerator, "_search")
def test_search_lines_calls_search_correctly(mock_search):
    SearchMotionGenerator(None)._search_lines(["a", "bc", "d"], 0, 0, 1, 1)
    mock_search.assert_called_once_with("a\nbc\nd", 0, 3)


================================================
FILE: tests/test_motions_simple.py
================================================
from unittest import mock

from pathfinder.server.motions import Motion
from pathfinder.server.motions.simple import SimpleMotionGenerator

INPUT_VIEW = mock.sentinel.input_view
OUTPUT_VIEW = mock.sentinel.output_view


@mock.patch("pathfinder.server.motions.simple.vim.command")
@mock.patch("pathfinder.server.motions.simple.winsaveview", return_value=OUTPUT_VIEW)
@mock.patch("pathfinder.server.motions.simple.winrestview")
def test_try_motion(winrestview, winsaveview, command):
    generator = SimpleMotionGenerator(None)
    assert generator._try_motion(INPUT_VIEW, "j") == OUTPUT_VIEW
    winrestview.assert_called_once_with(INPUT_VIEW)
    command.assert_called_once_with("silent! normal! j")
    winsaveview.assert_called_once()


@mock.patch("pathfinder.server.motions.simple.vim.command", side_effect=Exception)
@mock.patch("pathfinder.server.motions.simple.winsaveview", return_value=OUTPUT_VIEW)
@mock.patch("pathfinder.server.motions.simple.winrestview")
def test_try_motion_when_motion_raises_exception(winrestview, winsaveview, command):
    generator = SimpleMotionGenerator(None)
    assert generator._try_motion(INPUT_VIEW, "j") is None
    winrestview.assert_called_once_with(INPUT_VIEW)
    command.assert_called_once_with("silent! normal! j")
    winsaveview.assert_not_called()


@mock.patch.object(SimpleMotionGenerator, "_create_node")
@mock.patch.object(SimpleMotionGenerator, "_try_motion", return_value=OUTPUT_VIEW)
def test_generate(try_motion, create_node):
    generator = SimpleMotionGenerator(None)
    output = list(generator.generate(INPUT_VIEW))
    assert len(output) == len(SimpleMotionGenerator.MOTIONS)
    for motion in SimpleMotionGenerator.MOTIONS:
        try_motion.assert_any_call(INPUT_VIEW, motion)
        create_node.assert_any_call(OUTPUT_VIEW, Motion(motion, None))


@mock.patch.object(SimpleMotionGenerator, "_create_node")
@mock.patch.object(SimpleMotionGenerator, "_try_motion", return_value=None)
def test_generate_when_try_motion_returns_none(try_motion, create_node):
    generator = SimpleMotionGenerator(None)
    output = list(generator.generate(INPUT_VIEW))
    assert len(output) == 0
    create_node.assert_not_called()
    for motion in SimpleMotionGenerator.MOTIONS:
        try_motion.assert_any_call(INPUT_VIEW, motion)


@mock.patch.object(SimpleMotionGenerator, "_create_node")
@mock.patch.object(SimpleMotionGenerator, "_try_motion", return_value=INPUT_VIEW)
def test_generate_when_try_motion_returns_same_as_input(try_motion, create_node):
    generator = SimpleMotionGenerator(None)
    output = list(generator.generate(INPUT_VIEW))
    assert len(output) == 0
    create_node.assert_not_called()
    for motion in SimpleMotionGenerator.MOTIONS:
        try_motion.assert_any_call(INPUT_VIEW, motion)


================================================
FILE: tests/test_node.py
================================================
from unittest import mock

import pytest

from pathfinder.server.motions import Motion
from pathfinder.server.node import Node


def test_reconstruct_path():
    node1 = Node(None, None, None)
    node2 = Node(None, None, mock.sentinel.motion_1)
    node2.set_came_from(node1)
    node3 = Node(None, None, mock.sentinel.motion_2)
    node3.set_came_from(node2)
    node4 = Node(None, None, mock.sentinel.motion_3)
    node4.set_came_from(node3)

    output = node4.reconstruct_path()
    assert output == [
        mock.sentinel.motion_1,
        mock.sentinel.motion_2,
        mock.sentinel.motion_3,
    ]


def test_set_came_from_same_motion():
    node1 = Node(None, None, mock.sentinel.motion_1)
    node2 = Node(None, None, mock.sentinel.motion_1)
    node2.set_came_from(node1)
    assert node2.came_from == node1
    assert node2.came_by_motion_repetitions == 2


def test_set_came_from_different_motion():
    node1 = Node(None, None, mock.sentinel.motion_1)
    node2 = Node(None, None, mock.sentinel.motion_2)
    node2.set_came_from(node1)
    assert node2.came_from == node1
    assert node2.came_by_motion_repetitions == 1


@pytest.mark.parametrize(
    "repetitions,expected",
    [(1, 1), (2, 0), (9, 1), (10, 0), (99, 1)],
    ids=[1, 2, 9, 10, 99],
)
def test_motion_weight_same_motion(repetitions, expected):
    node = Node(None, None, mock.sentinel.motion)
    node.came_by_motion_repetitions = repetitions
    assert node.motion_weight(mock.sentinel.motion) == expected


@pytest.mark.parametrize(
    "motion,expected",
    [(Motion("b", None), 1), (Motion("cd", None), 2), (Motion("e", "fg"), 3)],
    ids=["b", "cd", "e/fg"],
)
def test_motion_weight_different_motion(motion, expected):
    node = Node(None, None, Motion("a", None))
    assert node.motion_weight(motion) == expected


================================================
FILE: tests/test_output.py
================================================
from unittest import mock

import pytest

from pathfinder.client.output import compact_motions, explained_motions, get_count
from pathfinder.server.motions import Motion

COUNTS = [
    ("j", 1, "j"),
    ("j", 2, "jj"),
    ("long", 2, "2long"),
    ("j", 3, "3j"),
    ("j", 10, "10j"),
]


@mock.patch("pathfinder.client.output.strtrans", lambda x: x)
@pytest.mark.parametrize("motion,count,expected", COUNTS, ids=[x[2] for x in COUNTS])
def test_get_count(motion, count, expected):
    motion = Motion(motion, None)
    assert get_count(motion, count) == expected


@mock.patch("pathfinder.client.output.strtrans", lambda x: x)
def test_compact_motions():
    motion1 = Motion("j", None)
    motion2 = Motion("k", None)
    assert compact_motions([motion1, motion2, motion2, motion2]) == "j 3k"


@mock.patch("pathfinder.client.output.strtrans", lambda x: x)
def test_explained_motions():
    motion1 = Motion("j", None)
    motion2 = Motion("f", "g")
    with mock.patch(
        "pathfinder.client.output.vim.vars",
        {
            "pf_descriptions": {
                "j": "Down {count} lines",
                "f": "To occurence {count} of {argument}",
            }
        },
    ):
        assert list(explained_motions([motion1, motion2, motion2])) == [
            "j  Down 1 lines",
            "2fg  To occurence 2 of g",
        ]


================================================
FILE: tests/test_popup.py
================================================
from unittest import mock

from pathfinder.client.popup import _neovim_popup, _vim_popup, open_popup


@mock.patch("pathfinder.client.popup._vim_popup")
@mock.patch("pathfinder.client.popup._neovim_popup")
@mock.patch(
    "pathfinder.client.popup.vim.eval",
    side_effect=lambda x: {
        "line('.')": "1",
        "has('nvim-0.4')": "1",
        "has('popupwin')": "0",
    }[x],
)
def test_open_popup_neovim(vim_eval, neovim_popup, vim_popup):
    open_popup(mock.sentinel.text)
    vim_eval.assert_any_call("has('nvim-0.4')")
    neovim_popup.assert_called_once_with(mock.sentinel.text, "+1")
    vim_popup.assert_not_called()


@mock.patch("pathfinder.client.popup._vim_popup")
@mock.patch("pathfinder.client.popup._neovim_popup")
@mock.patch(
    "pathfinder.client.popup.vim.eval",
    side_effect=lambda x: {
        "line('.')": "2",
        "has('nvim-0.4')": "0",
        "has('popupwin')": "1",
    }[x],
)
def test_open_popup_vim(vim_eval, neovim_popup, vim_popup):
    open_popup(mock.sentinel.text)
    vim_eval.assert_any_call("has('popupwin')")
    neovim_popup.assert_not_called()
    vim_popup.assert_called_once_with(mock.sentinel.text, "-1")


@mock.patch("pathfinder.client.popup._vim_popup")
@mock.patch("pathfinder.client.popup._neovim_popup")
@mock.patch(
    "pathfinder.client.popup.vim.eval",
    side_effect=lambda x: {
        "line('.')": "3",
        "has('nvim-0.4')": "0",
        "has('popupwin')": "0",
    }[x],
)
def test_open_popup_echo(vim_eval, neovim_popup, vim_popup):
    open_popup(mock.sentinel.text)
    neovim_popup.assert_not_called()
    vim_popup.assert_not_called()


@mock.patch("pathfinder.client.popup.vim.vars", {"pf_popup_time": b"2000"})
def test_vim_popup():
    popup_create = mock.MagicMock(name='vim.Function("popup_create")')

    with mock.patch("pathfinder.client.popup.vim.Function", return_value=popup_create):
        _vim_popup("hello world", "+1")

    popup_create.assert_called_once_with(
        "hello world",
        {
            "line": "cursor+1",
            "col": "cursor",
            "wrap": False,
            "padding": (0, 1, 0, 1),
            "highlight": "PathfinderPopup",
            "time": 2000,  # Mocked in decorator
            "zindex": 1000,
        },
    )


@mock.patch("pathfinder.client.popup.vim.vars", {"pf_popup_time": b"2000"})
@mock.patch("pathfinder.client.popup.vim.eval")
@mock.patch("pathfinder.client.popup.vim.api.win_set_option")
@mock.patch(
    "pathfinder.client.popup.vim.api.open_win", return_value=mock.sentinel.window
)
@mock.patch("pathfinder.client.popup.vim.api.buf_set_lines")
@mock.patch(
    "pathfinder.client.popup.vim.api.create_buf", return_value=mock.sentinel.buffer
)
def test_neovim_popup(create_buf, buf_set_lines, open_win, win_set_option, vim_eval):
    type(open_win.return_value).handle = mock.PropertyMock(return_value="window handle")

    _neovim_popup("hello world", "-1")

    create_buf.assert_called_once_with(False, True)
    buf_set_lines.assert_called_once_with(
        mock.sentinel.buffer, 0, -1, True, [" hello world "]
    )
    open_win.assert_called_once_with(
        mock.sentinel.buffer,
        0,
        {
            "relative": "cursor",
            "row": -1,
            "col": 0,
            "style": "minimal",
            "focusable": 0,
            "height": 1,
            "width": 13,
        },
    )
    win_set_option.assert_called_once_with(
        mock.sentinel.window, "winhl", "Normal:PathfinderPopup"
    )
    vim_eval.assert_called_once_with(
        "timer_start(2000, {-> nvim_win_close(window handle, 1)})"
    )


================================================
FILE: tests/test_state_tracker.py
================================================
from unittest import mock

import pytest

from pathfinder.client.state_tracker import StateTracker


@pytest.fixture
def tracker():
    with mock.patch.object(
        StateTracker,
        "_record_state",
        return_value=mock.sentinel.initial_state,
    ):
        return StateTracker()


def test_reset(tracker):
    initial_update_time = tracker.update_time

    with mock.patch.object(
        tracker,
        "_record_state",
        return_value=mock.sentinel.new_state,
    ):
        tracker.reset()

    assert tracker.start_state == mock.sentinel.new_state
    assert tracker.target_state == mock.sentinel.new_state
    assert tracker.update_time > initial_update_time


def test_set_new_target(tracker):
    initial_update_time = tracker.update_time

    with mock.patch.object(
        tracker,
        "_record_state",
        return_value=mock.sentinel.new_state,
    ):
        tracker.set_target()

    assert tracker.start_state == mock.sentinel.initial_state
    assert tracker.target_state == mock.sentinel.new_state
    assert tracker.update_time > initial_update_time


def test_set_same_target(tracker):
    initial_update_time = tracker.update_time

    with mock.patch.object(
        tracker,
        "_record_state",
        return_value=mock.sentinel.initial_state,
    ):
        tracker.set_target()

    assert tracker.start_state == mock.sentinel.initial_state
    assert tracker.target_state == mock.sentinel.initial_state
    assert tracker.update_time == initial_update_time


def test_choose_action_reset(tracker):
    update_time = tracker.update_time

    with mock.patch.object(tracker, "_reset") as reset:
        with mock.patch.object(tracker, "_set_target") as set_target:
            with mock.patch.object(
                tracker,
                "_record_state",
                return_value=mock.sentinel.new_state,
            ):
                chooser = mock.MagicMock(name="chooser", return_value="reset")
                assert tracker.choose_action_using(chooser) == "reset"

            chooser.assert_called_once_with(
                mock.sentinel.initial_state, mock.sentinel.new_state, update_time
            )
            reset.assert_called_once()
            set_target.assert_not_called()


def test_choose_action_set_target(tracker):
    update_time = tracker.update_time

    with mock.patch.object(tracker, "_reset") as reset:
        with mock.patch.object(tracker, "_set_target") as set_target:
            with mock.patch.object(
                tracker,
                "_record_state",
                return_value=mock.sentinel.new_state,
            ):
                chooser = mock.MagicMock(name="chooser", return_value="set_target")
                assert tracker.choose_action_using(chooser) == "set_target"

            chooser.assert_called_once_with(
                mock.sentinel.initial_state, mock.sentinel.new_state, update_time
            )
            set_target.assert_called_once()
            reset.assert_not_called()


def test_choose_action_other(tracker):
    update_time = tracker.update_time

    with mock.patch.object(tracker, "_reset") as reset:
        with mock.patch.object(tracker, "_set_target") as set_target:
            with mock.patch.object(
                tracker,
                "_record_state",
                return_value=mock.sentinel.new_state,
            ):
                chooser = mock.MagicMock(name="chooser", return_value="other")
                assert tracker.choose_action_using(chooser) == "other"

            chooser.assert_called_once_with(
                mock.sentinel.initial_state, mock.sentinel.new_state, update_time
            )
            reset.assert_not_called()
            set_target.assert_not_called()


================================================
FILE: tests/test_window.py
================================================
from unittest import mock

import pytest

from pathfinder.window import View, cursor_in_same_position, winrestview, winsaveview

TEST_DICT = {"lnum": 0, "col": 10, "curswant": 10, "leftcol": 5, "topline": 0}
TEST_DICT_SOME_STRINGS = {
    "lnum": 0,
    "col": "10",
    "curswant": b"10",
    "leftcol": 5,
    "topline": "0",
}
TEST_VIEW = View(0, 10, 10, 5, 0)


@mock.patch(
    "pathfinder.window.vim.eval",
    return_value={**TEST_DICT_SOME_STRINGS, "extra values should be ignored": 200},
)
def test_winsaveview(vim_eval):
    assert winsaveview() == TEST_VIEW
    vim_eval.assert_called_once_with("winsaveview()")


@mock.patch("pathfinder.window.vim.eval")
def test_winrestview(vim_eval):
    winrestview(TEST_VIEW)
    vim_eval.assert_called_once_with(f"winrestview({TEST_DICT})")


@pytest.mark.parametrize(
    "view_a,view_b,expected",
    [
        (View(0, 0, None, None, None), View(0, 0, None, None, None), True),
        (View(0, 0, None, None, None), View(1, 0, None, None, None), False),
        (View(0, 0, None, None, None), View(0, 1, None, None, None), False),
        (View(0, 0, None, None, None), View(1, 1, None, None, None), False),
    ],
    ids=["same position", "lnum differs", "col differs", "both differ"],
)
def test_cursor_in_same_position(view_a, view_b, expected):
    assert cursor_in_same_position(view_a, view_b) is expected
Download .txt
gitextract_r8990n86/

├── .github/
│   ├── CODEOWNERS
│   └── workflows/
│       ├── pr.yml
│       └── release.yml
├── .gitignore
├── .gitmodules
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE
├── MANIFEST.in
├── README.md
├── pathfinder/
│   ├── __init__.py
│   ├── client/
│   │   ├── __init__.py
│   │   ├── autorun.py
│   │   ├── client.py
│   │   ├── explore_lines.py
│   │   ├── output.py
│   │   ├── plugin.py
│   │   ├── popup.py
│   │   └── state_tracker.py
│   ├── debytes.py
│   ├── server/
│   │   ├── __init__.py
│   │   ├── dijkstra.py
│   │   ├── motions/
│   │   │   ├── __init__.py
│   │   │   ├── find.py
│   │   │   ├── search.py
│   │   │   └── simple.py
│   │   ├── node.py
│   │   └── server.py
│   └── window.py
├── plugin/
│   ├── defaults.vim
│   ├── dimensions.vim
│   └── main.vim
├── serverrc.vim
├── setup.cfg
├── setup.py
├── test_requirements.txt
└── tests/
    ├── __init__.py
    ├── conftest.py
    ├── test_autorun.py
    ├── test_debytes.py
    ├── test_explore_lines.py
    ├── test_motions_find.py
    ├── test_motions_search.py
    ├── test_motions_simple.py
    ├── test_node.py
    ├── test_output.py
    ├── test_popup.py
    ├── test_state_tracker.py
    └── test_window.py
Download .txt
SYMBOL INDEX (129 symbols across 27 files)

FILE: pathfinder/client/autorun.py
  function choose_action (line 6) | def choose_action(start_state, current_state, update_time):

FILE: pathfinder/client/client.py
  class Client (line 11) | class Client:
    method __init__ (line 23) | def __init__(self):
    method open (line 26) | def open(self):
    method _build_server_cmd (line 40) | def _build_server_cmd(self):
    method close (line 69) | def close(self):
    method poll_responses (line 78) | def poll_responses(self):
    method connect (line 93) | def connect(self):
    method handle_response (line 123) | def handle_response(self, response_type, data):
    method pathfind (line 141) | def pathfind(self, buffer_contents, start_view, target_view, callback):

FILE: pathfinder/client/explore_lines.py
  function get_explore_lines (line 4) | def get_explore_lines(search_area_lines):
  function get_line_limits (line 30) | def get_line_limits(start_view, target_view):

FILE: pathfinder/client/output.py
  function strtrans (line 10) | def strtrans(string):
  function get_count (line 16) | def get_count(motion, count):
  function compact_motions (line 30) | def compact_motions(motions):
  function get_description (line 44) | def get_description(motion, repetitions):
  function explained_motions (line 52) | def explained_motions(motions):

FILE: pathfinder/client/plugin.py
  class Plugin (line 13) | class Plugin:
    method __init__ (line 14) | def __init__(self):
    method _run (line 19) | def _run(self):
    method popup (line 28) | def popup(self, motions):
    method autorun (line 32) | def autorun(self):
    method command_begin (line 42) | def command_begin(self):
    method command_run (line 46) | def command_run(self):
    method command_explain (line 58) | def command_explain(self):
    method stop (line 67) | def stop(self):

FILE: pathfinder/client/popup.py
  function _neovim_popup (line 6) | def _neovim_popup(text, line_offset):
  function _vim_popup (line 34) | def _vim_popup(text, line_offset):
  function open_popup (line 50) | def open_popup(text):

FILE: pathfinder/client/state_tracker.py
  class StateTracker (line 11) | class StateTracker:
    method __init__ (line 12) | def __init__(self):
    method _set_update_time (line 15) | def _set_update_time(self):
    method _record_state (line 18) | def _record_state(self):
    method reset (line 27) | def reset(self):
    method _reset (line 31) | def _reset(self, new_state):
    method set_target (line 36) | def set_target(self):
    method _set_target (line 40) | def _set_target(self, new_state):
    method choose_action_using (line 45) | def choose_action_using(self, function):

FILE: pathfinder/debytes.py
  function debytes (line 1) | def debytes(string):

FILE: pathfinder/server/dijkstra.py
  class Dijkstra (line 9) | class Dijkstra:
    method __init__ (line 19) | def __init__(self, from_view, target_view, min_line, max_line):
    method find_path (line 39) | def find_path(self, client_connection):

FILE: pathfinder/server/motions/__init__.py
  class MotionGenerator (line 11) | class MotionGenerator(abc.ABC):
    method __init__ (line 12) | def __init__(self, dijkstra):
    method generate (line 16) | def generate(self, view):
    method _create_node (line 20) | def _create_node(self, *args, **kwargs):

FILE: pathfinder/server/motions/find.py
  class FindMotionGenerator (line 6) | class FindMotionGenerator(MotionGenerator):
    method generate (line 9) | def generate(self, view):
    method _find (line 13) | def _find(self, view, motion):

FILE: pathfinder/server/motions/search.py
  class SearchMotionGenerator (line 8) | class SearchMotionGenerator(MotionGenerator):
    method generate (line 9) | def generate(self, view):
    method _create_motion (line 24) | def _create_motion(self, search_query, motion="/"):
    method _escape_magic (line 27) | def _escape_magic(self, search_query):
    method _search (line 33) | def _search(self, text, start, target):
    method _search_lines (line 64) | def _search_lines(self, lines, start_line, start_col, target_line, tar...

FILE: pathfinder/server/motions/simple.py
  class SimpleMotionGenerator (line 7) | class SimpleMotionGenerator(MotionGenerator):
    method generate (line 67) | def generate(self, view):
    method _try_motion (line 73) | def _try_motion(self, view, motion):

FILE: pathfinder/server/node.py
  class Node (line 4) | class Node:
    method __init__ (line 7) | def __init__(self, dijkstra, view, came_by_motion):
    method get_neighbours (line 17) | def get_neighbours(self):
    method motion_weight (line 27) | def motion_weight(self, motion):
    method set_came_from (line 45) | def set_came_from(self, node):
    method is_target (line 54) | def is_target(self):
    method reconstruct_path (line 57) | def reconstruct_path(self):

FILE: pathfinder/server/server.py
  class Server (line 10) | class Server:
    method __init__ (line 24) | def __init__(self, file_path):
    method run (line 27) | def run(self):
    method message_loop (line 41) | def message_loop(self):
    method do_action (line 63) | def do_action(self, data):
    method pathfind (line 83) | def pathfind(self):

FILE: pathfinder/window.py
  function winsaveview (line 8) | def winsaveview():
  function winrestview (line 14) | def winrestview(view):
  function cursor_in_same_position (line 19) | def cursor_in_same_position(a, b):

FILE: tests/test_autorun.py
  function default_state (line 12) | def default_state():
  function test_choose_action_new_window (line 16) | def test_choose_action_new_window(default_state):
  function test_choose_action_non_motion_mode (line 28) | def test_choose_action_non_motion_mode(default_state):
  function test_choose_action_changed_mode (line 40) | def test_choose_action_changed_mode(default_state):
  function test_choose_action_changed_lines (line 52) | def test_choose_action_changed_lines(default_state):
  function test_choose_action_cursor_idle (line 64) | def test_choose_action_cursor_idle(default_state):
  function test_choose_action_set_target (line 76) | def test_choose_action_set_target(default_state):
  function test_choose_action_do_nothing (line 88) | def test_choose_action_do_nothing(default_state):

FILE: tests/test_debytes.py
  function test_debytes_bytes (line 4) | def test_debytes_bytes():
  function test_debytes_str (line 8) | def test_debytes_str():

FILE: tests/test_explore_lines.py
  function test_get_explore_lines (line 7) | def test_get_explore_lines():
  function test_get_explore_lines_max_0 (line 15) | def test_get_explore_lines_max_0():
  function test_get_explore_lines_scale_0 (line 23) | def test_get_explore_lines_scale_0():
  function test_get_line_limits (line 32) | def test_get_line_limits(mock_get_explore_lines):
  function test_get_line_limits_with_explore (line 42) | def test_get_line_limits_with_explore(mock_get_explore_lines):

FILE: tests/test_motions_find.py
  function test_find_f (line 12) | def test_find_f():
  function test_find_f_final_column (line 24) | def test_find_f_final_column():
  function test_find_t (line 32) | def test_find_t():
  function test_find_t_penultimate_column (line 43) | def test_find_t_penultimate_column():
  function test_find_F (line 51) | def test_find_F():
  function test_find_F_first_column (line 63) | def test_find_F_first_column():
  function test_find_T (line 71) | def test_find_T():
  function test_find_T_second_column (line 82) | def test_find_T_second_column():

FILE: tests/test_motions_search.py
  function test_escape_magic (line 7) | def test_escape_magic():
  function test_search_finds_shortest_possible_query (line 14) | def test_search_finds_shortest_possible_query():
  function test_search_when_target_is_before_start (line 20) | def test_search_when_target_is_before_start():
  function test_search_lines_calls_search_correctly (line 27) | def test_search_lines_calls_search_correctly(mock_search):

FILE: tests/test_motions_simple.py
  function test_try_motion (line 13) | def test_try_motion(winrestview, winsaveview, command):
  function test_try_motion_when_motion_raises_exception (line 24) | def test_try_motion_when_motion_raises_exception(winrestview, winsavevie...
  function test_generate (line 34) | def test_generate(try_motion, create_node):
  function test_generate_when_try_motion_returns_none (line 45) | def test_generate_when_try_motion_returns_none(try_motion, create_node):
  function test_generate_when_try_motion_returns_same_as_input (line 56) | def test_generate_when_try_motion_returns_same_as_input(try_motion, crea...

FILE: tests/test_node.py
  function test_reconstruct_path (line 9) | def test_reconstruct_path():
  function test_set_came_from_same_motion (line 26) | def test_set_came_from_same_motion():
  function test_set_came_from_different_motion (line 34) | def test_set_came_from_different_motion():
  function test_motion_weight_same_motion (line 47) | def test_motion_weight_same_motion(repetitions, expected):
  function test_motion_weight_different_motion (line 58) | def test_motion_weight_different_motion(motion, expected):

FILE: tests/test_output.py
  function test_get_count (line 19) | def test_get_count(motion, count, expected):
  function test_compact_motions (line 25) | def test_compact_motions():
  function test_explained_motions (line 32) | def test_explained_motions():

FILE: tests/test_popup.py
  function test_open_popup_neovim (line 16) | def test_open_popup_neovim(vim_eval, neovim_popup, vim_popup):
  function test_open_popup_vim (line 33) | def test_open_popup_vim(vim_eval, neovim_popup, vim_popup):
  function test_open_popup_echo (line 50) | def test_open_popup_echo(vim_eval, neovim_popup, vim_popup):
  function test_vim_popup (line 57) | def test_vim_popup():
  function test_neovim_popup (line 87) | def test_neovim_popup(create_buf, buf_set_lines, open_win, win_set_optio...

FILE: tests/test_state_tracker.py
  function tracker (line 9) | def tracker():
  function test_reset (line 18) | def test_reset(tracker):
  function test_set_new_target (line 33) | def test_set_new_target(tracker):
  function test_set_same_target (line 48) | def test_set_same_target(tracker):
  function test_choose_action_reset (line 63) | def test_choose_action_reset(tracker):
  function test_choose_action_set_target (line 83) | def test_choose_action_set_target(tracker):
  function test_choose_action_other (line 103) | def test_choose_action_other(tracker):

FILE: tests/test_window.py
  function test_winsaveview (line 22) | def test_winsaveview(vim_eval):
  function test_winrestview (line 28) | def test_winrestview(vim_eval):
  function test_cursor_in_same_position (line 43) | def test_cursor_in_same_position(view_a, view_b, expected):
Condensed preview — 49 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (82K chars).
[
  {
    "path": ".github/CODEOWNERS",
    "chars": 72,
    "preview": "LICENSE @AlphaMycelium\n.github/ @AlphaMycelium\nsetup.cfg @AlphaMycelium\n"
  },
  {
    "path": ".github/workflows/pr.yml",
    "chars": 939,
    "preview": "name: Checks\n\non: [pull_request]\n\njobs:\n  test:\n    name: Test on Python ${{ matrix.python-version }}\n    runs-on: ubunt"
  },
  {
    "path": ".github/workflows/release.yml",
    "chars": 1526,
    "preview": "name: Release\n\non:\n  push:\n    branches:\n      - master\n\njobs:\n  test:\n    name: Test on Python ${{ matrix.python-versio"
  },
  {
    "path": ".gitignore",
    "chars": 2035,
    "preview": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packagi"
  },
  {
    "path": ".gitmodules",
    "chars": 95,
    "preview": "[submodule \"heapdict\"]\n\tpath = heapdict\n\turl = https://github.com/DanielStutzbach/heapdict.git\n"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 416,
    "preview": "# Changelog\n\n<!--next-version-placeholder-->\n\n## v3.1.3 (2022-08-24)\n| Type | Change |\n| --- | --- |\n| Fix | Don't error"
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 2414,
    "preview": "# Contributing to Pathfinder.vim\n\n## Commit messages\n\n**Please follow the\n[Angular commit style](https://github.com/angu"
  },
  {
    "path": "LICENSE",
    "chars": 1072,
    "preview": "MIT License\n\nCopyright (c) 2020 Daniel Thwaites\n\nPermission is hereby granted, free of charge, to any person obtaining a"
  },
  {
    "path": "MANIFEST.in",
    "chars": 165,
    "preview": "include LICENSE README.md\ninclude plugin/* colors/* ftdetect/* ftplugin/* indent/* compiler/* after/* autoload/* doc/*\ni"
  },
  {
    "path": "README.md",
    "chars": 4005,
    "preview": "# pathfinder.vim\n\nA Vim plugin to give suggestions to improve your movements.\nIt's a bit like [Clippy][office-assistant]"
  },
  {
    "path": "pathfinder/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "pathfinder/client/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "pathfinder/client/autorun.py",
    "chars": 1235,
    "preview": "import time\n\nimport vim\n\n\ndef choose_action(start_state, current_state, update_time):\n    \"\"\"\n    Select an action to ta"
  },
  {
    "path": "pathfinder/client/client.py",
    "chars": 5881,
    "preview": "import os\nimport subprocess\nimport tempfile\nfrom multiprocessing import connection\n\nimport vim\n\nfrom pathfinder.client.e"
  },
  {
    "path": "pathfinder/client/explore_lines.py",
    "chars": 1581,
    "preview": "import vim\n\n\ndef get_explore_lines(search_area_lines):\n    \"\"\"\n    :param search_area_lines: Number of lines between, an"
  },
  {
    "path": "pathfinder/client/output.py",
    "chars": 1667,
    "preview": "import itertools\n\nimport vim\n\nfrom pathfinder.debytes import debytes\n\nlast_output = None\n\n\ndef strtrans(string):\n    \"\"\""
  },
  {
    "path": "pathfinder/client/plugin.py",
    "chars": 2252,
    "preview": "import time\n\nimport vim\n\nimport pathfinder.client.output as output\nfrom pathfinder.client.autorun import choose_action\nf"
  },
  {
    "path": "pathfinder/client/popup.py",
    "chars": 1690,
    "preview": "import vim\n\nfrom pathfinder.debytes import debytes\n\n\ndef _neovim_popup(text, line_offset):\n    \"\"\"Create a popup using N"
  },
  {
    "path": "pathfinder/client/state_tracker.py",
    "chars": 1835,
    "preview": "import time\nfrom collections import namedtuple\n\nimport vim\n\nfrom pathfinder.window import winsaveview\n\nState = namedtupl"
  },
  {
    "path": "pathfinder/debytes.py",
    "chars": 291,
    "preview": "def debytes(string):\n    \"\"\"\n    Decode string if it is a bytes object.\n\n    This is necessary since Neovim, correctly, "
  },
  {
    "path": "pathfinder/server/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "pathfinder/server/dijkstra.py",
    "chars": 2619,
    "preview": "from heapdict import heapdict\n\nfrom pathfinder.server.motions.find import FindMotionGenerator\nfrom pathfinder.server.mot"
  },
  {
    "path": "pathfinder/server/motions/__init__.py",
    "chars": 576,
    "preview": "import abc\nfrom collections import namedtuple\n\nfrom pathfinder.server.node import Node\n\n# motion - The motion such as h,"
  },
  {
    "path": "pathfinder/server/motions/find.py",
    "chars": 1849,
    "preview": "import vim\n\nfrom pathfinder.server.motions import Motion, MotionGenerator\n\n\nclass FindMotionGenerator(MotionGenerator):\n"
  },
  {
    "path": "pathfinder/server/motions/search.py",
    "chars": 2932,
    "preview": "import re\n\nimport vim\n\nfrom pathfinder.server.motions import Motion, MotionGenerator\n\n\nclass SearchMotionGenerator(Motio"
  },
  {
    "path": "pathfinder/server/motions/simple.py",
    "chars": 1546,
    "preview": "import vim\n\nfrom pathfinder.server.motions import Motion, MotionGenerator\nfrom pathfinder.window import winrestview, win"
  },
  {
    "path": "pathfinder/server/node.py",
    "chars": 2313,
    "preview": "from pathfinder.window import cursor_in_same_position\n\n\nclass Node:\n    \"\"\"Graph node linked to a view (cursor+scroll lo"
  },
  {
    "path": "pathfinder/server/server.py",
    "chars": 3703,
    "preview": "import traceback\nfrom multiprocessing import connection\n\nimport vim\n\nfrom pathfinder.debytes import debytes\nfrom pathfin"
  },
  {
    "path": "pathfinder/window.py",
    "chars": 642,
    "preview": "from collections import namedtuple\n\nimport vim\n\nView = namedtuple(\"View\", \"lnum col curswant leftcol topline\")\n\n\ndef win"
  },
  {
    "path": "plugin/defaults.vim",
    "chars": 4036,
    "preview": "highlight default link PathfinderPopup Cursor\n\nif !exists('g:pf_autorun_delay')\n  let g:pf_autorun_delay = 2\nendif\n\nif !"
  },
  {
    "path": "plugin/dimensions.vim",
    "chars": 780,
    "preview": "\" Get the width of the current window, excluding sign and number columns\nfunction! WindowTextWidth()\n  let l:decorationC"
  },
  {
    "path": "plugin/main.vim",
    "chars": 1435,
    "preview": "if exists('g:pf_loaded') | finish | endif\nif !has('python3')\n  echom 'The +python3 feature is required to run pathfinder"
  },
  {
    "path": "serverrc.vim",
    "chars": 229,
    "preview": "set nocompatible\n\n\" Disable unnecessary features\nset noruler\nset nonumber\nset noshowcmd\nset signcolumn=no\nfiletype off\ns"
  },
  {
    "path": "setup.cfg",
    "chars": 481,
    "preview": "[semantic_release]\nversion_variable=setup.py:__version__\nchangelog_components=semantic_release.changelog.changelog_table"
  },
  {
    "path": "setup.py",
    "chars": 308,
    "preview": "from setuptools import find_packages, setup\n\n__version__ = \"3.1.3\"\n\n\nsetup(\n    name=\"pathfinder.vim\",\n    version=__ver"
  },
  {
    "path": "test_requirements.txt",
    "chars": 33,
    "preview": "pytest >=5,<6\npytest-stub >=1,<2\n"
  },
  {
    "path": "tests/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/conftest.py",
    "chars": 105,
    "preview": "from pytest_stub.toolbox import stub_global\n\nstub_global(\n    {\n        \"vim\": \"[mock_persist]\",\n    }\n)\n"
  },
  {
    "path": "tests/test_autorun.py",
    "chars": 2348,
    "preview": "import time\nfrom unittest import mock\n\nimport pytest\n\nfrom pathfinder.client.autorun import choose_action\nfrom pathfinde"
  },
  {
    "path": "tests/test_debytes.py",
    "chars": 196,
    "preview": "from pathfinder.debytes import debytes\n\n\ndef test_debytes_bytes():\n    assert debytes(b\"hello world\") == \"hello world\"\n\n"
  },
  {
    "path": "tests/test_explore_lines.py",
    "chars": 1582,
    "preview": "from collections import namedtuple\nfrom unittest import mock\n\nfrom pathfinder.client.explore_lines import get_explore_li"
  },
  {
    "path": "tests/test_motions_find.py",
    "chars": 3015,
    "preview": "from collections import namedtuple\nfrom unittest import mock\n\nfrom pathfinder.server.motions import Motion\nfrom pathfind"
  },
  {
    "path": "tests/test_motions_search.py",
    "chars": 997,
    "preview": "from unittest import mock\n\nfrom pathfinder.server.motions import Motion\nfrom pathfinder.server.motions.search import Sea"
  },
  {
    "path": "tests/test_motions_simple.py",
    "chars": 2774,
    "preview": "from unittest import mock\n\nfrom pathfinder.server.motions import Motion\nfrom pathfinder.server.motions.simple import Sim"
  },
  {
    "path": "tests/test_node.py",
    "chars": 1811,
    "preview": "from unittest import mock\n\nimport pytest\n\nfrom pathfinder.server.motions import Motion\nfrom pathfinder.server.node impor"
  },
  {
    "path": "tests/test_output.py",
    "chars": 1353,
    "preview": "from unittest import mock\n\nimport pytest\n\nfrom pathfinder.client.output import compact_motions, explained_motions, get_c"
  },
  {
    "path": "tests/test_popup.py",
    "chars": 3605,
    "preview": "from unittest import mock\n\nfrom pathfinder.client.popup import _neovim_popup, _vim_popup, open_popup\n\n\n@mock.patch(\"path"
  },
  {
    "path": "tests/test_state_tracker.py",
    "chars": 3759,
    "preview": "from unittest import mock\n\nimport pytest\n\nfrom pathfinder.client.state_tracker import StateTracker\n\n\n@pytest.fixture\ndef"
  },
  {
    "path": "tests/test_window.py",
    "chars": 1368,
    "preview": "from unittest import mock\n\nimport pytest\n\nfrom pathfinder.window import View, cursor_in_same_position, winrestview, wins"
  }
]

About this extraction

This page contains the full source code of the AlphaMycelium/pathfinder.vim GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 49 files (73.8 KB), approximately 19.3k tokens, and a symbol index with 129 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!