Repository: denoland/deno
Pull request: perf(lsp): lazily start the ts server

Closes #28387.

Changed files:
filename: cli/lsp/diagnostics.rs
patch: @@ -2022,7 +2022,6 @@ let c: number = "a";
     .await;
     let snapshot = Arc::new(snapshot);
     let ts_server = TsServer::new(Default::default());
-    ts_server.start(None).unwrap();
 
     // test enabled
     {

filename: cli/lsp/language_server.rs
patch: @@ -213,9 +213,7 @@ pub struct Inner {
   registered_semantic_tokens_capabilities: bool,
   pub resolver: Arc<LspResolver>,
   task_queue: LanguageServerTaskQueue,
-  /// A memoized version of fixable diagnostic codes retrieved from TypeScript.
-  ts_fixable_diagnostics: Vec<String>,
-  /// An abstraction that handles interactions with TypeScript.
+  ts_fixable_diagnostics: tokio::sync::OnceCell<Vec<String>>,
   pub ts_server: Arc<TsServer>,
   /// A map of specifiers and URLs used to translate over the LSP.
   pub url_map: urls::LspUrlMap,
@@ -621,6 +619,19 @@ impl Inner {
     })
   }
 
+  pub async fn ts_fixable_diagnostics(&self) -> &Vec<String> {
+    self
+      .ts_fixable_diagnostics
+      .get_or_init(|| async {
+        self
+          .ts_server
+          .get_supported_code_fixes(self.snapshot())
+          .await
+          .unwrap()
+      })
+      .await
+  }
+
   pub fn update_tracing(&mut self) {
     let tracing =
       self
@@ -777,7 +788,7 @@ impl Inner {
 
 // lspower::LanguageServer methods. This file's LanguageServer delegates to us.
 impl Inner {
-  async fn initialize(
+  fn initialize(
     &mut self,
     params: InitializeParams,
   ) -> LspResult<InitializeResult> {
@@ -862,26 +873,13 @@ impl Inner {
     }
 
     self.diagnostics_server.start();
-    if let Err(e) = self
+    self
       .ts_server
-      .start(self.config.internal_inspect().to_address())
-    {
-      lsp_warn!("{}", e);
-      self.client.show_message(MessageType::ERROR, e);
-      return Err(tower_lsp::jsonrpc::Error::internal_error());
-    };
+      .set_inspector_server_addr(self.config.internal_inspect().to_address());
 
     self.update_tracing();
     self.update_debug_flag();
 
-    if capabilities.code_action_provider.is_some() {
-      let fixable_diagnostics = self
-        .ts_server
-        .get_supported_code_fixes(self.snapshot())
-        .await?;
-      self.ts_fixable_diagnostics = fixable_diagnostics;
-    }
-
     if capabilities.semantic_tokens_provider.is_some() {
       self.registered_semantic_tokens_capabilities = true;
     }
@@ -1746,6 +1744,7 @@ impl Inner {
     let line_index = asset_or_doc.line_index();
 
     // QuickFix
+    let ts_fixable_diagnosics = self.ts_fixable_diagnostics().await;
     let fixable_diagnostics: Vec<&Diagnostic> = params
       .context
       .diagnostics
@@ -1754,10 +1753,10 @@ impl Inner {
         Some(source) => match source.as_str() {
           "deno-ts" => match &d.code {
             Some(NumberOrString::String(code)) => {
-              self.ts_fixable_diagnostics.contains(code)
+              ts_fixable_diagnosics.contains(code)
             }
             Some(NumberOrString::Number(code)) => {
-              self.ts_fixable_diagnostics.contains(&code.to_string())
+              ts_fixable_diagnosics.contains(&code.to_string())
             }
             _ => false,
           },
@@ -3399,6 +3398,9 @@ impl Inner {
     params: RenameFilesParams,
     token: &CancellationToken,
   ) -> LspResult<Option<WorkspaceEdit>> {
+    if !self.ts_server.is_started() {
+      return Ok(None);
+    }
     let mut changes = vec![];
     for rename in params.files {
       let old_specifier = self.url_map.uri_to_specifier(
@@ -3461,6 +3463,10 @@ impl Inner {
     params: WorkspaceSymbolParams,
     token: &CancellationToken,
   ) -> LspResult<Option<Vec<SymbolInformation>>> {
+    if !self.ts_server.is_started() {
+      return Ok(None);
+    }
+
     let mark = self.performance.mark_with_args("lsp.symbol", &params);
 
     let navigate_to_items = self
@@ -3588,7 +3594,7 @@ impl tower_lsp::LanguageServer for LanguageServer {
     &self,
     params: InitializeParams,
   ) -> LspResult<InitializeResult> {
-    self.inner.write().await.initialize(params).await
+    self.inner.write().await.initialize(params)
   }
 
   async fn initialized(&self, _: InitializedParams) {

filename: cli/lsp/tsc.rs
patch: @@ -19,7 +19,6 @@ use std::thread;
 use dashmap::DashMap;
 use deno_ast::MediaType;
 use deno_core::anyhow::anyhow;
-use deno_core::anyhow::Context as _;
 use deno_core::convert::Smi;
 use deno_core::convert::ToV8;
 use deno_core::error::AnyError;
@@ -245,9 +244,11 @@ pub struct TsServer {
   sender: mpsc::UnboundedSender<Request>,
   receiver: Mutex<Option<mpsc::UnboundedReceiver<Request>>>,
   pub specifier_map: Arc<TscSpecifierMap>,
+  inspector_server_addr: Mutex<Option<String>>,
   inspector_server: Mutex<Option<Arc<InspectorServer>>>,
   pending_change: Mutex<Option<PendingChange>>,
   enable_tracing: Arc<AtomicBool>,
+  start_once: std::sync::Once,
 }
 
 impl std::fmt::Debug for TsServer {
@@ -257,7 +258,9 @@ impl std::fmt::Debug for TsServer {
       .field("sender", &self.sender)
       .field("receiver", &self.receiver)
       .field("specifier_map", &self.specifier_map)
+      .field("inspector_server_addr", &self.inspector_server_addr.lock())
       .field("inspector_server", &self.inspector_server.lock().is_some())
+      .field("start_once", &self.start_once)
       .finish()
   }
 }
@@ -404,9 +407,11 @@ impl TsServer {
       sender: tx,
       receiver: Mutex::new(Some(request_rx)),
       specifier_map: Arc::new(TscSpecifierMap::new()),
+      inspector_server_addr: Mutex::new(None),
       inspector_server: Mutex::new(None),
       pending_change: Mutex::new(None),
       enable_tracing: Default::default(),
+      start_once: std::sync::Once::new(),
     }
   }
 
@@ -416,40 +421,53 @@ impl TsServer {
       .store(enabled, std::sync::atomic::Ordering::Relaxed);
   }
 
-  pub fn start(
-    &self,
-    inspector_server_addr: Option<String>,
-  ) -> Result<(), AnyError> {
-    let maybe_inspector_server = match inspector_server_addr {
-      Some(addr) => {
-        let addr: SocketAddr = addr.parse().with_context(|| {
-          format!("Invalid inspector server address \"{}\"", &addr)
-        })?;
-        let server = InspectorServer::new(addr, "deno-lsp-tsc")?;
-        Some(Arc::new(server))
-      }
-      None => None,
-    };
-    self
-      .inspector_server
-      .lock()
-      .clone_from(&maybe_inspector_server);
-    // TODO(bartlomieju): why is the join_handle ignored here? Should we store it
-    // on the `TsServer` struct.
-    let receiver = self.receiver.lock().take().unwrap();
-    let performance = self.performance.clone();
-    let specifier_map = self.specifier_map.clone();
-    let enable_tracing = self.enable_tracing.clone();
-    let _join_handle = thread::spawn(move || {
-      run_tsc_thread(
-        receiver,
-        performance,
-        specifier_map,
-        maybe_inspector_server,
-        enable_tracing,
-      )
+  /// This should be called before `self.ensure_started()`.
+  pub fn set_inspector_server_addr(&self, addr: Option<String>) {
+    *self.inspector_server_addr.lock() = addr;
+  }
+
+  pub fn ensure_started(&self) {
+    self.start_once.call_once(|| {
+      let maybe_inspector_server = self
+        .inspector_server_addr
+        .lock()
+        .as_ref()
+        .and_then(|addr| {
+          addr
+            .parse::<SocketAddr>()
+            .inspect_err(|err| {
+              lsp_warn!("Invalid inspector server address: {:#}", err);
+            })
+            .ok()
+        })
+        .map(|addr| {
+          Arc::new(InspectorServer::new(addr, "deno-lsp-tsc").unwrap())
+        });
+      self
+        .inspector_server
+        .lock()
+        .clone_from(&maybe_inspector_server);
+      // TODO(bartlomieju): why is the join_handle ignored here? Should we store it
+      // on the `TsServer` struct.
+      let receiver = self.receiver.lock().take().unwrap();
+      let performance = self.performance.clone();
+      let specifier_map = self.specifier_map.clone();
+      let enable_tracing = self.enable_tracing.clone();
+      let _join_handle = thread::spawn(move || {
+        run_tsc_thread(
+          receiver,
+          performance,
+          specifier_map,
+          maybe_inspector_server,
+          enable_tracing,
+        )
+      });
+      lsp_log!("TS server started.");
     });
-    Ok(())
+  }
+
+  pub fn is_started(&self) -> bool {
+    self.start_once.is_completed()
   }
 
   pub fn project_changed<'a>(
@@ -549,6 +567,9 @@ impl TsServer {
 
   #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))]
   pub async fn cleanup_semantic_cache(&self, snapshot: Arc<StateSnapshot>) {
+    if !self.is_started() {
+      return;
+    }
     for scope in snapshot
       .config
       .tree
@@ -1365,6 +1386,7 @@ impl TsServer {
     R: de::DeserializeOwned,
   {
     use super::trace::SpanExt;
+    self.ensure_started();
     let context = super::trace::Span::current().context();
     let mark = self
       .performance
@@ -5772,7 +5794,6 @@ mod tests {
     });
     let performance = Arc::new(Performance::default());
     let ts_server = TsServer::new(performance);
-    ts_server.start(None).unwrap();
     ts_server.project_changed(
       snapshot.clone(),
       [],

filename: tests/integration/lsp_tests.rs
patch: @@ -11488,13 +11488,11 @@ fn lsp_performance() {
       "lsp.update_diagnostics_ts",
       "lsp.update_global_cache",
       "tsc.host.$getDiagnostics",
-      "tsc.host.$getSupportedCodeFixes",
       "tsc.host.getQuickInfoAtPosition",
       "tsc.op.op_is_node_file",
       "tsc.op.op_load",
       "tsc.op.op_script_names",
       "tsc.request.$getDiagnostics",
-      "tsc.request.$getSupportedCodeFixes",
       "tsc.request.getQuickInfoAtPosition",
     ]
   );
@@ -15135,6 +15133,7 @@ fn lsp_deno_json_scopes_file_rename_import_edits() {
   );
   let mut client = context.new_lsp_command().build();
   client.initialize_default();
+  client.did_open_file(&file1);
   let res = client.write_request(
     "workspace/willRenameFiles",
     json!({
@@ -15395,6 +15394,7 @@ fn lsp_deno_json_scopes_search_symbol() {
   );
   let mut client = context.new_lsp_command().build();
   client.initialize_default();
+  client.did_open_file(&file1);
   let res =
     client.write_request("workspace/symbol", json!({ "query": "someSymbol" }));
   assert_eq!(