From cefa197743a2f8111c7bae032fd3d05b794543d4 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Mon, 2 Feb 2026 15:10:43 +0100 Subject: [PATCH] SolidLanguageServer: Allow to open file buffers lazily (without opening in LS) Not passing the open request directly on to the LS can speed things up for cases where we don't necessarily need the file to be open in the LS at first (but do need to open the file and read its contents) Applied to request_document_symbols, where we check the cache based on the file contents but only need to open the file in the LS if there is a cache miss. Relates to #984 --- src/solidlsp/ls.py | 115 ++++++++++++++++++++++++++++++++------------- 1 file changed, 83 insertions(+), 32 deletions(-) diff --git a/src/solidlsp/ls.py b/src/solidlsp/ls.py index 116f31fc..c58ce749 100644 --- a/src/solidlsp/ls.py +++ b/src/solidlsp/ls.py @@ -64,7 +64,18 @@ class LSPFileBuffer: This class is used to store the contents of an open LSP file in memory. """ - def __init__(self, uri: str, contents: str, encoding: str, version: int, language_id: str, ref_count: int) -> None: + def __init__( + self, + uri: str, + contents: str, + encoding: str, + version: int, + language_id: str, + ref_count: int, + language_server: "SolidLanguageServer", + open_in_ls: bool = True, + ) -> None: + self.language_server = language_server self.uri = uri self.contents = contents self.version = version @@ -72,6 +83,41 @@ class LSPFileBuffer: self.ref_count = ref_count self.encoding = encoding self._content_hash: str | None = None + self._is_open_in_ls = False + if open_in_ls: + self._open_in_ls() + + def _open_in_ls(self): + """ + Open the file in the language server if it is not already open. + """ + if self._is_open_in_ls: + return + self._is_open_in_ls = True + self.language_server.server.notify.did_open_text_document( + { + LSPConstants.TEXT_DOCUMENT: { # type: ignore + LSPConstants.URI: self.uri, + LSPConstants.LANGUAGE_ID: self.language_id, + LSPConstants.VERSION: 0, + LSPConstants.TEXT: self.contents, + } + } + ) + + def close(self): + if self._is_open_in_ls: + self.language_server.server.notify.did_close_text_document( + { + LSPConstants.TEXT_DOCUMENT: { # type: ignore + LSPConstants.URI: self.uri, + } + } + ) + + def ensure_open_in_ls(self) -> None: + """Ensure that the file is opened in the language server.""" + self._open_in_ls() @property def content_hash(self) -> str: @@ -611,11 +657,14 @@ class SolidLanguageServer(ABC): return self.language_id @contextmanager - def open_file(self, relative_file_path: str) -> Iterator[LSPFileBuffer]: + def open_file(self, relative_file_path: str, open_in_ls: bool = True) -> Iterator[LSPFileBuffer]: """ Open a file in the Language Server. This is required before making any requests to the Language Server. :param relative_file_path: The relative path of the file to open. + :param open_in_ls: whether to open the file in the language server, sending the didOpen notification. + Set this to False to read the local file buffer without notifying the LS; the file can + be opened in the LS later by calling the `ensure_open_in_ls` method on the returned LSPFileBuffer. """ if not self.server_started: log.error("open_file called before Language Server started") @@ -625,56 +674,57 @@ class SolidLanguageServer(ABC): uri = pathlib.Path(absolute_file_path).as_uri() if uri in self.open_file_buffers: - assert self.open_file_buffers[uri].uri == uri - assert self.open_file_buffers[uri].ref_count >= 1 + fb = self.open_file_buffers[uri] + assert fb.uri == uri + assert fb.ref_count >= 1 - self.open_file_buffers[uri].ref_count += 1 - yield self.open_file_buffers[uri] - self.open_file_buffers[uri].ref_count -= 1 + fb.ref_count += 1 + if open_in_ls: + fb.ensure_open_in_ls() + yield fb + fb.ref_count -= 1 else: contents = FileUtils.read_file(absolute_file_path, self._encoding) version = 0 language_id = self._get_language_id_for_file(relative_file_path) - self.open_file_buffers[uri] = LSPFileBuffer( - uri=uri, contents=contents, encoding=self._encoding, version=version, language_id=language_id, ref_count=1 + fb = LSPFileBuffer( + uri=uri, + contents=contents, + encoding=self._encoding, + version=version, + language_id=language_id, + ref_count=1, + language_server=self, + open_in_ls=open_in_ls, ) - - self.server.notify.did_open_text_document( - { - LSPConstants.TEXT_DOCUMENT: { # type: ignore - LSPConstants.URI: uri, - LSPConstants.LANGUAGE_ID: language_id, - LSPConstants.VERSION: 0, - LSPConstants.TEXT: contents, - } - } - ) - yield self.open_file_buffers[uri] - self.open_file_buffers[uri].ref_count -= 1 + self.open_file_buffers[uri] = fb + yield fb + fb.ref_count -= 1 if self.open_file_buffers[uri].ref_count == 0: - self.server.notify.did_close_text_document( - { - LSPConstants.TEXT_DOCUMENT: { # type: ignore - LSPConstants.URI: uri, - } - } - ) + self.open_file_buffers[uri].close() del self.open_file_buffers[uri] @contextmanager - def _open_file_context(self, relative_file_path: str, file_buffer: LSPFileBuffer | None = None) -> Iterator[LSPFileBuffer]: + def _open_file_context( + self, relative_file_path: str, file_buffer: LSPFileBuffer | None = None, open_in_ls: bool = True + ) -> Iterator[LSPFileBuffer]: """ Internal context manager to open a file, optionally reusing an existing file buffer. :param relative_file_path: the relative path of the file to open. :param file_buffer: an optional existing file buffer to reuse. + :param open_in_ls: whether to open the file in the language server, sending the didOpen notification. + Set this to False to read the local file buffer without notifying the LS; the file can + be opened in the LS later by calling the `ensure_open_in_ls` method on the returned LSPFileBuffer. """ if file_buffer is not None: + if open_in_ls: + file_buffer.ensure_open_in_ls() yield file_buffer else: - with self.open_file(relative_file_path) as fb: + with self.open_file(relative_file_path, open_in_ls=open_in_ls) as fb: yield fb def insert_text_at_position(self, relative_file_path: str, line: int, column: int, text_to_be_inserted: str) -> ls_types.Position: @@ -1130,7 +1180,7 @@ class SolidLanguageServer(ABC): where the parent attribute will be the file symbol which in turn may have a package symbol as parent. If you need a symbol tree that contains file symbols as well, you should use `request_full_symbol_tree` instead. """ - with self._open_file_context(relative_file_path, file_buffer) as file_data: + with self._open_file_context(relative_file_path, file_buffer, open_in_ls=False) as file_data: # check if the desired result is cached cache_key = relative_file_path file_hash_and_result = self._document_symbols_cache.get(cache_key) @@ -1145,6 +1195,7 @@ class SolidLanguageServer(ABC): log.debug("No cache hit for document symbols in %s", relative_file_path) # no cached result: request the root symbols from the language server + file_data.ensure_open_in_ls() root_symbols = self._request_document_symbols(relative_file_path, file_data) if root_symbols is None: