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

def debytes ( string ) : try : return string . decode ( ) except AttributeError : return string


Check if the given views have the cursor on the same position .

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

def cursor_in_same_position ( a , b ) : return a . lnum == b . lnum and a . col == b . col


Continuously wait for and handle instructions from the client .

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


Process an instruction from the client .

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


Run the pathfinder then send the result back to the client .

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


Use Dijkstra s algorithm to find the optimal sequence of motions .

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

def find_path ( self , client_connection ) : 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


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 get_neighbours ( self ) : 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


Return the weight of using a motion from this 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)
            )


Set the node this node was reached from .

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 set_came_from ( self , node ) : 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


Return the sequence of motions used to reach this node .

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

def reconstruct_path ( self ) : motions = list ( ) node = self while node . came_from is not None : motions . insert ( 0 , node . came_by_motion ) node = node . came_from return motions


Add backslash escapes to any magic characters in a 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 _escape_magic ( self , search_query ) : for char in r"\^$.*[~/" : search_query = search_query . replace ( char , "\\" + char ) return search_query


Return the simplest possible searching motion to reach the given target .

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, "?")


Wrapper around _search which handles 2d coordinates and a list of lines .

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)

def _search_lines ( self , lines , start_line , start_col , target_line , target_col ) : 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 )


Use a motion inside Vim starting from the given view .

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

def
_try_motion
(
self
,
view
,
motion
)
:
winrestview
(
view
)
try
:
vim
.
command
(
f"silent! normal! {motion}"
)
except
:
return
return
winsaveview
(
)


Yield all neighbouring nodes found from the given view .

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


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

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


Return the minimum and maximum line numbers to explore .

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

def get_line_limits ( start_view , target_view ) : 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 ) , )


Choose an action to take using the given function .

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

def choose_action_using ( self , function ) : 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


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

def strtrans ( string ) : 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


Return the given motion sequence in single - line form .

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
compact_motions
(
motions
)
:
return
" "
.
join
(
[
get_count
(
motion
,
len
(
list
(
group
)
)
)
for
motion
,
group
in
itertools
.
groupby
(
motions
)
]
)


Yield each motion in the form motion <padding > help

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

def
explained_motions
(
motions
)
:
for
motion
,
group
in
itertools
.
groupby
(
motions
)
:
repetitions
=
len
(
list
(
group
)
)
yield
(
get_count
(
motion
,
repetitions
)
+
"  "
+
get_description
(
motion
,
repetitions
)
)


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


Build the command used to launch the server Vim .

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"):
            python3_host_prog = vim.eval("g:python3_host_prog")
            options += [
                "--headless",
                "--cmd",
                f"let g:python3_host_prog='{python3_host_prog}'",
            ]
        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 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


Process a response recieved from the server .

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)


Request a pathfinding result from the server .

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"],
        }


Create a popup using Neovim 0 . 4 + floating windows .

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 _vim_popup ( text , line_offset ) : 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 , } , )


Start calculating a path in the background .

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 _run ( self ) : self . client . pathfind ( self . state_tracker . start_state . lines , self . state_tracker . start_state . view , self . state_tracker . target_state . view , self . popup , )


Called on a timer several times per second .

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 autorun ( self ) : 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 ( )


Called for the : PathfinderBegin command .

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

def command_begin ( self ) : self . state_tracker . reset ( )


Called for the : PathfinderRun command .

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


Called for the : PathfinderExplain command .

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

def stop ( self ) : self . client . close ( )


Select an action to take automatically .

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"