Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions src/solidlsp/language_servers/clangd_language_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ class ClangdLanguageServer(SolidLanguageServer):
``compile_commands.json`` if needed.
- clangd_version: Override the pinned Clangd version downloaded by Serena
(default: the bundled Serena version).
- prime_files: Translation units to keep open after startup in order to
provide clangd with stable TU context for header queries.
"""

def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
Expand Down Expand Up @@ -368,3 +370,32 @@ def window_log_message(msg: dict) -> None:

# wait for server to be ready
self.server_ready.wait()
self._prime_translation_units()

def _prime_translation_units(self) -> None:
prime_files = self._custom_settings.get("prime_files", [])
if not prime_files:
return
if not isinstance(prime_files, list):
raise TypeError("ls_specific_settings['cpp']['prime_files'] must be a list of relative file paths")

for relative_path in prime_files:
if not isinstance(relative_path, str):
raise TypeError("Each entry in ls_specific_settings['cpp']['prime_files'] must be a string")

absolute_path = pathlib.Path(self.repository_root_path, relative_path)
if not absolute_path.is_file():
log.warning("Skipping clangd prime file %s because it does not exist", relative_path)
continue
if self.is_ignored_path(relative_path):
log.warning("Skipping clangd prime file %s because it is ignored", relative_path)
continue

file_buffer = self.open_file_persistently(relative_path)
log.info("Opened clangd prime file %s persistently", relative_path)

try:
self.request_document_symbols(relative_path, file_buffer=file_buffer)
log.info("Warmed clangd prime file %s via document symbols request", relative_path)
except Exception as e:
log.warning("Failed to warm clangd prime file %s: %s", relative_path, e)
74 changes: 71 additions & 3 deletions src/solidlsp/ls.py
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,7 @@ def __init__(

self.language_id = language_id
self.open_file_buffers: dict[str, LSPFileBuffer] = {}
self._persistently_open_file_uris: set[str] = set()
self.language = Language(language_id)

# initialise symbol caches
Expand Down Expand Up @@ -660,6 +661,14 @@ def _shutdown(self, timeout: float = 5.0) -> None:
A robust shutdown process designed to terminate cleanly on all platforms, including Windows,
by explicitly closing all I/O pipes.
"""
for uri in list(self._persistently_open_file_uris):
self.close_file_persistently(uri=uri)

for uri, file_buffer in list(self.open_file_buffers.items()):
if file_buffer.ref_count == 0:
file_buffer.close()
del self.open_file_buffers[uri]

if not self.server.is_running():
log.debug("Server process not running, skipping shutdown.")
return
Expand Down Expand Up @@ -736,6 +745,64 @@ def _get_language_id_for_file(self, relative_file_path: str) -> str:
"""
return self.language_id

def open_file_persistently(self, relative_file_path: str, open_in_ls: bool = True) -> LSPFileBuffer:
"""
Open a file in the language server and keep it open across tool calls.

The caller must explicitly close the file later via ``close_file_persistently`` or
rely on server shutdown to clean it up.

:param relative_file_path: The relative path of the file to keep open.
:param open_in_ls: whether to send the didOpen notification immediately.
:return: the managed file buffer
"""
if not self.server_started:
log.error("open_file_persistently called before Language Server started")
raise SolidLSPException("Language Server not started")

absolute_file_path = Path(self.repository_root_path, relative_file_path)
uri = absolute_file_path.as_uri()

if uri in self.open_file_buffers:
file_buffer = self.open_file_buffers[uri]
if open_in_ls:
file_buffer.ensure_open_in_ls()
else:
version = 0
language_id = self._get_language_id_for_file(relative_file_path)
file_buffer = LSPFileBuffer(
abs_path=absolute_file_path,
uri=uri,
encoding=self._encoding,
version=version,
language_id=language_id,
ref_count=0,
language_server=self,
open_in_ls=open_in_ls,
)
self.open_file_buffers[uri] = file_buffer

self._persistently_open_file_uris.add(uri)
return file_buffer

def close_file_persistently(self, relative_file_path: str | None = None, uri: str | None = None) -> None:
"""
Close a file that was previously opened persistently.

:param relative_file_path: The relative path of the file to close.
:param uri: The file URI to close. Either this or ``relative_file_path`` must be provided.
"""
if uri is None:
if relative_file_path is None:
raise ValueError("Either relative_file_path or uri must be provided")
uri = Path(self.repository_root_path, relative_file_path).as_uri()

self._persistently_open_file_uris.discard(uri)
file_buffer = self.open_file_buffers.get(uri)
if file_buffer is not None and file_buffer.ref_count == 0:
file_buffer.close()
del self.open_file_buffers[uri]

@contextmanager
def open_file(self, relative_file_path: str, open_in_ls: bool = True) -> Iterator[LSPFileBuffer]:
"""
Expand All @@ -756,7 +823,7 @@ def open_file(self, relative_file_path: str, open_in_ls: bool = True) -> Iterato
if uri in self.open_file_buffers:
fb = self.open_file_buffers[uri]
assert fb.uri == uri
assert fb.ref_count >= 1
assert fb.ref_count >= 0

fb.ref_count += 1
if open_in_ls:
Expand All @@ -781,8 +848,9 @@ def open_file(self, relative_file_path: str, open_in_ls: bool = True) -> Iterato
fb.ref_count -= 1

if self.open_file_buffers[uri].ref_count == 0:
self.open_file_buffers[uri].close()
del self.open_file_buffers[uri]
if uri not in self._persistently_open_file_uris:
self.open_file_buffers[uri].close()
del self.open_file_buffers[uri]

@contextmanager
def _open_file_context(
Expand Down
21 changes: 21 additions & 0 deletions test/solidlsp/test_ls_common.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
import pathlib

import pytest

Expand Down Expand Up @@ -41,3 +42,23 @@ def test_open_file_cache_invalidate(self, language_server: SolidLanguageServer)

finally:
os.remove(file_path)

@pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True)
def test_open_file_persistently(self, language_server: SolidLanguageServer) -> None:
"""
Tests that a persistently opened file remains available across normal open_file scopes.
"""
file_path = os.path.join("test_repo", "nested.py")
uri = pathlib.Path(language_server.repository_root_path, file_path).as_uri()

file_buffer = language_server.open_file_persistently(file_path)
assert file_buffer.uri == uri
assert uri in language_server.open_file_buffers

with language_server.open_file(file_path) as scoped_buffer:
assert scoped_buffer.uri == uri

assert uri in language_server.open_file_buffers

language_server.close_file_persistently(file_path)
assert uri not in language_server.open_file_buffers
Loading