[
  {
    "path": ".github/CODEOWNERS",
    "content": "LICENSE @AlphaMycelium\n.github/ @AlphaMycelium\nsetup.cfg @AlphaMycelium\n"
  },
  {
    "path": ".github/workflows/pr.yml",
    "content": "name: Checks\n\non: [pull_request]\n\njobs:\n  test:\n    name: Test on Python ${{ matrix.python-version }}\n    runs-on: ubuntu-latest\n\n    strategy:\n      matrix:\n        python-version: [3.6, 3.7, 3.8, 3.9]\n\n    steps:\n    - name: Set up Python ${{ matrix.python-version }}\n      uses: actions/setup-python@v1\n      with:\n        python-version: ${{ matrix.python-version }}\n\n    - name: Checkout repository\n      uses: actions/checkout@v2\n\n    - name: Cache dependencies\n      uses: actions/cache@v1\n      with:\n        path: ~/.cache/pip\n        key: ${{ runner.os }}-pip-${{ hashFiles('test_requirements.txt') }}\n        restore-keys: |\n          ${{ runner.os }}-pip-\n\n    - name: Install dependencies\n      run: python -m pip install -r test_requirements.txt\n\n    - name: Install pytest-github-actions-annotate-failures\n      run: python -m pip install pytest-github-actions-annotate-failures\n\n    - name: Run pytest\n      run: pytest -v\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Release\n\non:\n  push:\n    branches:\n      - master\n\njobs:\n  test:\n    name: Test on Python ${{ matrix.python-version }}\n    runs-on: ubuntu-latest\n\n    strategy:\n      matrix:\n        python-version: [3.6, 3.7, 3.8, 3.9]\n\n    steps:\n    - name: Set up Python ${{ matrix.python-version }}\n      uses: actions/setup-python@v1\n      with:\n        python-version: ${{ matrix.python-version }}\n\n    - name: Checkout repository\n      uses: actions/checkout@v2\n\n    - name: Cache dependencies\n      uses: actions/cache@v1\n      with:\n        path: ~/.cache/pip\n        key: ${{ runner.os }}-pip-${{ hashFiles('test_requirements.txt') }}\n        restore-keys: |\n          ${{ runner.os }}-pip-\n\n    - name: Install dependencies\n      run: python -m pip install -r test_requirements.txt\n\n    - name: Install pytest-github-actions-annotate-failures\n      run: python -m pip install pytest-github-actions-annotate-failures\n\n    - name: Run pytest\n      run: pytest -v\n\n  release:\n    name: Semantic Release\n    runs-on: ubuntu-latest\n    needs: [test]\n    if: github.repository_owner == 'danth'\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v2\n        with:\n          fetch-depth: 0\n          ref: ${{ needs.beautify.outputs.new_sha }}\n\n      - name: Fetch master\n        run: git fetch --prune origin +refs/heads/master:refs/remotes/origin/master\n\n      - name: Semantic Release\n        uses: relekang/python-semantic-release@v7.15.5\n        with:\n          github_token: ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": ".gitignore",
    "content": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\nshare/python-wheels/\n*.egg-info/\n.installed.cfg\n*.egg\nMANIFEST\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.nox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n*.py,cover\n.hypothesis/\n.pytest_cache/\ncover/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\ndb.sqlite3\ndb.sqlite3-journal\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\n.pybuilder/\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# IPython\nprofile_default/\nipython_config.py\n\n# pyenv\n#   For a library or package, you might want to ignore these files since the code is\n#   intended to run in multiple environments; otherwise, check them in:\n# .python-version\n\n# pipenv\n#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.\n#   However, in case of collaboration, if having platform-specific dependencies or dependencies\n#   having no cross-platform support, pipenv may install dependencies that don't work, or not\n#   install all needed dependencies.\n#Pipfile.lock\n\n# PEP 582; used by e.g. github.com/David-OConnor/pyflow\n__pypackages__/\n\n# Celery stuff\ncelerybeat-schedule\ncelerybeat.pid\n\n# SageMath parsed files\n*.sage.py\n\n# Environments\n.env\n.venv\nenv/\nvenv/\nENV/\nenv.bak/\nvenv.bak/\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n.dmypy.json\ndmypy.json\n\n# Pyre type checker\n.pyre/\n\n# pytype static type analyzer\n.pytype/\n\n# Cython debug symbols\ncython_debug/\n"
  },
  {
    "path": ".gitmodules",
    "content": "[submodule \"heapdict\"]\n\tpath = heapdict\n\turl = https://github.com/DanielStutzbach/heapdict.git\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\n<!--next-version-placeholder-->\n\n## v3.1.3 (2022-08-24)\n| Type | Change |\n| --- | --- |\n| Fix | Don't error when `g:python3_host_prog` is unset ([`4c01c96`](https://github.com/danth/pathfinder.vim/commit/4c01c9635a54e5669e05c26b9c443c3f86de84d2)) |\n| Documentation | Update contributing guidelines ([`c83f926`](https://github.com/danth/pathfinder.vim/commit/c83f9264f859367f2f7998e10907e57a99d414cb)) |\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing to Pathfinder.vim\n\n## Commit messages\n\n**Please follow the\n[Angular commit style](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#commits)\nin your commit messages so that the automatic version numbering can work.**\n\n## Development environment\n\nThe plugin files will be cloned somewhere under `~/.vim` by your plugin manager.\nFor example Plug places them in `~/.vim/plugged/pathfinder.vim/`.\n\nSome plugin managers also allow you to clone the repository into a directory of\nyour choice and use the path to that directory to install the plugin.\n\n## Pathfinding\n\nThe pathfinding algorithm used is [Dijkstra's algorithm][dijkstra], which is\njust a greedy algorithm. Vertices are generated on-the-fly rather than building\nthe entire graph beforehand.\n\nEach vertex in the graph is a tuple of `(view, most recent motion)`.  `view` is\nthe result of `winsaveview()` in Vim: this includes both the cursor and scroll\npositions. We need to store the most recent motion because it is used to\ncalculate certain weights - `2fk` should be cheaper than `fkfy`.\n\n[dijkstra]: https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm\n\n## Python to Vim interface\n\nVim (when compiled with `+python3`) comes with an embedded Python module called\n`vim`, which is used to send commands and output from Python.\n\nIn Vimscript, the following syntax is used to call Python code:\n\n```vim\npython3 <line of code>\n```\n\n```vim\npython3 << endpython\n<multiple lines of code>\nendpython\n```\n\n## Client and Server\n\nThe way we discover connections between nodes (characters) in the pathfinding\ngraph is:\n\n1. Move the cursor to the node position\n2. Run the motion with `silent! normal! <motion>`\n3. Check where the cursor moved to and record it as a connection\n\nThis causes the cursor to move around while things are being tested. We want to\nallow users to continue working while paths are found, hence we start a \"server\"\nwhich is basically another Vim process using [this barebones vimrc](serverrc.vim).\n\nThe user's Vim (the client) sends the contents of the current buffer, the start\nand target positions, the window dimensions and some other information to the\nserver when it wants to request a path. This communication happens through\n`multiprocessing.connection` with a temporary file.\n\nThe server will then do pathfinding in the background and send the result back\nwhen it is done, which the client receives and displays.\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2020 Daniel Thwaites\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "MANIFEST.in",
    "content": "include LICENSE README.md\ninclude plugin/* colors/* ftdetect/* ftplugin/* indent/* compiler/* after/* autoload/* doc/*\ninclude heapdict/LICENSE heapdict/heapdict.py\n"
  },
  {
    "path": "README.md",
    "content": "# pathfinder.vim\n\nA Vim plugin to give suggestions to improve your movements.\nIt's a bit like [Clippy][office-assistant].\n\n[![Demo](https://asciinema.org/a/CYX4I94GGBsHZqMVc9N8MerFD.svg)](https://asciinema.org/a/CYX4I94GGBsHZqMVc9N8MerFD)\n\n[office-assistant]: https://en.wikipedia.org/wiki/Office_Assistant\n\n\n## Features\n\n- Find the shortest motion sequence or search query to move the cursor\n- Help summaries to explain a suggestion\n- Asynchronous - pathfinding runs in a separate process\n\n\n## Installation\n\nUse your favorite plugin manager. I recommend\n[vim-plug](https://github.com/junegunn/vim-plug).\n\n```vim\nif has('python3') && has('timers')\n  Plug 'danth/pathfinder.vim'\nelse\n  echoerr 'pathfinder.vim is not supported on this Vim installation'\nendif\n```\n\nYou may also need to run `git submodule update --init` from inside the plugin\ndirectory. Most popular plugin managers will do that automatically.\n\n\n## Usage\n\n1. Move the cursor in normal, visual or visual-line mode.\n2. That's it.\n\nSuggestions pop up above the cursor if you have:\n\n- Vim 8.2 or above, with `+popupwin`\n- Neovim 0.4 or above\n\nOtherwise, they will appear as a plain `echo` at the bottom of the screen.\n\n### Explanations\n\nIf you don't understand how a suggestion works, you should use the\n`:PathfinderExplain` command, which will show a short description of each\nmotion used.\n\nIf you find yourself using this a lot, make a mapping for it!\n\n```vim\nnoremap <leader>pe :PathfinderExplain<CR>\n```\n\n### Manual Commands\n\nIf you set [`g:pf_autorun_delay`](#gpf_autorun_delay) to a negative value,\nyou get two commands instead:\n\n- `:PathfinderBegin`: Set the start position. This still happens automatically\n  when switching windows/tabs, or loading a new file.\n- `:PathfinderRun`: Set the target position and get a suggestion.\n\n\n## Related Plugins\n\n- [vim-be-good](https://github.com/ThePrimeagen/vim-be-good) - Various training games to practice certain actions\n- [vim-hardtime](https://github.com/takac/vim-hardtime) - Prevent yourself from repeating keys like `h`,`j`,`k`,`l`\n\n\n## Configuration\n\n*pathfinder.vim works out-of-the box with the default configuration. You don't\nneed to read this section if you don't want to.*\n\n### `highlight PathfinderPopup`\nChange the appearance of suggestion popups. *Default: same as cursor*\n\n### `g:pf_popup_time`\nMilliseconds to display the popup for. *Default: 3000*\n\n### `g:pf_autorun_delay`\nWhen this number of seconds have elapsed with no motions being made, the\npathfinder will run. It also runs for other events such as changing modes.\nA negative value will disable automatic suggestions. *Default: 2*\n\n### `g:pf_explore_scale`\nMultiplier which determines the range of lines to be explored around the start\nand target positions. This is calculated as (lines between start and target\n&times; multiplier) and added to both sides. *Default: 0.5*\n\nThis limitation improves performance by disallowing movements outside the area\nof interest. It also prevents suggestions which rely on knowing about the exact\ntext hundreds of lines away. Settings below 1 cause movements within a line to\nonly use motions inside that line.\n\nIf you have a powerful computer, you can increase this option to a high value\nto allow exploring more of the file. You can also disable it completely by\nsetting a negative value.\n\n### `g:pf_max_explore`\nCap the number of surrounding lines explored (see above) to a maximum value.\nAs usual, this can be disabled by making it negative. *Default: 10*\n\n### `g:pf_descriptions`\nDictionary of descriptions, used for `:PathfinderExplain`.\n\n```vim\nlet g:pf_descriptions['k'] = 'Up {count} lines'\nlet g:pf_descriptions['f'] = 'To occurence {count} of \"{argument}\", to the right'\n```\n\nEnsure the plugin is loaded before trying to override keys.  Otherwise, the\ndefault dictionary will not exist and you'll get an error.\n\nRe-defining the entire dictionary is not recommended since it could cause\nproblems if support for a new motion is added and you don't have a description\nfor it.\n"
  },
  {
    "path": "pathfinder/__init__.py",
    "content": ""
  },
  {
    "path": "pathfinder/client/__init__.py",
    "content": ""
  },
  {
    "path": "pathfinder/client/autorun.py",
    "content": "import time\n\nimport vim\n\n\ndef choose_action(start_state, current_state, update_time):\n    \"\"\"\n    Select an action to take automatically.\n\n    This is intended for use with StateTracker.choose_action_using().\n\n    Returns one of:\n    \"reset\" - Set start and target to the current state\n    \"set_target\" - Set target to the current state\n    \"pathfind\" - Start pathfinding, using the target from last time it was set\n    None - Do nothing\n    \"\"\"\n    if (\n        start_state.window != current_state.window\n        or start_state.buffer != current_state.buffer\n    ):\n        # Reset to ensure the start or target view isn't set to a location\n        # which is now impossible to access\n        return \"reset\"\n\n    delay = vim.vars[\"pf_autorun_delay\"]\n    if delay > 0:  # If delay <= 0, then the user disabled autorun\n        if start_state.mode not in {\"n\", \"v\", \"V\", \"\u0016\"}:\n            # Motions are not used in this mode, so pathfinding is useless\n            return \"reset\"\n\n        if (\n            time.time() >= update_time + delay\n            or start_state.mode != current_state.mode\n            or start_state.lines != current_state.lines\n        ):\n            return \"pathfind\"\n        else:\n            return \"set_target\"\n"
  },
  {
    "path": "pathfinder/client/client.py",
    "content": "import os\nimport subprocess\nimport tempfile\nfrom multiprocessing import connection\n\nimport vim\n\nfrom pathfinder.client.explore_lines import get_line_limits\n\n\nclass Client:\n    \"\"\"\n    Starts and connects to a separate Vim instance used for testing motions.\n\n    This is used to test motions in the exact same environment as the user (loaded via\n    a temporary session file), without moving the user's cursor in the process. This\n    allows pathfinding to happen in the background while the user may continue working.\n\n    A custom vimrc is used to only load this plugin, disabling other plugins and user\n    settings.\n    \"\"\"\n\n    def __init__(self):\n        self.open()\n\n    def open(self):\n        \"\"\"Launch and connect to the server Vim.\"\"\"\n        # Create a file used to communicate with the server\n        self.file_path = os.path.join(\n            tempfile.gettempdir(), \"pathfinder_vim_\" + vim.eval(\"getpid()\")\n        )\n\n        self.server_process = subprocess.Popen(\n            self._build_server_cmd(), stdout=subprocess.PIPE, stderr=subprocess.PIPE\n        )\n\n        self.server_connection = None\n        self.to_send = None\n\n    def _build_server_cmd(self):\n        \"\"\"Build the command used to launch the server Vim.\"\"\"\n        progpath = vim.eval(\"v:progpath\")\n\n        options = [\n            progpath,\n            \"--clean\",\n            \"--cmd\",\n            f\"let g:pf_server_communication_file='{self.file_path}'\",\n            \"-u\",\n            os.path.normpath(\n                # serverrc.vim in the root of this repository, instead of the user's\n                # regular .vimrc or init.vim\n                os.path.join(os.path.dirname(__file__), \"..\", \"..\", \"serverrc.vim\")\n            ),\n        ]\n\n        if progpath.endswith(\"nvim\"):\n            options += [\"--headless\"]\n            try:\n                python3_host_prog = vim.eval(\"g:python3_host_prog\")\n                options += [\"--cmd\", f\"let g:python3_host_prog='{python3_host_prog}'\"]\n            except:\n                pass\n        else:\n            options += [\"-v\", \"--not-a-term\"]\n\n        return options\n\n    def close(self):\n        \"\"\"Shut down the server Vim.\"\"\"\n        if self.server_connection is not None:\n            # Server will shut down Vim gracefully when we disconnect\n            self.server_connection.close()\n        elif self.server_process is not None:\n            # Not connected yet, terminate the process instead\n            self.server_process.terminate()\n\n    def poll_responses(self):\n        if not self.connect():\n            return\n\n        # Check if a request is waiting to be sent\n        if self.to_send is not None:\n            self.server_connection.send(self.to_send)\n            self.to_send = None\n\n        # Check if any data is available to be read\n        elif self.server_connection.poll():\n            # Get response (sent in a tuple of type, data)\n            response_type, data = self.server_connection.recv()\n            self.handle_response(response_type, data)\n\n    def connect(self):\n        \"\"\"\n        Attempt to connect to the server.\n\n        :returns: whether a connection is ready.\n        \"\"\"\n        if self.server_connection is not None:\n            return True\n\n        if self.server_process is None:\n            # Server process has exited but we already raised an exception\n            return False\n\n        return_code = self.server_process.poll()\n        if return_code is not None:\n            # Server process has exited\n            stdout, stderr = self.server_process.communicate()\n            self.server_process = None\n            raise Exception(\n                f\"Pathfinding server process exited with return code {return_code}:\\n\"\n                + stderr.decode()\n            )\n\n        try:\n            # Attempt to connect\n            self.server_connection = connection.Client(self.file_path)\n            return True\n        except FileNotFoundError:\n            return False\n\n    def handle_response(self, response_type, data):\n        \"\"\"\n        Process a response recieved from the server.\n\n        This will be one of:\n        - ``RESULT`` - A pathfinding result. Call the first queued callback.\n        - ``ERROR`` - An unexpected exception was caught and the server has exited.\n          Relay the traceback to the user for debugging.\n        \"\"\"\n        if response_type == \"RESULT\":\n            # Get the first callback function and pass the result to it\n            self.callback(data)\n            del self.callback\n        elif response_type == \"ERROR\":\n            raise Exception(\"Pathfinding server encountered an exception:\\n\" + data)\n        else:\n            raise Exception(\"Received an unexpected response \" + response_type)\n\n    def pathfind(self, buffer_contents, start_view, target_view, callback):\n        \"\"\"\n        Request a pathfinding result from the server.\n\n        :param buffer_contents: List of lines we are pathfinding inside.\n        :param start_view: The start position, in the current window.\n        :param target_view: The target position, in the current window.\n        :param callback: Function to be called once a path is found. Recieves a list\n            of motions as a parameter.\n        \"\"\"\n        self.callback = callback\n\n        min_line, max_line = get_line_limits(start_view, target_view)\n        self.to_send = {\n            \"start\": start_view,\n            \"target\": target_view,\n            \"min_line\": min_line,\n            \"max_line\": max_line,\n            \"size\": (\n                # WindowTextWidth() - see plugin/dimensions.vim\n                vim.eval(\"WindowTextWidth()\"),\n                vim.eval(\"winheight(0)\"),\n            ),\n            \"buffer\": buffer_contents,\n            \"wrap\": vim.current.window.options[\"wrap\"],\n            \"scrolloff\": vim.options[\"scrolloff\"],\n            \"sidescrolloff\": vim.options[\"sidescrolloff\"],\n        }\n"
  },
  {
    "path": "pathfinder/client/explore_lines.py",
    "content": "import vim\n\n\ndef get_explore_lines(search_area_lines):\n    \"\"\"\n    :param search_area_lines: Number of lines between, and including, the start and\n        target positions.\n    :returns: Number of lines to explore either side of the search area.\n    \"\"\"\n    # Get setting values from Vim variables\n    explore_scale = float(vim.vars[\"pf_explore_scale\"])\n    max_explore = int(vim.vars[\"pf_max_explore\"])\n\n    if explore_scale < 0:\n        # This filtering is disabled, explore the entire buffer\n        return len(vim.current.buffer)\n\n    # Number of lines to explore above and below the search area is scaled based\n    # on the length of the area. This setting defaults to 0.5, if the search area\n    # was e.g. 6 lines then 3 more lines would be explored on either side.\n    explore_lines = search_area_lines * explore_scale\n    if max_explore >= 0:\n        # Limit to no more than max_explore lines\n        return min(max_explore, explore_lines)\n    else:\n        # Do not limit\n        return explore_lines\n\n\ndef get_line_limits(start_view, target_view):\n    \"\"\"\n    Return the minimum and maximum line numbers to explore.\n\n    :param start_view: The start position.\n    :param target_view: The target position.\n    :returns: Tuple of (min line, max line)\n    \"\"\"\n    min_line = min(int(start_view.lnum), int(target_view.lnum))\n    max_line = max(int(start_view.lnum), int(target_view.lnum))\n    explore_lines = get_explore_lines(max_line - min_line)\n\n    return (\n        max(1, min_line - explore_lines),\n        min(len(vim.current.buffer), max_line + explore_lines),\n    )\n"
  },
  {
    "path": "pathfinder/client/output.py",
    "content": "import itertools\n\nimport vim\n\nfrom pathfinder.debytes import debytes\n\nlast_output = None\n\n\ndef strtrans(string):\n    \"\"\"Convert special characters like '\u0004' to '^D'.\"\"\"\n    escaped_string = string.replace(\"'\", \"\\\\'\").replace(\"\\\\\", \"\\\\\\\\\")\n    return vim.eval(f\"strtrans('{escaped_string}')\")\n\n\ndef get_count(motion, count):\n    \"\"\"Build a string like 'k', 'hh', '15w'\"\"\"\n    motion_str = strtrans(motion.motion + (motion.argument or \"\"))\n    if count == 1:\n        return motion_str\n\n    elif count == 2 and len(motion_str) == 1:\n        # It's easier to press a single-character motion twice\n        # than to type a 2 before it\n        return (motion_str) * 2\n\n    return str(count) + motion_str\n\n\ndef compact_motions(motions):\n    \"\"\"\n    Return the given motion sequence in single-line form.\n\n    e.g. 2* 5j $\n    \"\"\"\n    return \" \".join(\n        [\n            get_count(motion, len(list(group)))\n            for motion, group in itertools.groupby(motions)\n        ]\n    )\n\n\ndef get_description(motion, repetitions):\n    description = debytes(vim.vars[\"pf_descriptions\"][motion.motion])\n    description = description.replace(\"{count}\", str(repetitions))\n    if motion.argument is not None:\n        description = description.replace(\"{argument}\", motion.argument)\n    return description\n\n\ndef explained_motions(motions):\n    \"\"\"\n    Yield each motion in the form \"motion <padding> help\"\n\n    e.g. ['5j   Down 5 lines', '$    To the end of the line']\n    \"\"\"\n    for motion, group in itertools.groupby(motions):\n        repetitions = len(list(group))\n        yield (\n            get_count(motion, repetitions) + \"  \" + get_description(motion, repetitions)\n        )\n"
  },
  {
    "path": "pathfinder/client/plugin.py",
    "content": "import time\n\nimport vim\n\nimport pathfinder.client.output as output\nfrom pathfinder.client.autorun import choose_action\nfrom pathfinder.client.client import Client\nfrom pathfinder.client.popup import open_popup\nfrom pathfinder.client.state_tracker import StateTracker\nfrom pathfinder.window import cursor_in_same_position\n\n\nclass Plugin:\n    def __init__(self):\n        self.client = Client()\n        self.state_tracker = StateTracker()\n        self.last_output = None\n\n    def _run(self):\n        \"\"\"Start calculating a path in the background.\"\"\"\n        self.client.pathfind(\n            self.state_tracker.start_state.lines,\n            self.state_tracker.start_state.view,\n            self.state_tracker.target_state.view,\n            self.popup,\n        )\n\n    def popup(self, motions):\n        self.last_output = motions\n        open_popup(output.compact_motions(motions))\n\n    def autorun(self):\n        \"\"\"Called on a timer several times per second.\"\"\"\n        if self.state_tracker.choose_action_using(choose_action) == \"pathfind\":\n            if not cursor_in_same_position(\n                self.state_tracker.start_state.view,\n                self.state_tracker.target_state.view,\n            ):\n                self._run()\n            self.state_tracker.reset()\n\n    def command_begin(self):\n        \"\"\"Called for the :PathfinderBegin command.\"\"\"\n        self.state_tracker.reset()\n\n    def command_run(self):\n        \"\"\"Called for the :PathfinderRun command.\"\"\"\n        self.state_tracker.set_target()\n\n        if cursor_in_same_position(\n            self.state_tracker.start_state.view,\n            self.state_tracker.target_state.view,\n        ):\n            print(\"You must move the cursor to a new location first!\")\n        else:\n            self._run()\n\n    def command_explain(self):\n        \"\"\"Called for the :PathfinderExplain command.\"\"\"\n        if self.last_output is None:\n            print(\"No suggestion to explain.\")\n        else:\n            # explained_motions yields each line\n            # sep tells print to put \\n between them rather than space\n            print(*output.explained_motions(self.last_output), sep=\"\\n\")\n\n    def stop(self):\n        \"\"\"Called when Vim is about to shut down.\"\"\"\n        self.client.close()\n"
  },
  {
    "path": "pathfinder/client/popup.py",
    "content": "import vim\n\nfrom pathfinder.debytes import debytes\n\n\ndef _neovim_popup(text, line_offset):\n    \"\"\"Create a popup using Neovim 0.4+ floating windows.\"\"\"\n    # Insert text into a scratch buffer\n    buffer = vim.api.create_buf(False, True)\n    vim.api.buf_set_lines(buffer, 0, -1, True, [f\" {text} \"])\n\n    # Create a window containing the buffer\n    window = vim.api.open_win(\n        buffer,\n        0,\n        {\n            \"relative\": \"cursor\",\n            \"row\": int(line_offset),\n            \"col\": 0,\n            \"style\": \"minimal\",\n            \"focusable\": 0,\n            \"height\": 1,\n            \"width\": len(text) + 2,\n        },\n    )\n    # Set the highlight of the window to match the cursor\n    vim.api.win_set_option(window, \"winhl\", \"Normal:PathfinderPopup\")\n\n    # Create a timer to close the window\n    popup_time = int(vim.vars[\"pf_popup_time\"])\n    vim.eval(f\"timer_start({popup_time}, {{-> nvim_win_close({window.handle}, 1)}})\")\n\n\ndef _vim_popup(text, line_offset):\n    \"\"\"Create a popup using Vim +popupwin.\"\"\"\n    vim.Function(\"popup_create\")(\n        text,\n        {\n            \"line\": f\"cursor{line_offset}\",\n            \"col\": \"cursor\",\n            \"wrap\": False,\n            \"padding\": (0, 1, 0, 1),\n            \"highlight\": \"PathfinderPopup\",\n            \"time\": int(vim.vars[\"pf_popup_time\"]),\n            \"zindex\": 1000,\n        },\n    )\n\n\ndef open_popup(text):\n    line_offset = \"+1\" if vim.eval(\"line('.')\") == \"1\" else \"-1\"\n\n    if vim.eval(\"has('nvim-0.4')\") == \"1\":\n        _neovim_popup(text, line_offset)\n    elif vim.eval(\"has('popupwin')\") == \"1\":\n        _vim_popup(text, line_offset)\n    else:\n        # Not able to create a popup\n        print(text)\n"
  },
  {
    "path": "pathfinder/client/state_tracker.py",
    "content": "import time\nfrom collections import namedtuple\n\nimport vim\n\nfrom pathfinder.window import winsaveview\n\nState = namedtuple(\"State\", \"view mode buffer window lines\")\n\n\nclass StateTracker:\n    def __init__(self):\n        self.reset()\n\n    def _set_update_time(self):\n        self.update_time = time.time()\n\n    def _record_state(self):\n        return State(\n            winsaveview(),\n            vim.eval(\"mode()\"),\n            vim.current.buffer.number,\n            vim.current.window.number,\n            vim.current.buffer[:],\n        )\n\n    def reset(self):\n        current_state = self._record_state()\n        self._reset(current_state)\n\n    def _reset(self, new_state):\n        self.start_state = new_state\n        self.target_state = new_state\n        self._set_update_time()\n\n    def set_target(self):\n        current_state = self._record_state()\n        self._set_target(current_state)\n\n    def _set_target(self, new_state):\n        if new_state != self.target_state:\n            self.target_state = new_state\n            self._set_update_time()\n\n    def choose_action_using(self, function):\n        \"\"\"\n        Choose an action to take using the given function.\n\n        Function will be called with the arguments (start state, current state,\n        time of most recent update). It may return \"reset\" or \"set_target\" to\n        call the corresponding method, or any other value to do nothing.  The\n        function's return value is passed through this method, so can be used\n        to take further actions elsewhere.\n        \"\"\"\n        current_state = self._record_state()\n\n        result = function(self.start_state, current_state, self.update_time)\n        if result == \"reset\":\n            self._reset(current_state)\n        elif result == \"set_target\":\n            self._set_target(current_state)\n\n        return result\n"
  },
  {
    "path": "pathfinder/debytes.py",
    "content": "def debytes(string):\n    \"\"\"\n    Decode string if it is a bytes object.\n\n    This is necessary since Neovim, correctly, gives strings as a str, but regular\n    Vim leaves them encoded as bytes.\n    \"\"\"\n    try:\n        return string.decode()\n    except AttributeError:\n        return string\n"
  },
  {
    "path": "pathfinder/server/__init__.py",
    "content": ""
  },
  {
    "path": "pathfinder/server/dijkstra.py",
    "content": "from heapdict import heapdict\n\nfrom pathfinder.server.motions.find import FindMotionGenerator\nfrom pathfinder.server.motions.search import SearchMotionGenerator\nfrom pathfinder.server.motions.simple import SimpleMotionGenerator\nfrom pathfinder.server.node import Node\n\n\nclass Dijkstra:\n    \"\"\"\n    A path between a start and end point in the same window.\n\n    :param from_view: View of the start point\n    :param target_view: View of the target point\n    :param min_line: Do not explore nodes above this line\n    :param max_line: Do not explore nodes below this line\n    \"\"\"\n\n    def __init__(self, from_view, target_view, min_line, max_line):\n        self.from_view = from_view\n        self.target_view = target_view\n        self.min_line = min_line\n        self.max_line = max_line\n\n        self.motion_generators = {\n            SimpleMotionGenerator(self),\n            FindMotionGenerator(self),\n            SearchMotionGenerator(self),\n        }\n\n        self._open_queue = heapdict()  # Min-priority queue: Key -> Distance\n        self._open_nodes = dict()  # Key -> Node\n        self._closed_nodes = set()  # Key\n\n        start_node = Node(self, self.from_view, None)\n        self._open_queue[start_node.key] = 0\n        self._open_nodes[start_node.key] = start_node\n\n    def find_path(self, client_connection):\n        \"\"\"\n        Use Dijkstra's algorithm to find the optimal sequence of motions.\n\n        :param client_connection: If another pathfinding request is waiting on this\n            connection, exit (returning None) as soon as possible. This cancels the\n            pathfinding, moving on to the new request immediately.\n        \"\"\"\n        while len(self._open_queue) > 0 and not client_connection.poll():\n            current_node_key, current_distance = self._open_queue.popitem()\n            current_node = self._open_nodes.pop(current_node_key)\n            self._closed_nodes.add(current_node_key)\n\n            if current_node.is_target():\n                return current_node.reconstruct_path()\n\n            for node in current_node.get_neighbours():\n                if node.key in self._closed_nodes:\n                    continue\n\n                new_distance = current_distance + current_node.motion_weight(\n                    node.came_by_motion\n                )\n                if (\n                    node.key not in self._open_nodes\n                    or new_distance < self._open_queue[node.key]\n                ):\n                    node.set_came_from(current_node)\n                    self._open_nodes[node.key] = node\n                    self._open_queue[node.key] = new_distance\n"
  },
  {
    "path": "pathfinder/server/motions/__init__.py",
    "content": "import abc\nfrom collections import namedtuple\n\nfrom pathfinder.server.node import Node\n\n# motion - The motion such as h,j,k,f,T,gM\n# argument - Used for the additional argument to f,t,/ etc\nMotion = namedtuple(\"Motion\", \"motion argument\")\n\n\nclass MotionGenerator(abc.ABC):\n    def __init__(self, dijkstra):\n        self.dijkstra = dijkstra\n\n    @abc.abstractmethod\n    def generate(self, view):\n        \"\"\"Yield all neighbouring nodes found from the given view.\"\"\"\n        pass\n\n    def _create_node(self, *args, **kwargs):\n        return Node(self.dijkstra, *args, **kwargs)\n"
  },
  {
    "path": "pathfinder/server/motions/find.py",
    "content": "import vim\n\nfrom pathfinder.server.motions import Motion, MotionGenerator\n\n\nclass FindMotionGenerator(MotionGenerator):\n    MOTIONS = {\"f\", \"t\", \"F\", \"T\"}\n\n    def generate(self, view):\n        for motion in self.MOTIONS:\n            yield from self._find(view, motion)\n\n    def _find(self, view, motion):\n        line_text = vim.current.buffer[view.lnum - 1]\n        seen_characters = set()\n\n        # characters = string of characters which may be accessible using this motion\n        # column = lambda function which converts index in `characters` to a column number\n        if motion == \"f\" and view.col < len(line_text):\n            column = lambda i: view.col + i + 1\n            characters = line_text[view.col + 1 :]\n        elif motion == \"t\" and view.col < len(line_text) - 1:\n            column = lambda i: view.col + i + 1\n            characters = line_text[view.col + 2 :]\n            seen_characters.add(line_text[view.col + 1])\n        elif motion == \"F\" and view.col > 0 and len(line_text) > view.col:\n            column = lambda i: view.col - i - 1\n            # Characters are reversed because we are looking backwards\n            characters = line_text[: view.col][::-1]\n        elif motion == \"T\" and view.col > 1 and len(line_text) > view.col:\n            column = lambda i: view.col - i - 1\n            characters = line_text[: view.col - 1][::-1]\n            seen_characters.add(line_text[view.col - 1])\n        else:\n            return\n\n        for i, character in enumerate(characters):\n            # Only use each unique character once\n            if character in seen_characters:\n                continue\n            seen_characters.add(character)\n\n            new_col = column(i)\n            new_view = view._replace(col=new_col, curswant=new_col)\n            yield self._create_node(new_view, Motion(motion, character))\n"
  },
  {
    "path": "pathfinder/server/motions/search.py",
    "content": "import re\n\nimport vim\n\nfrom pathfinder.server.motions import Motion, MotionGenerator\n\n\nclass SearchMotionGenerator(MotionGenerator):\n    def generate(self, view):\n        # Only return results from the starting node\n        if view != self.dijkstra.from_view:\n            return\n\n        motion = self._search_lines(\n            vim.current.buffer[:],\n            view.lnum - 1,\n            view.col,\n            self.dijkstra.target_view.lnum - 1,\n            self.dijkstra.target_view.col,\n        )\n        if motion:\n            yield self._create_node(self.dijkstra.target_view, motion)\n\n    def _create_motion(self, search_query, motion=\"/\"):\n        return Motion(motion, self._escape_magic(search_query))\n\n    def _escape_magic(self, search_query):\n        \"\"\"Add backslash escapes to any \"magic\" characters in a query.\"\"\"\n        for char in r\"\\^$.*[~/\":\n            search_query = search_query.replace(char, \"\\\\\" + char)\n        return search_query\n\n    def _search(self, text, start, target):\n        \"\"\"\n        Return the simplest possible searching motion to reach the given target.\n\n        :param text: Contents of the file.\n        :param start: Index in ``text`` to start the search from.\n        :param target: Index of the target position in ``text``.\n        \"\"\"\n        search_text = text[target:]\n\n        # (\"a\", \"ab\", \"abc\", \"abcd\"...) until we reach\n        # the end of search_text or find a working query\n        for query_length in range(1, len(search_text) + 1):\n            query = search_text[:query_length]\n\n            # Get a list of all match positions for this search query\n            # query=\"x\" text=\"x___x_xx\" == [0, 4, 6, 7]\n            pattern = re.escape(query)\n            matches = [m.start() for m in re.finditer(pattern, text)]\n\n            if matches:\n                # Sort the list so it begins with matches after `start`, rather\n                # than matches at the beginning of the file\n                # sorted([True, False]) == [False, True]\n                matches.sort(key=lambda position: position <= start)\n\n                if matches[0] == target:\n                    return self._create_motion(query)\n                if matches[-1] == target:\n                    return self._create_motion(query, \"?\")\n\n    def _search_lines(self, lines, start_line, start_col, target_line, target_col):\n        \"\"\"\n        Wrapper around _search which handles 2d coordinates and a list of lines.\n\n        :param lines: List of lines.\n        :param start_line: Starting line, indexed from 0.\n        :param start_col: Starting column.\n        :param target_line: Target line, indexed from 0.\n        :param target_col: Target column.\n        \"\"\"\n        text = \"\\n\".join(lines)\n        start = sum(len(line) + 1 for line in lines[:start_line]) + start_col\n        target = sum(len(line) + 1 for line in lines[:target_line]) + target_col\n        return self._search(text, start, target)\n"
  },
  {
    "path": "pathfinder/server/motions/simple.py",
    "content": "import vim\n\nfrom pathfinder.server.motions import Motion, MotionGenerator\nfrom pathfinder.window import winrestview, winsaveview\n\n\nclass SimpleMotionGenerator(MotionGenerator):\n    MOTIONS = {\n        \"h\",\n        \"j\",\n        \"k\",\n        \"l\",\n        \"gj\",\n        \"gk\",\n        \"gg\",\n        \"G\",\n        \"H\",\n        \"M\",\n        \"L\",\n        \"\u0005\",\n        \"\u0019\",\n        \"\u0006\",\n        \"\u0002\",\n        \"\u0004\",\n        \"\u0015\",\n        \"zt\",\n        \"z\\\n\",\n        \"z.\",\n        \"zb\",\n        \"z-\",\n        \"0\",\n        \"^\",\n        \"g^\",\n        \"$\",\n        \"g$\",\n        \"g_\",\n        \"gm\",\n        \"gM\",\n        \"W\",\n        \"E\",\n        \"B\",\n        \"gE\",\n        \"w\",\n        \"e\",\n        \"b\",\n        \"ge\",\n        \"(\",\n        \")\",\n        \"{\",\n        \"}\",\n        \"]]\",\n        \"][\",\n        \"[[\",\n        \"[]\",\n        \"]m\",\n        \"[m\",\n        \"]M\",\n        \"[M\",\n        \"*\",\n        \"#\",\n        \"g*\",\n        \"g#\",\n        \"%\",\n    }\n\n    def generate(self, view):\n        for motion in self.MOTIONS:\n            result_view = self._try_motion(view, motion)\n            if result_view is not None and result_view != view:\n                yield self._create_node(result_view, Motion(motion, None))\n\n    def _try_motion(self, view, motion):\n        \"\"\"\n        Use a motion inside Vim, starting from the given view.\n\n        If the motion causes an error, return None.\n        \"\"\"\n        winrestview(view)\n        try:\n            vim.command(f\"silent! normal! {motion}\")\n        except:\n            return None\n        return winsaveview()\n"
  },
  {
    "path": "pathfinder/server/node.py",
    "content": "from pathfinder.window import cursor_in_same_position\n\n\nclass Node:\n    \"\"\"Graph node linked to a view (cursor+scroll location) within the document.\"\"\"\n\n    def __init__(self, dijkstra, view, came_by_motion):\n        self.dijkstra = dijkstra\n        self.view = view\n        self.came_by_motion = came_by_motion\n\n        self.key = (view, came_by_motion)\n\n        self.came_from = None\n        self.came_by_motion_repetitions = 1\n\n    def get_neighbours(self):\n        \"\"\"Yield all neighbours of this node.\"\"\"\n        for motion_generator in self.dijkstra.motion_generators:\n            for node in motion_generator.generate(self.view):\n                if (\n                    node.view.lnum >= self.dijkstra.min_line\n                    and node.view.lnum <= self.dijkstra.max_line\n                ):\n                    yield node\n\n    def motion_weight(self, motion):\n        \"\"\"Return the weight of using a motion from this node.\"\"\"\n        if motion != self.came_by_motion:\n            # First repetition, return number of characters in the motion\n            return len(motion.motion) + (\n                0 if motion.argument is None else len(motion.argument)\n            )\n        elif self.came_by_motion_repetitions == 1:\n            # Second repetition, adding a \"2\" is 1 extra character\n            return 1\n        else:\n            # Difference in length of current and future count\n            # 2j -> 3j = 0\n            # 9j -> 10j = 1\n            return len(str(self.came_by_motion_repetitions + 1)) - len(\n                str(self.came_by_motion_repetitions)\n            )\n\n    def set_came_from(self, node):\n        \"\"\"Set the node this node was reached from.\"\"\"\n        self.came_from = node\n\n        if node.came_by_motion == self.came_by_motion:\n            self.came_by_motion_repetitions = node.came_by_motion_repetitions + 1\n        else:\n            self.came_by_motion_repetitions = 1\n\n    def is_target(self):\n        return cursor_in_same_position(self.view, self.dijkstra.target_view)\n\n    def reconstruct_path(self):\n        \"\"\"Return the sequence of motions used to reach this node.\"\"\"\n        motions = list()\n        node = self\n        while node.came_from is not None:\n            motions.insert(0, node.came_by_motion)\n            node = node.came_from\n        return motions\n"
  },
  {
    "path": "pathfinder/server/server.py",
    "content": "import traceback\nfrom multiprocessing import connection\n\nimport vim\n\nfrom pathfinder.debytes import debytes\nfrom pathfinder.server.dijkstra import Dijkstra\n\n\nclass Server:\n    \"\"\"\n    Local server which runs inside an instance of Vim in a separate process.\n\n    This is used to test motions in the same environment as the user, but without\n    blocking their Vim or moving their cursor. This allows pathfinding to happen in\n    the background while the user may continue working.\n\n    The server's Vim is quitted as soon as the client (the user's Vim) disconnects,\n    which happens when it is closed. It will also exit if an exception is caught,\n    relaying the traceback message to the client beforehand so that it can be displayed\n    for debugging.\n    \"\"\"\n\n    def __init__(self, file_path):\n        self.listener = connection.Listener(debytes(file_path))\n\n    def run(self):\n        try:\n            # Wait for the user's Vim to connect\n            self.client_connection = self.listener.accept()\n            # Continuously process jobs until EOFError is raised\n            # (when the client disconnects)\n            self.message_loop()\n        except EOFError:\n            pass\n        finally:\n            self.listener.close()\n            # Exit the background Vim since it is no longer needed\n            vim.command(\"qa!\")\n\n    def message_loop(self):\n        \"\"\"\n        Continuously wait for and handle instructions from the client.\n\n        This waiting blocks Vim, but that does not matter since nobody is looking at\n        it. Blocking also prevents CPU resources from being wasted on redrawing.\n\n        :raises EOFError: when the connection is closed.\n        \"\"\"\n        while True:\n            try:\n                data = self.client_connection.recv()\n\n                # If there is still data waiting, then multiple requests were sent,\n                # so we skip pathfinding and move on to the next one\n                if not self.client_connection.poll():\n                    self.do_action(data)\n            except:\n                # Send any unexpected exceptions back to the client\n                # to be displayed for debugging purposes\n                self.client_connection.send((\"ERROR\", traceback.format_exc()))\n\n    def do_action(self, data):\n        \"\"\"Process an instruction from the client.\"\"\"\n        self.start_view = data[\"start\"]\n        self.target_view = data[\"target\"]\n        self.min_line = data[\"min_line\"]\n        self.max_line = data[\"max_line\"]\n\n        vim.current.buffer[:] = data[\"buffer\"]\n\n        vim.current.window.options[\"wrap\"] = data[\"wrap\"]\n        vim.options[\"scrolloff\"] = data[\"scrolloff\"]\n        vim.options[\"sidescrolloff\"] = data[\"sidescrolloff\"]\n\n        # Set size of the entire Vim display to match the size of the\n        # corresponding window in the client\n        vim.options[\"columns\"] = int(data[\"size\"][0])\n        vim.options[\"lines\"] = vim.options[\"cmdheight\"] + int(data[\"size\"][1])\n\n        self.pathfind()\n\n    def pathfind(self):\n        \"\"\"Run the pathfinder, then send the result back to the client.\"\"\"\n        dijkstra = Dijkstra(\n            self.start_view, self.target_view, self.min_line, self.max_line\n        )\n        motions = dijkstra.find_path(self.client_connection)\n\n        # If motions is None, that means we cancelled pathfinding because a new\n        # request was received. We also check for another request now in case one was\n        # sent during the last iteration of the pathfinding loop.\n        if not (motions is None or self.client_connection.poll()):\n            self.client_connection.send((\"RESULT\", motions))\n\n\nserver = Server(vim.vars[\"pf_server_communication_file\"])\nserver.run()\n"
  },
  {
    "path": "pathfinder/window.py",
    "content": "from collections import namedtuple\n\nimport vim\n\nView = namedtuple(\"View\", \"lnum col curswant leftcol topline\")\n\n\ndef winsaveview():\n    view_dict = vim.eval(\"winsaveview()\")\n    # Any dictionary elements not in View._fields will be discarded\n    return View._make(int(view_dict[field]) for field in View._fields)\n\n\ndef winrestview(view):\n    view_dict = dict(view._asdict())\n    vim.eval(f\"winrestview({view_dict})\")\n\n\ndef cursor_in_same_position(a, b):\n    \"\"\"\n    Check if the given views have the cursor on the same position.\n\n    The scroll position and other properties may differ.\n    \"\"\"\n    return a.lnum == b.lnum and a.col == b.col\n"
  },
  {
    "path": "plugin/defaults.vim",
    "content": "highlight default link PathfinderPopup Cursor\n\nif !exists('g:pf_autorun_delay')\n  let g:pf_autorun_delay = 2\nendif\n\nif !exists('g:pf_popup_time')\n  let g:pf_popup_time = 3000\nendif\n\nif !exists('g:pf_explore_scale')\n  let g:pf_explore_scale = 0.5\nendif\nif !exists('g:pf_max_explore')\n  let g:pf_max_explore = 10\nendif\n\nif !exists('g:pf_descriptions')\n  let g:pf_descriptions = {\n    \\ 'h': 'Left {count} columns',\n    \\ 'l': 'Right {count} columns',\n    \\ 'j': 'Down {count} lines',\n    \\ 'k': 'Up {count} lines',\n    \\ 'gj': 'Down {count} display lines',\n    \\ 'gk': 'Up {count} display lines',\n    \\ 'gg': 'To the start of the buffer',\n    \\ 'G': 'To the end of the buffer',\n    \\ 'H': 'To line {count} from the top of the window',\n    \\ 'M': 'To the middle of the window',\n    \\ 'L': 'To line {count} from the bottom of the window',\n    \\ '\u0005': 'Scroll downward {count} lines',\n    \\ '\u0019': 'Scroll upward {count} lines',\n    \\ '\u0006': 'Scroll forward {count} pages',\n    \\ '\u0002': 'Scroll backward {count} pages',\n    \\ '\u0004': 'Scroll downward {count} times',\n    \\ '\u0015': 'Scroll upward {count} times',\n    \\ 'zt': 'Current line to the top of the window',\n    \\ 'z\\\r': 'Current line to the top of the window and move to the first non-blank character',\n    \\ 'zz': 'Current line to the centre of the window',\n    \\ 'z.': 'Current line to the centre of the window and move to the first non-blank character',\n    \\ 'zb': 'Current line to the bottom of the window',\n    \\ 'z-': 'Current line to the bottom of the window and move to the first non-blank character',\n    \\ '0': 'To the start of the line',\n    \\ '^': 'To the first non-blank character on the line',\n    \\ 'g^': 'To the first non-blank character on the display line',\n    \\ '$': 'To the end of the line',\n    \\ 'g$': 'To the end of the display line',\n    \\ 'g_': 'To the last non-blank character on the line',\n    \\ 'gm': 'To the centre of the screen',\n    \\ 'gM': 'To the centre of the line',\n    \\ 'W': '{count} WORDs forward (inclusive)',\n    \\ 'E': 'Forward to the end of WORD {count} (exclusive)',\n    \\ 'B': '{count} WORDs backward (inclusive)',\n    \\ 'gE': 'Backward to the end of WORD {count} (exclusive)',\n    \\ 'w': '{count} words forward (inclusive)',\n    \\ 'e': 'Forward to the end of word {count} (exclusive)',\n    \\ 'b': '{count} words backward (inclusive)',\n    \\ 'ge': 'Backward to the end of word {count} (exclusive)',\n    \\ '(': '{count} sentences backward',\n    \\ ')': '{count} sentences forward',\n    \\ '{': '{count} paragraphs backward',\n    \\ '}': '{count} paragraphs forward',\n    \\ ']]': '{count} sections forward or to the next { in the first column',\n    \\ '][': '{count} sections forward or to the next } in the first column',\n    \\ '[[': '{count} sections backward or to the previous { in the first column',\n    \\ '[]': '{count} sections backward or to the previous } in the first column',\n    \\ ']m': '{count} next start of a method (Java or similar)',\n    \\ '[m': '{count} previous start of a method (Java or similar)',\n    \\ ']M': '{count} next end of a method (Java or similar)',\n    \\ '[M': '{count} previous end of a method (Java or similar)',\n    \\ '*': 'Search forward for occurrence {count} of the word nearest to the cursor',\n    \\ '#': 'Search backward for occurrence {count} of the word nearest to the cursor',\n    \\ 'g*': 'Search forward for occurrence {count} of the word nearest to the cursor, allowing matches which are not a whole word',\n    \\ 'g#': 'Search backward for occurrence {count} of the word nearest to the cursor, allowing matches which are not a whole word',\n    \\ '%': 'Go to the matching bracket',\n    \\ 'f': 'To occurrence {count} of \"{argument}\", to the right',\n    \\ 't': 'Till before occurrence {count} of \"{argument}\", to the right',\n    \\ 'F': 'To occurrence {count} of \"{argument}\", to the left',\n    \\ 'T': 'Till after occurrence {count} of \"{argument}\", to the left',\n    \\ '/': 'Search forwards for occurence {count} of \"{argument}\"',\n    \\ '?': 'Search backwards for occurence {count} of \"{argument}\"',\n    \\ }\nendif\n"
  },
  {
    "path": "plugin/dimensions.vim",
    "content": "\" Get the width of the current window, excluding sign and number columns\nfunction! WindowTextWidth()\n  let l:decorationColumns = &number ? &numberwidth : 0\n\n  if has('folding')\n    let l:decorationColumns += &foldcolumn\n  endif\n\n  if has('signs')\n    if &signcolumn ==? 'yes' || &signcolumn ==? 'number'\n      \" Sign column is always enabled\n      let l:decorationColumns += 2\n    elseif &signcolumn ==? 'auto'\n      redir => l:signs\n      silent execute 'sign place buffer=' . bufnr('')\n      redir END\n\n      \" The output of the command above contains two header lines\n      \" Any more and that means signs are displayed\n      if len(split(l:signs, \"\\n\")) > 2\n\t  let l:decorationColumns += 2\n      endif\n    endif\n  endif\n\n  return winwidth(0) - l:decorationColumns\nendfunction\n"
  },
  {
    "path": "plugin/main.vim",
    "content": "if exists('g:pf_loaded') | finish | endif\nif !has('python3')\n  echom 'The +python3 feature is required to run pathfinder.vim'\n  finish\nendif\nif !has('timers')\n  echom 'The +timers feature is required to run pathfinder.vim'\n  finish\nendif\n\n\npython3 << endpython\nimport vim\nimport sys\nfrom os.path import normpath, join\n\nplugin_root = vim.eval(\"fnamemodify(resolve(expand('<sfile>:p')), ':h')\")\nsys.path.insert(0, normpath(join(plugin_root, '..')))\nsys.path.insert(0, normpath(join(plugin_root, '..', 'heapdict')))\nendpython\n\n\nif exists('g:pf_server_communication_file')\n  \" Importing this will run the server and connect back to the client\n  python3 import pathfinder.server.server\nelse\n  python3 from pathfinder.client.plugin import Plugin; plugin = Plugin()\n\n  command! PathfinderBegin python3 plugin.command_begin()\n  command! PathfinderRun python3 plugin.command_run()\n  command! PathfinderExplain python3 plugin.command_explain()\n\n  function! PathfinderLoop(timer)\n    \" Check for responses from the server\n    python3 plugin.client.poll_responses()\n    \" Check if we should take any actions automatically\n    python3 plugin.autorun()\n  endfunction\n\n  if !exists(\"s:timer\")\n    let s:timer = timer_start(100, 'PathfinderLoop', {'repeat': -1})\n  endif\n  augroup PathfinderStopOnLeave\n    autocmd!\n    autocmd VimLeave * call timer_stop(s:timer)\n    autocmd VimLeave * python3 plugin.stop()\n  augroup END\nendif\n\n\nlet g:pf_loaded = 1\n"
  },
  {
    "path": "serverrc.vim",
    "content": "set nocompatible\n\n\" Disable unnecessary features\nset noruler\nset nonumber\nset noshowcmd\nset signcolumn=no\nfiletype off\nsyntax off\n\n\" Add pathfinder.vim to runtimepath\nlet &runtimepath .= ',' . escape(expand('<sfile>:p:h'), '\\,')\n"
  },
  {
    "path": "setup.cfg",
    "content": "[semantic_release]\nversion_variable=setup.py:__version__\nchangelog_components=semantic_release.changelog.changelog_table\ncommit_subject=release: v{version}\nupload_to_pypi=false\nbuild_command=python setup.py sdist --formats=gztar,zip\n\n[isort]\ndefault_section=THIRDPARTY\nknown_third_party=vim\nknown_first_party=pathfinder\norder_by_type=True\ncombine_star=True\nreverse_relative=True\nmulti_line_output=3\ninclude_trailing_comma=True\nforce_grid_wrap=0\nuse_parentheses=True\nline_length=88\n"
  },
  {
    "path": "setup.py",
    "content": "from setuptools import find_packages, setup\n\n__version__ = \"3.1.3\"\n\n\nsetup(\n    name=\"pathfinder.vim\",\n    version=__version__,\n    author=\"Daniel Thwaites\",\n    author_email=\"danthwaites30@btinternet.com\",\n    url=\"https://github.com/danth/pathfinder.vim\",\n    packages=find_packages(exclude=(\"tests\",)),\n)\n"
  },
  {
    "path": "test_requirements.txt",
    "content": "pytest >=5,<6\npytest-stub >=1,<2\n"
  },
  {
    "path": "tests/__init__.py",
    "content": ""
  },
  {
    "path": "tests/conftest.py",
    "content": "from pytest_stub.toolbox import stub_global\n\nstub_global(\n    {\n        \"vim\": \"[mock_persist]\",\n    }\n)\n"
  },
  {
    "path": "tests/test_autorun.py",
    "content": "import time\nfrom unittest import mock\n\nimport pytest\n\nfrom pathfinder.client.autorun import choose_action\nfrom pathfinder.client.state_tracker import State\nfrom pathfinder.window import View\n\n\n@pytest.fixture\ndef default_state():\n    return State(View(0, 0, 0, 0, 0), \"n\", 1, 1, [\"hello world\"])\n\n\ndef test_choose_action_new_window(default_state):\n    assert (\n        choose_action(\n            default_state._replace(window=1),\n            default_state._replace(window=2),\n            time.time(),\n        )\n        == \"reset\"\n    )\n\n\n@mock.patch(\"pathfinder.client.autorun.vim.vars\", {\"pf_autorun_delay\": 1})\ndef test_choose_action_non_motion_mode(default_state):\n    assert (\n        choose_action(\n            default_state._replace(mode=\"i\"),\n            default_state._replace(mode=\"i\"),\n            time.time(),\n        )\n        == \"reset\"\n    )\n\n\n@mock.patch(\"pathfinder.client.autorun.vim.vars\", {\"pf_autorun_delay\": 1})\ndef test_choose_action_changed_mode(default_state):\n    assert (\n        choose_action(\n            default_state._replace(mode=\"n\"),\n            default_state._replace(mode=\"i\"),\n            time.time(),\n        )\n        == \"pathfind\"\n    )\n\n\n@mock.patch(\"pathfinder.client.autorun.vim.vars\", {\"pf_autorun_delay\": 1})\ndef test_choose_action_changed_lines(default_state):\n    assert (\n        choose_action(\n            default_state._replace(lines=[\"foo\"]),\n            default_state._replace(lines=[\"bar\"]),\n            time.time(),\n        )\n        == \"pathfind\"\n    )\n\n\n@mock.patch(\"pathfinder.client.autorun.vim.vars\", {\"pf_autorun_delay\": 1})\ndef test_choose_action_cursor_idle(default_state):\n    assert (\n        choose_action(\n            default_state,\n            default_state,\n            time.time() - 2,\n        )\n        == \"pathfind\"\n    )\n\n\n@mock.patch(\"pathfinder.client.autorun.vim.vars\", {\"pf_autorun_delay\": 1})\ndef test_choose_action_set_target(default_state):\n    assert (\n        choose_action(\n            default_state,\n            default_state,\n            time.time(),\n        )\n        == \"set_target\"\n    )\n\n\n@mock.patch(\"pathfinder.client.autorun.vim.vars\", {\"pf_autorun_delay\": 0})\ndef test_choose_action_do_nothing(default_state):\n    assert (\n        choose_action(\n            default_state,\n            default_state,\n            time.time(),\n        )\n        is None\n    )\n"
  },
  {
    "path": "tests/test_debytes.py",
    "content": "from pathfinder.debytes import debytes\n\n\ndef test_debytes_bytes():\n    assert debytes(b\"hello world\") == \"hello world\"\n\n\ndef test_debytes_str():\n    assert debytes(\"hello world\") == \"hello world\"\n"
  },
  {
    "path": "tests/test_explore_lines.py",
    "content": "from collections import namedtuple\nfrom unittest import mock\n\nfrom pathfinder.client.explore_lines import get_explore_lines, get_line_limits\n\n\ndef test_get_explore_lines():\n    with mock.patch(\n        \"pathfinder.client.explore_lines.vim.vars\",\n        {\"pf_explore_scale\": 0.5, \"pf_max_explore\": 10},\n    ):\n        assert get_explore_lines(10) == 5\n\n\ndef test_get_explore_lines_max_0():\n    with mock.patch(\n        \"pathfinder.client.explore_lines.vim.vars\",\n        {\"pf_explore_scale\": 0.5, \"pf_max_explore\": 0},\n    ):\n        assert get_explore_lines(10) == 0\n\n\ndef test_get_explore_lines_scale_0():\n    with mock.patch(\n        \"pathfinder.client.explore_lines.vim.vars\",\n        {\"pf_explore_scale\": 0, \"pf_max_explore\": 10},\n    ):\n        assert get_explore_lines(10) == 0\n\n\n@mock.patch(\"pathfinder.client.explore_lines.get_explore_lines\", return_value=0)\ndef test_get_line_limits(mock_get_explore_lines):\n    View = namedtuple(\"View\", \"lnum\")\n    with mock.patch(\n        \"pathfinder.client.explore_lines.vim.current.buffer\", [\"line\"] * 10\n    ):\n        assert get_line_limits(View(2), View(8)) == (2, 8)\n        assert mock_get_explore_lines.called_once_with(6)\n\n\n@mock.patch(\"pathfinder.client.explore_lines.get_explore_lines\", return_value=2)\ndef test_get_line_limits_with_explore(mock_get_explore_lines):\n    View = namedtuple(\"View\", \"lnum\")\n    with mock.patch(\n        \"pathfinder.client.explore_lines.vim.current.buffer\", [\"line\"] * 10\n    ):\n        assert get_line_limits(View(4), View(6)) == (2, 8)\n        assert mock_get_explore_lines.called_once_with(4)\n"
  },
  {
    "path": "tests/test_motions_find.py",
    "content": "from collections import namedtuple\nfrom unittest import mock\n\nfrom pathfinder.server.motions import Motion\nfrom pathfinder.server.motions.find import FindMotionGenerator\n\nView = namedtuple(\"View\", \"lnum col curswant\")\n\n\n@mock.patch(\"pathfinder.server.motions.find.vim.current.buffer\", [\"abcdde\"])\n@mock.patch.object(FindMotionGenerator, \"_create_node\", new=lambda self, v, m: (v, m))\ndef test_find_f():\n    generator = FindMotionGenerator(None)\n    output = list(generator._find(View(1, 0, 0), \"f\"))\n    assert output == [\n        (View(1, 1, 1), Motion(\"f\", \"b\")),\n        (View(1, 2, 2), Motion(\"f\", \"c\")),\n        (View(1, 3, 3), Motion(\"f\", \"d\")),\n        (View(1, 5, 5), Motion(\"f\", \"e\")),\n    ]\n\n\n@mock.patch(\"pathfinder.server.motions.find.vim.current.buffer\", [\"abcdde\"])\ndef test_find_f_final_column():\n    generator = FindMotionGenerator(None)\n    output = list(generator._find(View(1, 5, 5), \"f\"))\n    assert len(output) == 0\n\n\n@mock.patch(\"pathfinder.server.motions.find.vim.current.buffer\", [\"abcdde\"])\n@mock.patch.object(FindMotionGenerator, \"_create_node\", new=lambda self, v, m: (v, m))\ndef test_find_t():\n    generator = FindMotionGenerator(None)\n    output = list(generator._find(View(1, 0, 0), \"t\"))\n    assert output == [\n        (View(1, 1, 1), Motion(\"t\", \"c\")),\n        (View(1, 2, 2), Motion(\"t\", \"d\")),\n        (View(1, 4, 4), Motion(\"t\", \"e\")),\n    ]\n\n\n@mock.patch(\"pathfinder.server.motions.find.vim.current.buffer\", [\"abcdde\"])\ndef test_find_t_penultimate_column():\n    generator = FindMotionGenerator(None)\n    output = list(generator._find(View(1, 4, 4), \"t\"))\n    assert len(output) == 0\n\n\n@mock.patch(\"pathfinder.server.motions.find.vim.current.buffer\", [\"abcdde\"])\n@mock.patch.object(FindMotionGenerator, \"_create_node\", new=lambda self, v, m: (v, m))\ndef test_find_F():\n    generator = FindMotionGenerator(None)\n    output = list(generator._find(View(1, 5, 5), \"F\"))\n    assert output == [\n        (View(1, 4, 4), Motion(\"F\", \"d\")),\n        (View(1, 2, 2), Motion(\"F\", \"c\")),\n        (View(1, 1, 1), Motion(\"F\", \"b\")),\n        (View(1, 0, 0), Motion(\"F\", \"a\")),\n    ]\n\n\n@mock.patch(\"pathfinder.server.motions.find.vim.current.buffer\", [\"abcdde\"])\ndef test_find_F_first_column():\n    generator = FindMotionGenerator(None)\n    output = list(generator._find(View(1, 0, 0), \"F\"))\n    assert len(output) == 0\n\n\n@mock.patch(\"pathfinder.server.motions.find.vim.current.buffer\", [\"abcdde\"])\n@mock.patch.object(FindMotionGenerator, \"_create_node\", new=lambda self, v, m: (v, m))\ndef test_find_T():\n    generator = FindMotionGenerator(None)\n    output = list(generator._find(View(1, 5, 5), \"T\"))\n    assert output == [\n        (View(1, 3, 3), Motion(\"T\", \"c\")),\n        (View(1, 2, 2), Motion(\"T\", \"b\")),\n        (View(1, 1, 1), Motion(\"T\", \"a\")),\n    ]\n\n\n@mock.patch(\"pathfinder.server.motions.find.vim.current.buffer\", [\"abcdde\"])\ndef test_find_T_second_column():\n    generator = FindMotionGenerator(None)\n    output = list(generator._find(View(1, 1, 1), \"T\"))\n    assert len(output) == 0\n"
  },
  {
    "path": "tests/test_motions_search.py",
    "content": "from unittest import mock\n\nfrom pathfinder.server.motions import Motion\nfrom pathfinder.server.motions.search import SearchMotionGenerator\n\n\ndef test_escape_magic():\n    assert (\n        SearchMotionGenerator(None)._escape_magic(r\"x\\x^x$x.x*x[x~x/x\")\n        == r\"x\\\\x\\^x\\$x\\.x\\*x\\[x\\~x\\/x\"\n    )\n\n\ndef test_search_finds_shortest_possible_query():\n    generator = SearchMotionGenerator(None)\n    assert generator._search(\"abcde\", 0, 2) == Motion(\"/\", \"c\")\n    assert generator._search(\"abcbcdebbc\", 0, 3) == Motion(\"/\", \"bcd\")\n\n\ndef test_search_when_target_is_before_start():\n    generator = SearchMotionGenerator(None)\n    assert generator._search(\"abcde\", 2, 0) == Motion(\"/\", \"a\")\n    assert generator._search(\"bcdabc\", 3, 0) == Motion(\"?\", \"b\")\n\n\n@mock.patch.object(SearchMotionGenerator, \"_search\")\ndef test_search_lines_calls_search_correctly(mock_search):\n    SearchMotionGenerator(None)._search_lines([\"a\", \"bc\", \"d\"], 0, 0, 1, 1)\n    mock_search.assert_called_once_with(\"a\\nbc\\nd\", 0, 3)\n"
  },
  {
    "path": "tests/test_motions_simple.py",
    "content": "from unittest import mock\n\nfrom pathfinder.server.motions import Motion\nfrom pathfinder.server.motions.simple import SimpleMotionGenerator\n\nINPUT_VIEW = mock.sentinel.input_view\nOUTPUT_VIEW = mock.sentinel.output_view\n\n\n@mock.patch(\"pathfinder.server.motions.simple.vim.command\")\n@mock.patch(\"pathfinder.server.motions.simple.winsaveview\", return_value=OUTPUT_VIEW)\n@mock.patch(\"pathfinder.server.motions.simple.winrestview\")\ndef test_try_motion(winrestview, winsaveview, command):\n    generator = SimpleMotionGenerator(None)\n    assert generator._try_motion(INPUT_VIEW, \"j\") == OUTPUT_VIEW\n    winrestview.assert_called_once_with(INPUT_VIEW)\n    command.assert_called_once_with(\"silent! normal! j\")\n    winsaveview.assert_called_once()\n\n\n@mock.patch(\"pathfinder.server.motions.simple.vim.command\", side_effect=Exception)\n@mock.patch(\"pathfinder.server.motions.simple.winsaveview\", return_value=OUTPUT_VIEW)\n@mock.patch(\"pathfinder.server.motions.simple.winrestview\")\ndef test_try_motion_when_motion_raises_exception(winrestview, winsaveview, command):\n    generator = SimpleMotionGenerator(None)\n    assert generator._try_motion(INPUT_VIEW, \"j\") is None\n    winrestview.assert_called_once_with(INPUT_VIEW)\n    command.assert_called_once_with(\"silent! normal! j\")\n    winsaveview.assert_not_called()\n\n\n@mock.patch.object(SimpleMotionGenerator, \"_create_node\")\n@mock.patch.object(SimpleMotionGenerator, \"_try_motion\", return_value=OUTPUT_VIEW)\ndef test_generate(try_motion, create_node):\n    generator = SimpleMotionGenerator(None)\n    output = list(generator.generate(INPUT_VIEW))\n    assert len(output) == len(SimpleMotionGenerator.MOTIONS)\n    for motion in SimpleMotionGenerator.MOTIONS:\n        try_motion.assert_any_call(INPUT_VIEW, motion)\n        create_node.assert_any_call(OUTPUT_VIEW, Motion(motion, None))\n\n\n@mock.patch.object(SimpleMotionGenerator, \"_create_node\")\n@mock.patch.object(SimpleMotionGenerator, \"_try_motion\", return_value=None)\ndef test_generate_when_try_motion_returns_none(try_motion, create_node):\n    generator = SimpleMotionGenerator(None)\n    output = list(generator.generate(INPUT_VIEW))\n    assert len(output) == 0\n    create_node.assert_not_called()\n    for motion in SimpleMotionGenerator.MOTIONS:\n        try_motion.assert_any_call(INPUT_VIEW, motion)\n\n\n@mock.patch.object(SimpleMotionGenerator, \"_create_node\")\n@mock.patch.object(SimpleMotionGenerator, \"_try_motion\", return_value=INPUT_VIEW)\ndef test_generate_when_try_motion_returns_same_as_input(try_motion, create_node):\n    generator = SimpleMotionGenerator(None)\n    output = list(generator.generate(INPUT_VIEW))\n    assert len(output) == 0\n    create_node.assert_not_called()\n    for motion in SimpleMotionGenerator.MOTIONS:\n        try_motion.assert_any_call(INPUT_VIEW, motion)\n"
  },
  {
    "path": "tests/test_node.py",
    "content": "from unittest import mock\n\nimport pytest\n\nfrom pathfinder.server.motions import Motion\nfrom pathfinder.server.node import Node\n\n\ndef test_reconstruct_path():\n    node1 = Node(None, None, None)\n    node2 = Node(None, None, mock.sentinel.motion_1)\n    node2.set_came_from(node1)\n    node3 = Node(None, None, mock.sentinel.motion_2)\n    node3.set_came_from(node2)\n    node4 = Node(None, None, mock.sentinel.motion_3)\n    node4.set_came_from(node3)\n\n    output = node4.reconstruct_path()\n    assert output == [\n        mock.sentinel.motion_1,\n        mock.sentinel.motion_2,\n        mock.sentinel.motion_3,\n    ]\n\n\ndef test_set_came_from_same_motion():\n    node1 = Node(None, None, mock.sentinel.motion_1)\n    node2 = Node(None, None, mock.sentinel.motion_1)\n    node2.set_came_from(node1)\n    assert node2.came_from == node1\n    assert node2.came_by_motion_repetitions == 2\n\n\ndef test_set_came_from_different_motion():\n    node1 = Node(None, None, mock.sentinel.motion_1)\n    node2 = Node(None, None, mock.sentinel.motion_2)\n    node2.set_came_from(node1)\n    assert node2.came_from == node1\n    assert node2.came_by_motion_repetitions == 1\n\n\n@pytest.mark.parametrize(\n    \"repetitions,expected\",\n    [(1, 1), (2, 0), (9, 1), (10, 0), (99, 1)],\n    ids=[1, 2, 9, 10, 99],\n)\ndef test_motion_weight_same_motion(repetitions, expected):\n    node = Node(None, None, mock.sentinel.motion)\n    node.came_by_motion_repetitions = repetitions\n    assert node.motion_weight(mock.sentinel.motion) == expected\n\n\n@pytest.mark.parametrize(\n    \"motion,expected\",\n    [(Motion(\"b\", None), 1), (Motion(\"cd\", None), 2), (Motion(\"e\", \"fg\"), 3)],\n    ids=[\"b\", \"cd\", \"e/fg\"],\n)\ndef test_motion_weight_different_motion(motion, expected):\n    node = Node(None, None, Motion(\"a\", None))\n    assert node.motion_weight(motion) == expected\n"
  },
  {
    "path": "tests/test_output.py",
    "content": "from unittest import mock\n\nimport pytest\n\nfrom pathfinder.client.output import compact_motions, explained_motions, get_count\nfrom pathfinder.server.motions import Motion\n\nCOUNTS = [\n    (\"j\", 1, \"j\"),\n    (\"j\", 2, \"jj\"),\n    (\"long\", 2, \"2long\"),\n    (\"j\", 3, \"3j\"),\n    (\"j\", 10, \"10j\"),\n]\n\n\n@mock.patch(\"pathfinder.client.output.strtrans\", lambda x: x)\n@pytest.mark.parametrize(\"motion,count,expected\", COUNTS, ids=[x[2] for x in COUNTS])\ndef test_get_count(motion, count, expected):\n    motion = Motion(motion, None)\n    assert get_count(motion, count) == expected\n\n\n@mock.patch(\"pathfinder.client.output.strtrans\", lambda x: x)\ndef test_compact_motions():\n    motion1 = Motion(\"j\", None)\n    motion2 = Motion(\"k\", None)\n    assert compact_motions([motion1, motion2, motion2, motion2]) == \"j 3k\"\n\n\n@mock.patch(\"pathfinder.client.output.strtrans\", lambda x: x)\ndef test_explained_motions():\n    motion1 = Motion(\"j\", None)\n    motion2 = Motion(\"f\", \"g\")\n    with mock.patch(\n        \"pathfinder.client.output.vim.vars\",\n        {\n            \"pf_descriptions\": {\n                \"j\": \"Down {count} lines\",\n                \"f\": \"To occurence {count} of {argument}\",\n            }\n        },\n    ):\n        assert list(explained_motions([motion1, motion2, motion2])) == [\n            \"j  Down 1 lines\",\n            \"2fg  To occurence 2 of g\",\n        ]\n"
  },
  {
    "path": "tests/test_popup.py",
    "content": "from unittest import mock\n\nfrom pathfinder.client.popup import _neovim_popup, _vim_popup, open_popup\n\n\n@mock.patch(\"pathfinder.client.popup._vim_popup\")\n@mock.patch(\"pathfinder.client.popup._neovim_popup\")\n@mock.patch(\n    \"pathfinder.client.popup.vim.eval\",\n    side_effect=lambda x: {\n        \"line('.')\": \"1\",\n        \"has('nvim-0.4')\": \"1\",\n        \"has('popupwin')\": \"0\",\n    }[x],\n)\ndef test_open_popup_neovim(vim_eval, neovim_popup, vim_popup):\n    open_popup(mock.sentinel.text)\n    vim_eval.assert_any_call(\"has('nvim-0.4')\")\n    neovim_popup.assert_called_once_with(mock.sentinel.text, \"+1\")\n    vim_popup.assert_not_called()\n\n\n@mock.patch(\"pathfinder.client.popup._vim_popup\")\n@mock.patch(\"pathfinder.client.popup._neovim_popup\")\n@mock.patch(\n    \"pathfinder.client.popup.vim.eval\",\n    side_effect=lambda x: {\n        \"line('.')\": \"2\",\n        \"has('nvim-0.4')\": \"0\",\n        \"has('popupwin')\": \"1\",\n    }[x],\n)\ndef test_open_popup_vim(vim_eval, neovim_popup, vim_popup):\n    open_popup(mock.sentinel.text)\n    vim_eval.assert_any_call(\"has('popupwin')\")\n    neovim_popup.assert_not_called()\n    vim_popup.assert_called_once_with(mock.sentinel.text, \"-1\")\n\n\n@mock.patch(\"pathfinder.client.popup._vim_popup\")\n@mock.patch(\"pathfinder.client.popup._neovim_popup\")\n@mock.patch(\n    \"pathfinder.client.popup.vim.eval\",\n    side_effect=lambda x: {\n        \"line('.')\": \"3\",\n        \"has('nvim-0.4')\": \"0\",\n        \"has('popupwin')\": \"0\",\n    }[x],\n)\ndef test_open_popup_echo(vim_eval, neovim_popup, vim_popup):\n    open_popup(mock.sentinel.text)\n    neovim_popup.assert_not_called()\n    vim_popup.assert_not_called()\n\n\n@mock.patch(\"pathfinder.client.popup.vim.vars\", {\"pf_popup_time\": b\"2000\"})\ndef test_vim_popup():\n    popup_create = mock.MagicMock(name='vim.Function(\"popup_create\")')\n\n    with mock.patch(\"pathfinder.client.popup.vim.Function\", return_value=popup_create):\n        _vim_popup(\"hello world\", \"+1\")\n\n    popup_create.assert_called_once_with(\n        \"hello world\",\n        {\n            \"line\": \"cursor+1\",\n            \"col\": \"cursor\",\n            \"wrap\": False,\n            \"padding\": (0, 1, 0, 1),\n            \"highlight\": \"PathfinderPopup\",\n            \"time\": 2000,  # Mocked in decorator\n            \"zindex\": 1000,\n        },\n    )\n\n\n@mock.patch(\"pathfinder.client.popup.vim.vars\", {\"pf_popup_time\": b\"2000\"})\n@mock.patch(\"pathfinder.client.popup.vim.eval\")\n@mock.patch(\"pathfinder.client.popup.vim.api.win_set_option\")\n@mock.patch(\n    \"pathfinder.client.popup.vim.api.open_win\", return_value=mock.sentinel.window\n)\n@mock.patch(\"pathfinder.client.popup.vim.api.buf_set_lines\")\n@mock.patch(\n    \"pathfinder.client.popup.vim.api.create_buf\", return_value=mock.sentinel.buffer\n)\ndef test_neovim_popup(create_buf, buf_set_lines, open_win, win_set_option, vim_eval):\n    type(open_win.return_value).handle = mock.PropertyMock(return_value=\"window handle\")\n\n    _neovim_popup(\"hello world\", \"-1\")\n\n    create_buf.assert_called_once_with(False, True)\n    buf_set_lines.assert_called_once_with(\n        mock.sentinel.buffer, 0, -1, True, [\" hello world \"]\n    )\n    open_win.assert_called_once_with(\n        mock.sentinel.buffer,\n        0,\n        {\n            \"relative\": \"cursor\",\n            \"row\": -1,\n            \"col\": 0,\n            \"style\": \"minimal\",\n            \"focusable\": 0,\n            \"height\": 1,\n            \"width\": 13,\n        },\n    )\n    win_set_option.assert_called_once_with(\n        mock.sentinel.window, \"winhl\", \"Normal:PathfinderPopup\"\n    )\n    vim_eval.assert_called_once_with(\n        \"timer_start(2000, {-> nvim_win_close(window handle, 1)})\"\n    )\n"
  },
  {
    "path": "tests/test_state_tracker.py",
    "content": "from unittest import mock\n\nimport pytest\n\nfrom pathfinder.client.state_tracker import StateTracker\n\n\n@pytest.fixture\ndef tracker():\n    with mock.patch.object(\n        StateTracker,\n        \"_record_state\",\n        return_value=mock.sentinel.initial_state,\n    ):\n        return StateTracker()\n\n\ndef test_reset(tracker):\n    initial_update_time = tracker.update_time\n\n    with mock.patch.object(\n        tracker,\n        \"_record_state\",\n        return_value=mock.sentinel.new_state,\n    ):\n        tracker.reset()\n\n    assert tracker.start_state == mock.sentinel.new_state\n    assert tracker.target_state == mock.sentinel.new_state\n    assert tracker.update_time > initial_update_time\n\n\ndef test_set_new_target(tracker):\n    initial_update_time = tracker.update_time\n\n    with mock.patch.object(\n        tracker,\n        \"_record_state\",\n        return_value=mock.sentinel.new_state,\n    ):\n        tracker.set_target()\n\n    assert tracker.start_state == mock.sentinel.initial_state\n    assert tracker.target_state == mock.sentinel.new_state\n    assert tracker.update_time > initial_update_time\n\n\ndef test_set_same_target(tracker):\n    initial_update_time = tracker.update_time\n\n    with mock.patch.object(\n        tracker,\n        \"_record_state\",\n        return_value=mock.sentinel.initial_state,\n    ):\n        tracker.set_target()\n\n    assert tracker.start_state == mock.sentinel.initial_state\n    assert tracker.target_state == mock.sentinel.initial_state\n    assert tracker.update_time == initial_update_time\n\n\ndef test_choose_action_reset(tracker):\n    update_time = tracker.update_time\n\n    with mock.patch.object(tracker, \"_reset\") as reset:\n        with mock.patch.object(tracker, \"_set_target\") as set_target:\n            with mock.patch.object(\n                tracker,\n                \"_record_state\",\n                return_value=mock.sentinel.new_state,\n            ):\n                chooser = mock.MagicMock(name=\"chooser\", return_value=\"reset\")\n                assert tracker.choose_action_using(chooser) == \"reset\"\n\n            chooser.assert_called_once_with(\n                mock.sentinel.initial_state, mock.sentinel.new_state, update_time\n            )\n            reset.assert_called_once()\n            set_target.assert_not_called()\n\n\ndef test_choose_action_set_target(tracker):\n    update_time = tracker.update_time\n\n    with mock.patch.object(tracker, \"_reset\") as reset:\n        with mock.patch.object(tracker, \"_set_target\") as set_target:\n            with mock.patch.object(\n                tracker,\n                \"_record_state\",\n                return_value=mock.sentinel.new_state,\n            ):\n                chooser = mock.MagicMock(name=\"chooser\", return_value=\"set_target\")\n                assert tracker.choose_action_using(chooser) == \"set_target\"\n\n            chooser.assert_called_once_with(\n                mock.sentinel.initial_state, mock.sentinel.new_state, update_time\n            )\n            set_target.assert_called_once()\n            reset.assert_not_called()\n\n\ndef test_choose_action_other(tracker):\n    update_time = tracker.update_time\n\n    with mock.patch.object(tracker, \"_reset\") as reset:\n        with mock.patch.object(tracker, \"_set_target\") as set_target:\n            with mock.patch.object(\n                tracker,\n                \"_record_state\",\n                return_value=mock.sentinel.new_state,\n            ):\n                chooser = mock.MagicMock(name=\"chooser\", return_value=\"other\")\n                assert tracker.choose_action_using(chooser) == \"other\"\n\n            chooser.assert_called_once_with(\n                mock.sentinel.initial_state, mock.sentinel.new_state, update_time\n            )\n            reset.assert_not_called()\n            set_target.assert_not_called()\n"
  },
  {
    "path": "tests/test_window.py",
    "content": "from unittest import mock\n\nimport pytest\n\nfrom pathfinder.window import View, cursor_in_same_position, winrestview, winsaveview\n\nTEST_DICT = {\"lnum\": 0, \"col\": 10, \"curswant\": 10, \"leftcol\": 5, \"topline\": 0}\nTEST_DICT_SOME_STRINGS = {\n    \"lnum\": 0,\n    \"col\": \"10\",\n    \"curswant\": b\"10\",\n    \"leftcol\": 5,\n    \"topline\": \"0\",\n}\nTEST_VIEW = View(0, 10, 10, 5, 0)\n\n\n@mock.patch(\n    \"pathfinder.window.vim.eval\",\n    return_value={**TEST_DICT_SOME_STRINGS, \"extra values should be ignored\": 200},\n)\ndef test_winsaveview(vim_eval):\n    assert winsaveview() == TEST_VIEW\n    vim_eval.assert_called_once_with(\"winsaveview()\")\n\n\n@mock.patch(\"pathfinder.window.vim.eval\")\ndef test_winrestview(vim_eval):\n    winrestview(TEST_VIEW)\n    vim_eval.assert_called_once_with(f\"winrestview({TEST_DICT})\")\n\n\n@pytest.mark.parametrize(\n    \"view_a,view_b,expected\",\n    [\n        (View(0, 0, None, None, None), View(0, 0, None, None, None), True),\n        (View(0, 0, None, None, None), View(1, 0, None, None, None), False),\n        (View(0, 0, None, None, None), View(0, 1, None, None, None), False),\n        (View(0, 0, None, None, None), View(1, 1, None, None, None), False),\n    ],\n    ids=[\"same position\", \"lnum differs\", \"col differs\", \"both differ\"],\n)\ndef test_cursor_in_same_position(view_a, view_b, expected):\n    assert cursor_in_same_position(view_a, view_b) is expected\n"
  }
]