Repository: denoland/deno
Pull request: fix(lsp): refresh configuration when tsconfig.json is changed


Changed files:
filename: cli/lsp/compiler_options.rs
patch: @@ -1,6 +1,7 @@
 // Copyright 2018-2025 the Deno authors. MIT license.
 
 use std::collections::BTreeMap;
+use std::collections::HashSet;
 use std::sync::Arc;
 
 use deno_config::deno_json::CompilerOptions;
@@ -17,6 +18,7 @@ use crate::lsp::config::Config;
 use crate::lsp::logging::lsp_warn;
 use crate::lsp::resolver::LspResolver;
 use crate::sys::CliSys;
+use crate::util::fs::canonicalize_path_maybe_not_exists;
 
 #[derive(Debug, Clone)]
 pub struct LspCompilerOptionsData {
@@ -26,6 +28,7 @@ pub struct LspCompilerOptionsData {
   pub skip_lib_check: bool,
   pub jsx_import_source_config: Option<Arc<JsxImportSourceConfig>>,
   pub ts_config_files: Option<(Arc<Url>, Vec<TsConfigFile>)>,
+  watched_files: HashSet<Arc<Url>>,
 }
 
 #[derive(Debug)]
@@ -95,6 +98,19 @@ impl LspCompilerOptionsResolver {
               .flatten()
               .cloned(),
             ts_config_files: f.map(|(r, f)| (r.clone(), f.clone())),
+            watched_files: d
+              .sources
+              .iter()
+              .flat_map(|s| {
+                std::iter::once(s.specifier.clone()).chain(
+                  s.specifier
+                    .to_file_path()
+                    .ok()
+                    .and_then(|p| canonicalize_path_maybe_not_exists(&p).ok())
+                    .and_then(|p| Url::from_file_path(p).ok().map(Arc::new)),
+                )
+              })
+              .collect(),
           },
         )
       })
@@ -129,10 +145,16 @@ impl LspCompilerOptionsResolver {
     self.data.get(key)
   }
 
-  #[allow(clippy::type_complexity)]
   pub fn entries(
     &self,
   ) -> impl Iterator<Item = (&CompilerOptionsKey, &LspCompilerOptionsData)> {
     self.data.iter()
   }
+
+  pub fn is_watched_file(&self, specifier: &Url) -> bool {
+    self
+      .data
+      .values()
+      .any(|d| d.watched_files.contains(specifier))
+  }
 }

filename: cli/lsp/config.rs
patch: @@ -1776,18 +1776,6 @@ impl ConfigTree {
   }
 
   pub fn is_watched_file(&self, specifier: &Url) -> bool {
-    let path = specifier.path();
-    if !path.contains("/node_modules/")
-      && (path.ends_with("/deno.json")
-        || path.ends_with("/deno.jsonc")
-        || path.ends_with("/package.json"))
-      || path.ends_with("/node_modules/.package-lock.json")
-      || path.ends_with("/node_modules/.yarn-integrity.json")
-      || path.ends_with("/node_modules/.modules.yaml")
-      || path.ends_with("/node_modules/.deno/.setup-cache.bin")
-    {
-      return true;
-    }
     self
       .scopes
       .values()

filename: cli/lsp/language_server.rs
patch: @@ -1684,10 +1684,20 @@ impl Inner {
       .into_iter()
       .map(|e| (uri_to_url(&e.uri), e))
       .collect::<Vec<_>>();
-    if changes
-      .iter()
-      .any(|(s, _)| self.config.tree.is_watched_file(s))
-    {
+    if changes.iter().any(|(specifier, _)| {
+      let path = specifier.path();
+      !path.contains("/node_modules/")
+        && (path.ends_with("/deno.json")
+          || path.ends_with("/deno.jsonc")
+          || path.ends_with("/package.json")
+          || path.ends_with("/tsconfig.json"))
+        || path.ends_with("/node_modules/.package-lock.json")
+        || path.ends_with("/node_modules/.yarn-integrity.json")
+        || path.ends_with("/node_modules/.modules.yaml")
+        || path.ends_with("/node_modules/.deno/.setup-cache.bin")
+        || self.config.tree.is_watched_file(specifier)
+        || self.compiler_options_resolver.is_watched_file(specifier)
+    }) {
       let mut deno_config_changes = IndexSet::with_capacity(changes.len());
       let mut changed_deno_json = false;
       deno_config_changes.extend(changes.iter().filter_map(|(s, e)| {

filename: tests/integration/lsp_tests.rs
patch: @@ -17567,6 +17567,48 @@ fn lsp_tsconfig_root_dirs() {
   client.shutdown();
 }
 
+#[test]
+#[timeout(300_000)]
+fn lsp_tsconfig_watched() {
+  let context = TestContextBuilder::new().use_temp_cwd().build();
+  let temp_dir = context.temp_dir();
+  temp_dir.write("deno.json", json!({}).to_string());
+  let file = temp_dir.source_file("main.ts", "document;\n");
+  let mut client = context.new_lsp_command().build();
+  client.initialize_default();
+  let diagnostics = client.did_open_file(&file);
+  assert_eq!(
+    json!(diagnostics.all()),
+    json!([
+      {
+        "range": {
+          "start": { "line": 0, "character": 0 },
+          "end": { "line": 0, "character": 8 },
+        },
+        "severity": 1,
+        "code": 2584,
+        "source": "deno-ts",
+        "message": "Cannot find name 'document'. Do you need to change your target library? Try changing the 'lib' compiler option to include 'dom'.",
+      },
+    ]),
+  );
+  let tsconfig_file = temp_dir.source_file(
+    "tsconfig.json",
+    json!({
+      "compilerOptions": {
+        "lib": ["esnext", "dom"],
+      },
+    })
+    .to_string(),
+  );
+  client.did_change_watched_files(json!({
+    "changes": [{ "uri": tsconfig_file.uri(), "type": 1 }],
+  }));
+  let diagnostics = client.read_diagnostics();
+  assert_eq!(json!(diagnostics.all()), json!([]));
+  client.shutdown();
+}
+
 #[test]
 #[timeout(300_000)]
 fn lsp_npm_workspace() {