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
This commit is contained in:
Dominik Jain authored and Dominik Jain committed 2026-02-02 15:10:43 +01:00
1 parent 811e1981b5
commit cefa197743
1 file changed
+83 -32
+83 -32
View File
@@ -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: