summaryrefslogtreecommitdiff
path: root/cli
diff options
context:
space:
mode:
authorDavid Sherret <dsherret@users.noreply.github.com>2023-07-21 09:12:26 -0400
committerGitHub <noreply@github.com>2023-07-21 09:12:26 -0400
commitd6e086d681fd0564bbb8e62744aca0514e3bc575 (patch)
tree5bc4e688c0250283f696c9d0ab68d524ba5a9466 /cli
parentda709729e3dd6f310182581ca1c6380ad51443fc (diff)
fix(lsp): handle watched files events from symlinked config files (#19898)
Related to https://github.com/denoland/vscode_deno/issues/784
Diffstat (limited to 'cli')
-rw-r--r--cli/lsp/config.rs76
-rw-r--r--cli/lsp/language_server.rs79
-rw-r--r--cli/tests/integration/lsp_tests.rs66
3 files changed, 179 insertions, 42 deletions
diff --git a/cli/lsp/config.rs b/cli/lsp/config.rs
index 03464d1f8..2438e8a85 100644
--- a/cli/lsp/config.rs
+++ b/cli/lsp/config.rs
@@ -425,12 +425,19 @@ pub struct Settings {
pub workspace: WorkspaceSettings,
}
+#[derive(Debug)]
+struct WithCanonicalizedSpecifier<T> {
+ /// Stored canonicalized specifier, which is used for file watcher events.
+ canonicalized_specifier: ModuleSpecifier,
+ file: T,
+}
+
/// Contains the config file and dependent information.
#[derive(Debug)]
struct LspConfigFileInfo {
- config_file: ConfigFile,
+ config_file: WithCanonicalizedSpecifier<ConfigFile>,
/// An optional deno.lock file, which is resolved relative to the config file.
- maybe_lockfile: Option<Arc<Mutex<Lockfile>>>,
+ maybe_lockfile: Option<WithCanonicalizedSpecifier<Arc<Mutex<Lockfile>>>>,
/// The canonicalized node_modules directory, which is found relative to the config file.
maybe_node_modules_dir: Option<PathBuf>,
excluded_paths: Vec<Url>,
@@ -469,14 +476,42 @@ impl Config {
}
pub fn maybe_config_file(&self) -> Option<&ConfigFile> {
- self.maybe_config_file_info.as_ref().map(|c| &c.config_file)
+ self
+ .maybe_config_file_info
+ .as_ref()
+ .map(|c| &c.config_file.file)
+ }
+
+ /// Canonicalized specifier of the config file, which should only be used for
+ /// file watcher events. Otherwise, prefer using the non-canonicalized path
+ /// as the rest of the CLI does for config files.
+ pub fn maybe_config_file_canonicalized_specifier(
+ &self,
+ ) -> Option<&ModuleSpecifier> {
+ self
+ .maybe_config_file_info
+ .as_ref()
+ .map(|c| &c.config_file.canonicalized_specifier)
}
pub fn maybe_lockfile(&self) -> Option<&Arc<Mutex<Lockfile>>> {
self
.maybe_config_file_info
.as_ref()
- .and_then(|c| c.maybe_lockfile.as_ref())
+ .and_then(|c| c.maybe_lockfile.as_ref().map(|l| &l.file))
+ }
+
+ /// Canonicalized specifier of the lockfile, which should only be used for
+ /// file watcher events. Otherwise, prefer using the non-canonicalized path
+ /// as the rest of the CLI does for config files.
+ pub fn maybe_lockfile_canonicalized_specifier(
+ &self,
+ ) -> Option<&ModuleSpecifier> {
+ self.maybe_config_file_info.as_ref().and_then(|c| {
+ c.maybe_lockfile
+ .as_ref()
+ .map(|l| &l.canonicalized_specifier)
+ })
}
pub fn clear_config_file(&mut self) {
@@ -485,7 +520,17 @@ impl Config {
pub fn set_config_file(&mut self, config_file: ConfigFile) {
self.maybe_config_file_info = Some(LspConfigFileInfo {
- maybe_lockfile: resolve_lockfile_from_config(&config_file),
+ maybe_lockfile: resolve_lockfile_from_config(&config_file).map(
+ |lockfile| {
+ let path = canonicalize_path_maybe_not_exists(&lockfile.filename)
+ .unwrap_or_else(|_| lockfile.filename.clone());
+ WithCanonicalizedSpecifier {
+ canonicalized_specifier: ModuleSpecifier::from_file_path(path)
+ .unwrap(),
+ file: Arc::new(Mutex::new(lockfile)),
+ }
+ },
+ ),
maybe_node_modules_dir: resolve_node_modules_dir(&config_file),
excluded_paths: config_file
.to_files_config()
@@ -496,7 +541,16 @@ impl Config {
.into_iter()
.filter_map(|path| ModuleSpecifier::from_file_path(path).ok())
.collect(),
- config_file,
+ config_file: WithCanonicalizedSpecifier {
+ canonicalized_specifier: config_file
+ .specifier
+ .to_file_path()
+ .ok()
+ .and_then(|p| canonicalize_path_maybe_not_exists(&p).ok())
+ .and_then(|p| ModuleSpecifier::from_file_path(p).ok())
+ .unwrap_or_else(|| config_file.specifier.clone()),
+ file: config_file,
+ },
});
}
@@ -739,9 +793,7 @@ fn specifier_enabled(
.unwrap_or_else(|| settings.workspace.enable)
}
-fn resolve_lockfile_from_config(
- config_file: &ConfigFile,
-) -> Option<Arc<Mutex<Lockfile>>> {
+fn resolve_lockfile_from_config(config_file: &ConfigFile) -> Option<Lockfile> {
let lockfile_path = match config_file.resolve_lockfile_path() {
Ok(Some(value)) => value,
Ok(None) => return None,
@@ -769,15 +821,13 @@ fn resolve_node_modules_dir(config_file: &ConfigFile) -> Option<PathBuf> {
canonicalize_path_maybe_not_exists(&node_modules_dir).ok()
}
-fn resolve_lockfile_from_path(
- lockfile_path: PathBuf,
-) -> Option<Arc<Mutex<Lockfile>>> {
+fn resolve_lockfile_from_path(lockfile_path: PathBuf) -> Option<Lockfile> {
match Lockfile::new(lockfile_path, false) {
Ok(value) => {
if let Ok(specifier) = ModuleSpecifier::from_file_path(&value.filename) {
lsp_log!(" Resolved lock file: \"{}\"", specifier);
}
- Some(Arc::new(Mutex::new(value)))
+ Some(value)
}
Err(err) => {
lsp_warn!("Error loading lockfile: {:#}", err);
diff --git a/cli/lsp/language_server.rs b/cli/lsp/language_server.rs
index 5a4c3aeae..3ac9610b3 100644
--- a/cli/lsp/language_server.rs
+++ b/cli/lsp/language_server.rs
@@ -1458,18 +1458,8 @@ impl Inner {
&mut self,
params: DidChangeWatchedFilesParams,
) {
- fn has_lockfile_changed(
- lockfile: &Lockfile,
- changed_urls: &HashSet<Url>,
- ) -> bool {
- let lockfile_path = lockfile.filename.clone();
- let Ok(specifier) = ModuleSpecifier::from_file_path(&lockfile_path) else {
- return false;
- };
- if !changed_urls.contains(&specifier) {
- return false;
- }
- match Lockfile::new(lockfile_path, false) {
+ fn has_lockfile_content_changed(lockfile: &Lockfile) -> bool {
+ match Lockfile::new(lockfile.filename.clone(), false) {
Ok(new_lockfile) => {
// only update if the lockfile has changed
FastInsecureHasher::hash(lockfile)
@@ -1482,6 +1472,53 @@ impl Inner {
}
}
+ fn has_config_changed(config: &Config, changes: &HashSet<Url>) -> bool {
+ // Check the canonicalized specifier here because file watcher
+ // changes will be for the canonicalized path in vscode, but also check the
+ // non-canonicalized specifier in order to please the tests and handle
+ // a client that might send that instead.
+ if config
+ .maybe_config_file_canonicalized_specifier()
+ .map(|s| changes.contains(s))
+ .unwrap_or(false)
+ {
+ return true;
+ }
+ match config.maybe_config_file() {
+ Some(file) => {
+ if changes.contains(&file.specifier) {
+ return true;
+ }
+ }
+ None => {
+ // check for auto-discovery
+ if changes.iter().any(|url| {
+ url.path().ends_with("/deno.json")
+ || url.path().ends_with("/deno.jsonc")
+ }) {
+ return true;
+ }
+ }
+ }
+
+ // if the lockfile has changed, reload the config as well
+ if let Some(lockfile) = config.maybe_lockfile() {
+ let lockfile_matches = config
+ .maybe_lockfile_canonicalized_specifier()
+ .map(|s| changes.contains(s))
+ .or_else(|| {
+ ModuleSpecifier::from_file_path(&lockfile.lock().filename)
+ .ok()
+ .map(|s| changes.contains(&s))
+ })
+ .unwrap_or(false);
+ lockfile_matches && has_lockfile_content_changed(&lockfile.lock())
+ } else {
+ // check for auto-discovery
+ changes.iter().any(|url| url.path().ends_with("/deno.lock"))
+ }
+ }
+
let mark = self
.performance
.mark("did_change_watched_files", Some(&params));
@@ -1493,23 +1530,7 @@ impl Inner {
.collect();
// if the current deno.json has changed, we need to reload it
- let has_config_changed = match self.config.maybe_config_file() {
- Some(config_file) => changes.contains(&config_file.specifier),
- None => {
- // check for auto-discovery
- changes.iter().any(|url| {
- url.path().ends_with("/deno.json")
- || url.path().ends_with("/deno.jsonc")
- })
- }
- } || match self.config.maybe_lockfile() {
- Some(lockfile) => has_lockfile_changed(&lockfile.lock(), &changes),
- None => {
- // check for auto-discovery
- changes.iter().any(|url| url.path().ends_with("/deno.lock"))
- }
- };
- if has_config_changed {
+ if has_config_changed(&self.config, &changes) {
if let Err(err) = self.update_config_file().await {
self.client.show_message(MessageType::WARNING, err);
}
diff --git a/cli/tests/integration/lsp_tests.rs b/cli/tests/integration/lsp_tests.rs
index 261c98b52..dc68ab514 100644
--- a/cli/tests/integration/lsp_tests.rs
+++ b/cli/tests/integration/lsp_tests.rs
@@ -545,6 +545,72 @@ fn lsp_import_map_config_file_auto_discovered() {
}
#[test]
+fn lsp_import_map_config_file_auto_discovered_symlink() {
+ let context = TestContextBuilder::new()
+ // DO NOT COPY THIS CODE. Very rare case where we want to force the temp
+ // directory on the CI to not be a symlinked directory because we are
+ // testing a scenario with a symlink to a non-symlink in the same directory
+ // tree. Generally you want to ensure your code works in symlinked directories
+ // so don't use this unless you have a similar scenario.
+ .temp_dir_path(std::env::temp_dir().canonicalize().unwrap())
+ .use_temp_cwd()
+ .build();
+ let temp_dir = context.temp_dir();
+ temp_dir.create_dir_all("lib");
+ temp_dir.write("lib/b.ts", r#"export const b = "b";"#);
+
+ let mut client = context.new_lsp_command().capture_stderr().build();
+ client.initialize_default();
+
+ // now create a symlink in the current directory to a subdir/deno.json
+ // and ensure the watched files notification still works
+ temp_dir.create_dir_all("subdir");
+ temp_dir.write("subdir/deno.json", r#"{ }"#);
+ temp_dir.symlink_file(
+ temp_dir.path().join("subdir").join("deno.json"),
+ temp_dir.path().join("deno.json"),
+ );
+ client.did_change_watched_files(json!({
+ "changes": [{
+ // the client will give a watched file changed event for the symlink's target
+ "uri": temp_dir.path().join("subdir/deno.json").canonicalize().uri(),
+ "type": 2
+ }]
+ }));
+
+ // this will discover the deno.json in the root
+ let search_line = format!(
+ "Auto-resolved configuration file: \"{}\"",
+ temp_dir.uri().join("deno.json").unwrap().as_str()
+ );
+ client.wait_until_stderr_line(|line| line.contains(&search_line));
+
+ // now open a file which will cause a diagnostic because the import map is empty
+ let diagnostics = client.did_open(json!({
+ "textDocument": {
+ "uri": temp_dir.uri().join("a.ts").unwrap(),
+ "languageId": "typescript",
+ "version": 1,
+ "text": "import { b } from \"/~/b.ts\";\n\nconsole.log(b);\n"
+ }
+ }));
+ assert_eq!(diagnostics.all().len(), 1);
+
+ // update the import map to have the imports now
+ temp_dir.write("subdir/deno.json", r#"{ "imports": { "/~/": "./lib/" } }"#);
+ client.did_change_watched_files(json!({
+ "changes": [{
+ // now still say that the target path has changed
+ "uri": temp_dir.path().join("subdir/deno.json").canonicalize().uri(),
+ "type": 2
+ }]
+ }));
+ assert_eq!(client.read_diagnostics().all().len(), 0);
+
+ client.shutdown();
+}
+
+#[test]
fn lsp_deno_task() {
let context = TestContextBuilder::new().use_temp_cwd().build();
let temp_dir = context.temp_dir();