From 1f02cc5f4fa4e1c25669f8943385e018ef84d64a Mon Sep 17 00:00:00 2001 From: Martin Dimitrov Date: Sun, 8 Mar 2026 01:01:00 +0200 Subject: [PATCH] Replace sleep-based indexing wait with $/progress tracking Replace blind sleep() in VueLanguageServer with event-driven $/progress tracking from the companion TypeScript language server. This makes Vue file indexing both faster (no unnecessary waiting) and more reliable (no under-waiting on slow machines). Changes: - Add workDoneProgress capability to TS server initialize params - Track window/workDoneProgress/create and $/progress notifications to detect when tsserver finishes async project loading - Expose public wait_for_indexing()/expect_indexing() API on TypeScriptLanguageServer (avoids cross-class private access) - Wait for indexing completion in TS server's own _start_server(), matching the existing Kotlin LS pattern - Unify indexing timeout as a single class constant (15s Win / 10s Unix) - Remove experimental/serverStatus handler (not sent by ts-language-server) --- .../typescript_language_server.py | 97 +++++++++++++++++-- .../language_servers/vue_language_server.py | 27 ++++-- 2 files changed, 108 insertions(+), 16 deletions(-) diff --git a/src/solidlsp/language_servers/typescript_language_server.py b/src/solidlsp/language_servers/typescript_language_server.py index bebd475b..21864455 100644 --- a/src/solidlsp/language_servers/typescript_language_server.py +++ b/src/solidlsp/language_servers/typescript_language_server.py @@ -67,6 +67,10 @@ class TypeScriptLanguageServer(SolidLanguageServer): - typescript_language_server_version: Version of typescript-language-server to install (default: "5.1.3") """ + # Safety timeout for $/progress-based indexing wait. Normally the event fires + # well within this window; the timeout is only hit if the server never sends progress. + INDEXING_PROGRESS_TIMEOUT = 15.0 if os.name == "nt" else 10.0 + def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings): """ Creates a TypeScriptLanguageServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. @@ -81,6 +85,29 @@ class TypeScriptLanguageServer(SolidLanguageServer): self.server_ready = threading.Event() self.initialize_searcher_command_available = threading.Event() + # Progress tracking for $/progress notifications (project indexing, etc.) + self._progress_lock = threading.Lock() + self._active_progress_tokens: set[str] = set() + self._indexing_complete = threading.Event() + self._indexing_complete.set() # Initially set (no active work) + + def wait_for_indexing(self, timeout: float) -> bool: + """Block until all $/progress tokens complete. + + :param timeout: Maximum seconds to wait. + :return: True if indexing completed, False on timeout. + """ + return self._indexing_complete.wait(timeout=timeout) + + def expect_indexing(self) -> None: + """Signal that new files are about to be opened and async indexing should be awaited. + + Clears the internal indexing-complete event so that a subsequent + :meth:`wait_for_indexing` call blocks until all $/progress tokens + complete (or the timeout expires). + """ + self._indexing_complete.clear() + def _create_dependency_provider(self) -> LanguageServerDependencyProvider: return self.DependencyProvider(self._custom_settings, self._ls_resources_dir) @@ -217,6 +244,9 @@ class TypeScriptLanguageServer(SolidLanguageServer): "didChangeConfiguration": {"dynamicRegistration": True}, "symbol": {"dynamicRegistration": True}, }, + "window": { + "workDoneProgress": True, # Enables $/progress notifications for project loading + }, }, "processId": os.getpid(), "rootPath": repository_absolute_path, @@ -266,26 +296,62 @@ class TypeScriptLanguageServer(SolidLanguageServer): def handle_typescript_version(params: dict) -> None: """ The $/typescriptVersion notification is sent by typescript-language-server - once tsserver has loaded and reported its version. This is the most reliable - readiness signal. + once tsserver has loaded and reported its version. This is a reliable + signal that tsserver is running and responsive. """ - log.info(f"TypeScript server ready signal detected via $/typescriptVersion: {params}") + log.info(f"TypeScript server version notification received: {params}") self.server_ready.set() - def check_experimental_status(params: dict) -> None: + def work_done_progress_create(params: dict) -> dict: + """Handle window/workDoneProgress/create: the server is about to report async progress. + + Clear the indexing-complete event so callers waiting on it will block until + all progress tokens finish. This is sent by typescript-language-server when + tsserver starts processing files (e.g. "Initializing JS/TS language features..."). """ - Also listen for experimental/serverStatus as a backup readiness signal. + token = str(params.get("token", "")) + log.debug(f"TypeScript LSP workDoneProgress/create: token={token!r}") + with self._progress_lock: + self._active_progress_tokens.add(token) + self._indexing_complete.clear() + return {} + + def progress_handler(params: dict) -> None: + """Track $/progress begin/end to detect when all async work finishes. + + typescript-language-server sends $/progress for project loading operations + like "Initializing JS/TS language features...". When all progress tokens + complete (kind='end'), _indexing_complete is set. """ - if params.get("quiescent") == True: - self.server_ready.set() + token = str(params.get("token", "")) + value = params.get("value", {}) + kind = value.get("kind") + if kind == "begin": + title = value.get("title", "") + log.info(f"TypeScript LSP progress [{token}]: started - {title}") + with self._progress_lock: + self._active_progress_tokens.add(token) + self._indexing_complete.clear() + elif kind == "report": + pct = value.get("percentage") + msg = value.get("message", "") + pct_str = f" ({pct}%)" if pct is not None else "" + log.debug(f"TypeScript LSP progress [{token}]: {msg}{pct_str}") + elif kind == "end": + msg = value.get("message", "") + log.info(f"TypeScript LSP progress [{token}]: ended - {msg}") + with self._progress_lock: + self._active_progress_tokens.discard(token) + if not self._active_progress_tokens: + self._indexing_complete.set() self.server.on_request("client/registerCapability", register_capability_handler) self.server.on_notification("window/logMessage", window_log_message) self.server.on_request("workspace/executeClientCommand", execute_client_command_handler) - self.server.on_notification("$/progress", do_nothing) + self.server.on_request("window/workDoneProgress/create", work_done_progress_create) + self.server.on_notification("$/progress", progress_handler) self.server.on_notification("$/typescriptVersion", handle_typescript_version) self.server.on_notification("textDocument/publishDiagnostics", do_nothing) - self.server.on_notification("experimental/serverStatus", check_experimental_status) log.info("Starting TypeScript server process") self.server.start() @@ -312,6 +378,19 @@ class TypeScriptLanguageServer(SolidLanguageServer): # Fallback: assume server is ready after timeout self.server_ready.set() + # Wait for any async project loading to complete. + # typescript-language-server may send $/progress for "Initializing JS/TS + # language features…" after initialized. If no progress is sent, + # _indexing_complete stays SET and wait() returns immediately. + log.info("Waiting for TypeScript project indexing to complete (if async)...") + if self.wait_for_indexing(timeout=self.INDEXING_PROGRESS_TIMEOUT): + log.info("TypeScript project indexing complete") + else: + log.warning( + "TypeScript project indexing did not complete within %.0fs; proceeding anyway", + self.INDEXING_PROGRESS_TIMEOUT, + ) + @override def _get_wait_time_for_cross_file_referencing(self) -> float: return 2 diff --git a/src/solidlsp/language_servers/vue_language_server.py b/src/solidlsp/language_servers/vue_language_server.py index 61c3ec34..1314bec1 100644 --- a/src/solidlsp/language_servers/vue_language_server.py +++ b/src/solidlsp/language_servers/vue_language_server.py @@ -134,8 +134,6 @@ class VueLanguageServer(SolidLanguageServer): TS_SERVER_READY_TIMEOUT = 5.0 VUE_SERVER_READY_TIMEOUT = 3.0 - # Windows requires more time due to slower I/O and process operations. - VUE_INDEXING_WAIT_TIME = 4.0 if os.name == "nt" else 2.0 def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings): vue_lsp_executable_path, self.tsdk_path, self._ts_ls_cmd = self._setup_runtime_dependencies(config, solidlsp_settings) @@ -204,6 +202,11 @@ class VueLanguageServer(SolidLanguageServer): vue_files = self._find_all_vue_files() log.debug(f"Found {len(vue_files)} .vue files to index") + # Prepare the TS server to track new $/progress notifications triggered + # by the didOpen calls below. Must happen BEFORE opening files to avoid + # a race where progress begins and ends before we start waiting. + self._ts_server.expect_indexing() + for vue_file in vue_files: try: with self._ts_server.open_file(vue_file) as file_buffer: @@ -213,13 +216,23 @@ class VueLanguageServer(SolidLanguageServer): log.debug(f"Failed to open {vue_file} on TS server: {e}") self._vue_files_indexed = True - log.info("Vue file indexing on TypeScript server complete") + log.info("Vue file indexing on TypeScript server complete, waiting for TS server to finish processing") - sleep(self._get_vue_indexing_wait_time()) - log.debug("Wait period after Vue file indexing complete") + self._wait_for_ts_indexing_complete() - def _get_vue_indexing_wait_time(self) -> float: - return self.VUE_INDEXING_WAIT_TIME + def _wait_for_ts_indexing_complete(self) -> None: + """Wait for the companion TypeScript server to finish processing opened Vue files. + + Uses the $/progress tracking in TypeScriptLanguageServer: after Vue files are + opened, tsserver sends "Initializing JS/TS language features…" progress. + We wait for all progress tokens to complete, with a timeout fallback. + """ + assert self._ts_server is not None + timeout = TypeScriptLanguageServer.INDEXING_PROGRESS_TIMEOUT + if self._ts_server.wait_for_indexing(timeout=timeout): + log.info("TypeScript server finished indexing Vue files (signaled via $/progress)") + else: + log.warning(f"Timeout ({timeout}s) waiting for TypeScript server to finish indexing Vue files, proceeding anyway") def _send_references_request(self, relative_file_path: str, line: int, column: int) -> list[lsp_types.Location] | None: uri = PathUtils.path_to_uri(os.path.join(self.repository_root_path, relative_file_path))