Repository: denoland/deno
Pull request: perf(lsp): register semantic tokens provider upon opening enabled doc

We handle semantic tokens for Deno-disabled files because we register the provider per language ID. You can see this by `Deno` being highlighted as a namespace in such files even though it's untyped. No other request is used for Deno-disabled files.

This PR delays that provider registration at least until a Deno-enabled specifier is opened for the first time.

This will allow us to, for example, lazily spin up the TS server upon first use in an Deno-enabled file (whereas previously that wouldn't work because the TS server was immediately used for semantic tokens even in a Deno-disabled file). This will reduce idle memory usage of the LSP in Node projects. I'll handle this in a follow up PR.

Changed files:
filename: cli/lsp/capabilities.rs
patch: @@ -37,6 +37,43 @@ fn code_action_capabilities(
     .unwrap_or(CodeActionProviderCapability::Simple(true))
 }
 
+pub fn semantic_tokens_registration_options(
+) -> SemanticTokensRegistrationOptions {
+  SemanticTokensRegistrationOptions {
+    text_document_registration_options: TextDocumentRegistrationOptions {
+      document_selector: Some(vec![
+        DocumentFilter {
+          language: Some("javascript".to_string()),
+          scheme: None,
+          pattern: None,
+        },
+        DocumentFilter {
+          language: Some("javascriptreact".to_string()),
+          scheme: None,
+          pattern: None,
+        },
+        DocumentFilter {
+          language: Some("typescript".to_string()),
+          scheme: None,
+          pattern: None,
+        },
+        DocumentFilter {
+          language: Some("typescriptreact".to_string()),
+          scheme: None,
+          pattern: None,
+        },
+      ]),
+    },
+    semantic_tokens_options: SemanticTokensOptions {
+      legend: get_legend(),
+      range: Some(true),
+      full: Some(SemanticTokensFullOptions::Bool(true)),
+      ..Default::default()
+    },
+    static_registration_options: Default::default(),
+  }
+}
+
 pub fn server_capabilities(
   client_capabilities: &ClientCapabilities,
 ) -> ServerCapabilities {
@@ -126,43 +163,21 @@ pub fn server_capabilities(
       ..Default::default()
     }),
     call_hierarchy_provider: Some(CallHierarchyServerCapability::Simple(true)),
-    semantic_tokens_provider: Some(
-      SemanticTokensServerCapabilities::SemanticTokensRegistrationOptions(
-        SemanticTokensRegistrationOptions {
-          text_document_registration_options: TextDocumentRegistrationOptions {
-            document_selector: Some(vec![
-              DocumentFilter {
-                language: Some("javascript".to_string()),
-                scheme: None,
-                pattern: None,
-              },
-              DocumentFilter {
-                language: Some("javascriptreact".to_string()),
-                scheme: None,
-                pattern: None,
-              },
-              DocumentFilter {
-                language: Some("typescript".to_string()),
-                scheme: None,
-                pattern: None,
-              },
-              DocumentFilter {
-                language: Some("typescriptreact".to_string()),
-                scheme: None,
-                pattern: None,
-              },
-            ]),
-          },
-          semantic_tokens_options: SemanticTokensOptions {
-            legend: get_legend(),
-            range: Some(true),
-            full: Some(SemanticTokensFullOptions::Bool(true)),
-            ..Default::default()
-          },
-          static_registration_options: Default::default(),
-        },
-      ),
-    ),
+    semantic_tokens_provider: if client_capabilities
+      .text_document
+      .as_ref()
+      .and_then(|t| t.semantic_tokens.as_ref())
+      .and_then(|s| s.dynamic_registration)
+      .unwrap_or_default()
+    {
+      None
+    } else {
+      Some(
+        SemanticTokensServerCapabilities::SemanticTokensRegistrationOptions(
+          semantic_tokens_registration_options(),
+        ),
+      )
+    },
     workspace: Some(WorkspaceServerCapabilities {
       workspace_folders: Some(WorkspaceFoldersServerCapabilities {
         supported: Some(true),

filename: cli/lsp/language_server.rs
patch: @@ -57,6 +57,7 @@ use super::analysis::CodeActionData;
 use super::analysis::TsResponseImportMapper;
 use super::cache::LspCache;
 use super::capabilities;
+use super::capabilities::semantic_tokens_registration_options;
 use super::client::Client;
 use super::code_lens;
 use super::completions;
@@ -209,6 +210,7 @@ pub struct Inner {
   project_version: usize,
   /// A collection of measurements which instrument that performance of the LSP.
   performance: Arc<Performance>,
+  registered_semantic_tokens_capabilities: bool,
   pub resolver: Arc<LspResolver>,
   task_queue: LanguageServerTaskQueue,
   /// A memoized version of fixable diagnostic codes retrieved from TypeScript.
@@ -512,6 +514,7 @@ impl Inner {
       module_registry,
       npm_search_api,
       performance,
+      registered_semantic_tokens_capabilities: false,
       resolver: Default::default(),
       ts_fixable_diagnostics: Default::default(),
       ts_server,
@@ -721,6 +724,55 @@ impl Inner {
     let internal_debug = self.config.workspace_settings().internal_debug;
     super::logging::set_lsp_debug_flag(internal_debug)
   }
+
+  pub fn check_semantic_tokens_capabilities(&mut self) {
+    if self.registered_semantic_tokens_capabilities {
+      return;
+    }
+    if !self
+      .config
+      .client_capabilities
+      .text_document
+      .as_ref()
+      .and_then(|t| t.semantic_tokens.as_ref())
+      .and_then(|s| s.dynamic_registration)
+      .unwrap_or_default()
+    {
+      return;
+    }
+    let exists_enabled_document = self
+      .documents
+      .documents(DocumentsFilter::OpenDiagnosable)
+      .into_iter()
+      .any(|doc| {
+        doc.maybe_language_id().is_some_and(|l| {
+          matches!(
+            l,
+            LanguageId::JavaScript
+              | LanguageId::Jsx
+              | LanguageId::TypeScript
+              | LanguageId::Tsx
+          )
+        }) && self.config.specifier_enabled(doc.specifier())
+      });
+    if !exists_enabled_document {
+      return;
+    }
+    self.task_queue.queue_task(Box::new(|ls| {
+      spawn(async move {
+        let register_options =
+          serde_json::to_value(semantic_tokens_registration_options()).unwrap();
+        ls.client.when_outside_lsp_lock().register_capability(vec![Registration {
+          id: "textDocument/semanticTokens".to_string(),
+          method: "textDocument/semanticTokens".to_string(),
+          register_options: Some(register_options.clone()),
+        }]).await.inspect_err(|err| {
+          lsp_warn!("Couldn't register capability for \"textDocument/semanticTokens\": {err}");
+        }).ok();
+      });
+    }));
+    self.registered_semantic_tokens_capabilities = true;
+  }
 }
 
 // lspower::LanguageServer methods. This file's LanguageServer delegates to us.
@@ -830,6 +882,10 @@ impl Inner {
       self.ts_fixable_diagnostics = fixable_diagnostics;
     }
 
+    if capabilities.semantic_tokens_provider.is_some() {
+      self.registered_semantic_tokens_capabilities = true;
+    }
+
     self.performance.measure(mark);
     Ok(InitializeResult {
       capabilities,
@@ -1123,6 +1179,7 @@ impl Inner {
       file_referrer,
     );
     if document.is_diagnosable() {
+      self.check_semantic_tokens_capabilities();
       self.project_changed([(document.specifier(), ChangeKind::Opened)], false);
       self.refresh_dep_info().await;
       self.diagnostics_server.invalidate(&[specifier]);
@@ -1256,6 +1313,7 @@ impl Inner {
     };
     // TODO(nathanwhit): allow updating after startup, needs work to set thread local collector on tsc thread
     // self.update_tracing();
+    self.check_semantic_tokens_capabilities();
     self.update_debug_flag();
     self.update_global_cache().await;
     self.refresh_workspace_files();
@@ -4438,7 +4496,7 @@ impl Inner {
 "#,
         serde_json::to_string_pretty(&workspace_settings)
           .inspect_err(|e| {
-            dbg!(e);
+            lsp_warn!("{e}");
           })
           .unwrap(),
         documents_specifiers.len(),