diff --git a/cli/lsp/language_server.rs b/cli/lsp/language_server.rs index fdd497bbac..db3eb869ac 100644 --- a/cli/lsp/language_server.rs +++ b/cli/lsp/language_server.rs @@ -1196,14 +1196,14 @@ impl Inner { // a @types/node package and now's a good time to do that anyway self.refresh_npm_specifiers().await; - self.project_changed(&[], true).await; + self.project_changed([], true); } fn shutdown(&self) -> LspResult<()> { Ok(()) } - async fn did_open( + fn did_open( &mut self, specifier: &ModuleSpecifier, params: DidOpenTextDocumentParams, @@ -1231,9 +1231,7 @@ impl Inner { params.text_document.language_id.parse().unwrap(), params.text_document.text.into(), ); - self - .project_changed(&[(document.specifier(), ChangeKind::Opened)], false) - .await; + self.project_changed([(document.specifier(), ChangeKind::Opened)], false); self.performance.measure(mark); document @@ -1251,12 +1249,10 @@ impl Inner { ) { Ok(document) => { if document.is_diagnosable() { - self - .project_changed( - &[(document.specifier(), ChangeKind::Modified)], - false, - ) - .await; + self.project_changed( + [(document.specifier(), ChangeKind::Modified)], + false, + ); self.refresh_npm_specifiers().await; self.diagnostics_server.invalidate(&[specifier]); self.send_diagnostics_update(); @@ -1307,9 +1303,7 @@ impl Inner { if let Err(err) = self.documents.close(&specifier) { error!("{:#}", err); } - self - .project_changed(&[(&specifier, ChangeKind::Closed)], false) - .await; + self.project_changed([(&specifier, ChangeKind::Closed)], false); self.performance.measure(mark); } @@ -1423,15 +1417,10 @@ impl Inner { self.recreate_npm_services_if_necessary().await; self.refresh_documents_config().await; self.diagnostics_server.invalidate_all(); - self - .project_changed( - &changes - .iter() - .map(|(s, _)| (s, ChangeKind::Modified)) - .collect::>(), - false, - ) - .await; + self.project_changed( + changes.iter().map(|(s, _)| (s, ChangeKind::Modified)), + false, + ); self.ts_server.cleanup_semantic_cache(self.snapshot()).await; self.send_diagnostics_update(); self.send_testing_update(); @@ -3004,16 +2993,17 @@ impl Inner { Ok(maybe_symbol_information) } - async fn project_changed( + fn project_changed<'a>( &mut self, - modified_scripts: &[(&ModuleSpecifier, ChangeKind)], + modified_scripts: impl IntoIterator, config_changed: bool, ) { self.project_version += 1; // increment before getting the snapshot - self - .ts_server - .project_changed(self.snapshot(), modified_scripts, config_changed) - .await; + self.ts_server.project_changed( + self.snapshot(), + modified_scripts, + config_changed, + ); } fn send_diagnostics_update(&self) { @@ -3221,7 +3211,7 @@ impl tower_lsp::LanguageServer for LanguageServer { let specifier = inner .url_map .normalize_url(¶ms.text_document.uri, LspUrlKind::File); - let document = inner.did_open(&specifier, params).await; + let document = inner.did_open(&specifier, params); if document.is_diagnosable() { inner.refresh_npm_specifiers().await; inner.diagnostics_server.invalidate(&[specifier]); @@ -3561,7 +3551,7 @@ impl Inner { // the language server for TypeScript (as it might hold to some stale // documents). self.diagnostics_server.invalidate_all(); - self.project_changed(&[], false).await; + self.project_changed([], false); self.ts_server.cleanup_semantic_cache(self.snapshot()).await; self.send_diagnostics_update(); self.send_testing_update(); diff --git a/cli/lsp/tsc.rs b/cli/lsp/tsc.rs index 2873ba703b..4cb93e8024 100644 --- a/cli/lsp/tsc.rs +++ b/cli/lsp/tsc.rs @@ -107,6 +107,7 @@ type Request = ( Arc, oneshot::Sender>, CancellationToken, + Option, ); #[derive(Debug, Clone, Copy, Serialize_repr)] @@ -224,6 +225,7 @@ pub struct TsServer { receiver: Mutex>>, specifier_map: Arc, inspector_server: Mutex>>, + pending_change: Mutex>, } impl std::fmt::Debug for TsServer { @@ -256,6 +258,47 @@ impl Serialize for ChangeKind { } } +pub struct PendingChange { + pub modified_scripts: Vec<(String, ChangeKind)>, + pub project_version: usize, + pub config_changed: bool, +} + +impl PendingChange { + fn to_json(&self) -> Value { + json!([ + self.modified_scripts, + self.project_version, + self.config_changed, + ]) + } + + fn coalesce( + &mut self, + new_version: usize, + modified_scripts: Vec<(String, ChangeKind)>, + config_changed: bool, + ) { + self.project_version = self.project_version.max(new_version); + self.config_changed |= config_changed; + for (spec, new) in modified_scripts { + if let Some((_, current)) = + self.modified_scripts.iter_mut().find(|(s, _)| s == &spec) + { + match (*current, new) { + (_, ChangeKind::Closed) => { + *current = ChangeKind::Closed; + } + (ChangeKind::Opened, ChangeKind::Modified) => { + *current = ChangeKind::Modified; + } + _ => {} + } + } + } + } +} + impl TsServer { pub fn new(performance: Arc, cache: Arc) -> Self { let (tx, request_rx) = mpsc::unbounded_channel::(); @@ -266,6 +309,7 @@ impl TsServer { receiver: Mutex::new(Some(request_rx)), specifier_map: Arc::new(TscSpecifierMap::new()), inspector_server: Mutex::new(None), + pending_change: Mutex::new(None), } } @@ -302,30 +346,33 @@ impl TsServer { Ok(()) } - pub async fn project_changed( + pub fn project_changed<'a>( &self, snapshot: Arc, - modified_scripts: &[(&ModuleSpecifier, ChangeKind)], + modified_scripts: impl IntoIterator, config_changed: bool, ) { let modified_scripts = modified_scripts - .iter() + .into_iter() .map(|(spec, change)| (self.specifier_map.denormalize(spec), change)) .collect::>(); - let req = TscRequest { - method: "$projectChanged", - args: json!( - [modified_scripts, snapshot.project_version, config_changed,] - ), - }; - self - .request::<()>(snapshot, req) - .await - .map_err(|err| { - log::error!("Failed to request to tsserver {}", err); - LspError::invalid_request() - }) - .ok(); + match &mut *self.pending_change.lock() { + Some(pending_change) => { + pending_change.coalesce( + snapshot.project_version, + modified_scripts, + config_changed, + ); + } + pending => { + let pending_change = PendingChange { + modified_scripts, + project_version: snapshot.project_version, + config_changed, + }; + *pending = Some(pending_change); + } + } } pub async fn get_diagnostics( @@ -1069,7 +1116,12 @@ impl TsServer { let token = token.child_token(); let droppable_token = DroppableToken(token.clone()); let (tx, rx) = oneshot::channel::>(); - if self.sender.send((req, snapshot, tx, token)).is_err() { + let change = self.pending_change.lock().take(); + if self + .sender + .send((req, snapshot, tx, token, change.map(|c| c.to_json()))) + .is_err() + { return Err(anyhow!("failed to send request to tsc thread")); } let value = rx.await??; @@ -4245,8 +4297,8 @@ fn run_tsc_thread( tokio::select! { biased; (maybe_request, mut tsc_runtime) = async { (request_rx.recv().await, tsc_runtime.lock().await) } => { - if let Some((req, state_snapshot, tx, token)) = maybe_request { - let value = request(&mut tsc_runtime, state_snapshot, req, token.clone()); + if let Some((req, state_snapshot, tx, token, pending_change)) = maybe_request { + let value = request(&mut tsc_runtime, state_snapshot, req, pending_change, token.clone()); request_signal_tx.send(()).unwrap(); let was_sent = tx.send(value).is_ok(); // Don't print the send error if the token is cancelled, it's expected @@ -4664,6 +4716,7 @@ fn request( runtime: &mut JsRuntime, state_snapshot: Arc, request: TscRequest, + change: Option, token: CancellationToken, ) -> Result { if token.is_cancelled() { @@ -4688,8 +4741,10 @@ fn request( "Internal error: expected args to be array" ); let request_src = format!( - "globalThis.serverRequest({id}, \"{}\", {});", - request.method, &request.args + "globalThis.serverRequest({id}, \"{}\", {}, {});", + request.method, + &request.args, + change.unwrap_or_default() ); runtime.execute_script(located_script_name!(), request_src)?; @@ -5221,13 +5276,11 @@ mod tests { ..snapshot.as_ref().clone() }) }; - ts_server - .project_changed( - snapshot.clone(), - &[(&specifier_dep, ChangeKind::Opened)], - false, - ) - .await; + ts_server.project_changed( + snapshot.clone(), + [(&specifier_dep, ChangeKind::Opened)], + false, + ); let specifier = resolve_url("file:///a.ts").unwrap(); let diagnostics = ts_server .get_diagnostics(snapshot.clone(), vec![specifier], Default::default()) diff --git a/cli/tsc/99_main_compiler.js b/cli/tsc/99_main_compiler.js index 836d81f87e..c2c9a88d57 100644 --- a/cli/tsc/99_main_compiler.js +++ b/cli/tsc/99_main_compiler.js @@ -542,7 +542,7 @@ delete Object.prototype.__proto__; } } - /** @type {ts.LanguageService} */ + /** @type {ts.LanguageService & { [k:string]: any }} */ let languageService; /** An object literal of the incremental compiler host, which provides the @@ -1073,42 +1073,43 @@ delete Object.prototype.__proto__; ops.op_respond(JSON.stringify(data)); } - function serverRequest(id, method, args) { + /** + * @param {number} id + * @param {string} method + * @param {any[]} args + * @param {[[string, number][], number, boolean] | null} maybeChange + */ + function serverRequest(id, method, args, maybeChange) { if (logDebug) { - debug(`serverRequest()`, id, method, args); + debug(`serverRequest()`, id, method, args, maybeChange); } lastRequestMethod = method; - switch (method) { - case "$projectChanged": { - /** @type {[string, number][]} */ - const changedScripts = args[0]; - /** @type {number} */ - const newProjectVersion = args[1]; - /** @type {boolean} */ - const configChanged = args[2]; + if (maybeChange !== null) { + const changedScripts = maybeChange[0]; + const newProjectVersion = maybeChange[1]; + const configChanged = maybeChange[2]; - if (configChanged) { - tsConfigCache = null; - isNodeSourceFileCache.clear(); - } - - projectVersionCache = newProjectVersion; - - let opened = false; - for (const { 0: script, 1: changeKind } of changedScripts) { - if (changeKind == ChangeKind.Opened) { - opened = true; - } - scriptVersionCache.delete(script); - sourceTextCache.delete(script); - } - - if (configChanged || opened) { - scriptFileNamesCache = undefined; - } - - return respond(id); + if (configChanged) { + tsConfigCache = null; + isNodeSourceFileCache.clear(); } + + projectVersionCache = newProjectVersion; + + let opened = false; + for (const { 0: script, 1: changeKind } of changedScripts) { + if (changeKind == ChangeKind.Opened) { + opened = true; + } + scriptVersionCache.delete(script); + sourceTextCache.delete(script); + } + + if (configChanged || opened) { + scriptFileNamesCache = undefined; + } + } + switch (method) { case "$getSupportedCodeFixes": { return respond( id, diff --git a/tests/integration/lsp_tests.rs b/tests/integration/lsp_tests.rs index 76d6e22e10..3554f303a8 100644 --- a/tests/integration/lsp_tests.rs +++ b/tests/integration/lsp_tests.rs @@ -9044,7 +9044,6 @@ fn lsp_performance() { "tsc.host.$getAssets", "tsc.host.$getDiagnostics", "tsc.host.$getSupportedCodeFixes", - "tsc.host.$projectChanged", "tsc.host.getQuickInfoAtPosition", "tsc.op.op_is_node_file", "tsc.op.op_load", @@ -9052,7 +9051,6 @@ fn lsp_performance() { "tsc.op.op_ts_config", "tsc.request.$getAssets", "tsc.request.$getSupportedCodeFixes", - "tsc.request.$projectChanged", "tsc.request.getQuickInfoAtPosition", ] );