import pathlib from typing import Generator from lsprotocol.types import CompletionItem from lsprotocol.types import CompletionItemKind from lsprotocol.types import Position from lsprotocol.types import Range from lsprotocol.types import TextEdit from esbonio.lsp.rst import CompletionContext def path_to_completion_item( context: CompletionContext, path: pathlib.Path ) -> CompletionItem: """Create the ``CompletionItem`` for the given path. In the case where there are multiple filepath components, this function needs to provide an appropriate ``TextEdit`` so that the most recent entry in the path can be easily edited - without clobbering the existing path. Also bear in mind that this function must play nice with both role target and directive argument completions. """ new_text = f"{path.name}" kind = CompletionItemKind.Folder if path.is_dir() else CompletionItemKind.File # If we can't find the '/' we may as well not bother with a `TextEdit` and let the # `Roles` feature provide the default handling. start = _find_start_char(context) if start == -1: insert_text = new_text filter_text = None text_edit = None else: start += 1 _, end = context.match.span() prefix = context.match.group(0)[start:] insert_text = None filter_text = ( f"{prefix}{new_text}" # Needed so VSCode will actually show the results. ) text_edit = TextEdit( range=Range( start=Position(line=context.position.line, character=start), end=Position(line=context.position.line, character=end), ), new_text=new_text, ) return CompletionItem( label=new_text, kind=kind, insert_text=insert_text, filter_text=filter_text, text_edit=text_edit, ) def _find_start_char(context: CompletionContext) -> int: matched_text = context.match.group(0) idx = matched_text.find("/") while True: next_idx = matched_text.find("/", idx + 1) if next_idx == -1: break idx = next_idx return idx def complete_filepaths(base: str, partial: str) -> Generator[pathlib.Path, None, None]: """Generate filepath completion suggestions relative to the given base. This function is for "docutils style" behaviour where the path is relative to the current document. Parameters ---------- base The directory containing the current document partial The existing path entered so far. """ candidate_dir = pathlib.Path(base) / pathlib.Path(partial) if partial and not partial.endswith("/"): candidate_dir = candidate_dir.parent return candidate_dir.glob("*") def complete_sphinx_filepaths( srcdir: str, base: str, partial: str ) -> Generator[pathlib.Path, None, None]: """Generate filepath completion suggestions relative to the given base or ``srcdir``. This function is for "sphinx style" behaviour where the path is relative to the current document *unless* ``partial`` starts with a ``/`` character. In this case completions should be relative to the given ``srcdir``. Parameters ---------- srcdir The ``srcdir`` of the project, used when ``partial`` starts with a ``/``. base The directory containing the current document. partial The existing path entered so far. """ if partial and partial.startswith("/"): candidate_dir = pathlib.Path(srcdir) # Be sure to take off the leading '/' character, otherwise the partial # path will wipe out the srcdir when concatenated... partial = partial[1:] else: candidate_dir = pathlib.Path(base) candidate_dir /= pathlib.Path(partial) if partial and not partial.endswith("/"): candidate_dir = candidate_dir.parent return candidate_dir.glob("*")