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)
This commit is contained in:
Martin Dimitrov committed 2026-03-08 01:11:04 +02:00
1 parent 3569a8145b
commit 1f02cc5f4f
2 files changed
+108 -16

No files matched your search

@@ -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
@@ -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))