From 8728e1abd3ad187a9ca1fa56c7204f2bb4ef576b Mon Sep 17 00:00:00 2001 From: g-ortuno Date: Sat, 7 Mar 2026 18:15:49 -0500 Subject: [PATCH 1/7] Fix incorrect project dir when external config is specified fix variable fix test --- crates/biome_lsp/src/server.tests.rs | 123 ++++++++++++++++++++++++++- crates/biome_lsp/src/session.rs | 43 ++++++++-- 2 files changed, 157 insertions(+), 9 deletions(-) diff --git a/crates/biome_lsp/src/server.tests.rs b/crates/biome_lsp/src/server.tests.rs index 0e873cfa4dff..873da536381d 100644 --- a/crates/biome_lsp/src/server.tests.rs +++ b/crates/biome_lsp/src/server.tests.rs @@ -436,9 +436,23 @@ async fn wait_for_notification( /// Basic handler for requests and notifications coming from the server for tests async fn client_handler( + stream: I, + sink: O, + notify: Sender, +) -> Result<()> +where + I: Stream + Unpin, + O: Sink + Unpin, +{ + client_handler_with_settings(stream, sink, notify, WorkspaceSettings::default()).await +} + +/// Handler for requests and notifications coming from the server for tests with custom settings +async fn client_handler_with_settings( mut stream: I, mut sink: O, mut notify: Sender, + settings: WorkspaceSettings, ) -> Result<()> where // This function has to be generic as `RequestStream` and `ResponseSink` @@ -470,7 +484,6 @@ where let res = match req.method() { "workspace/configuration" => { - let settings = WorkspaceSettings::default(); let result = to_value(slice::from_ref(&settings)).context("failed to serialize settings")?; @@ -4863,6 +4876,114 @@ async fn relative_configuration_path_resolves_against_correct_workspace_folder() Ok(()) } +/// Verifies that an absolute `configurationPath` (e.g. `C:/shared-config/biome.json`) +/// pointing to a file outside the workspace roots properly attributes the registered +/// project to the workspace root, so that files opened inside the workspace are +/// matched correctly. +/// +/// Regression test for external absolute config path bug (PR #9049). +#[tokio::test] +async fn absolute_configuration_path_resolves_outside_workspace() -> Result<()> { + let fs = MemoryFileSystem::default(); + + // The config lives outside the workspace. + let external_config_path = to_utf8_file_path_buf( + lsp::Uri::from_str(if cfg!(windows) { + "file:///z%3A/shared-config/biome.json" + } else { + "file:///shared-config/biome.json" + }) + .unwrap(), + ); + + let config = r#"{ + "formatter": { + "enabled": true + } + }"#; + + fs.insert( + external_config_path.clone(), + config, + ); + + let factory = ServerFactory::new_with_fs(Arc::new(fs)); + let (service, client) = factory.create().into_inner(); + let (stream, sink) = client.split(); + let mut server = Server::new(service); + + let (sender, _) = channel(CHANNEL_BUFFER_SIZE); + let settings = WorkspaceSettings { + configuration_path: Some(external_config_path.to_string()), + ..Default::default()}; + + // To reproduce the bug, the initial settings must have + // `configuration_path` set. This matches what happens when an IDE starts. + let reader = tokio::spawn(client_handler_with_settings(stream, sink, sender, settings)); + + + server.initialize().await?; + server.initialized().await?; + + // Open a document inside the workspace. + server.open_document("statement( );\n").await?; + + // The document has extra whitespace, so there should be formatting changes. + let res: Option> = server + .request( + "textDocument/formatting", + "formatting", + DocumentFormattingParams { + text_document: TextDocumentIdentifier { + uri: uri!("document.js"), + }, + options: FormattingOptions { + tab_size: 4, + insert_spaces: false, + properties: HashMap::default(), + trim_trailing_whitespace: None, + insert_final_newline: None, + trim_final_newlines: None, + }, + work_done_progress_params: WorkDoneProgressParams { + work_done_token: None, + }, + }, + ) + .await? + .context("formatting returned None")?; + + assert!( + res.is_some(), + "Expected formatting edits because the external config is enabled and there's extra spaces. \ + If this is None, the configurationPath caused the project to be created with the wrong path." + ); + + let edits = res.unwrap(); + assert_eq!( + edits, + vec![TextEdit { + range: Range { + start: Position { + line: 0, + character: 10, + }, + end: Position { + line: 0, + character: 13, + }, + }, + new_text: "".to_string(), + }] + ); + + server.close_document().await?; + server.shutdown().await?; + reader.abort(); + + Ok(()) +} + // #endregion // #region TEST UTILS diff --git a/crates/biome_lsp/src/session.rs b/crates/biome_lsp/src/session.rs index 009e65f40f5c..a21931ea8ca9 100644 --- a/crates/biome_lsp/src/session.rs +++ b/crates/biome_lsp/src/session.rs @@ -1027,14 +1027,41 @@ impl Session { path.to_path_buf() } ConfigurationPathHint::FromUser(path) => { - if path.is_file() { - path.parent() - .map_or(fs.working_directory().unwrap_or_default(), |p| { - p.to_path_buf() - }) - } else { - path.to_path_buf() - } + // Collect workspace folder roots as absolute Utf8PathBufs. + let workspace_roots: Vec = self + .get_workspace_folders() + .unwrap_or_default() + .into_iter() + .filter_map(|folder| { + folder + .uri + .to_file_path() + .and_then(|p| Utf8PathBuf::from_path_buf(p.to_path_buf()).ok()) + }) + .collect(); + + // Find the workspace folder that contains this config file. + // If it's outside all workspace folders (e.g. an external shared config), + // we should NOT use the config file's directory as the project root, + // because then workspace files won't match the project. + // Instead, we fallback to the first workspace root or the session's base_path. + workspace_roots + .iter() + .filter(|root| path.starts_with(*root)) + .max_by_key(|root| root.as_str().len()) + .cloned() + .or_else(|| workspace_roots.first().cloned()) + .or_else(|| self.base_path()) + .unwrap_or_else(|| { + if fs.path_is_file(path) { + path.parent() + .map_or(fs.working_directory().unwrap_or_default(), |p| { + p.to_path_buf() + }) + } else { + path.to_path_buf() + } + }) } _ => self .base_path() From 16cea277f2e7170a39443367e0b957d35ce20f12 Mon Sep 17 00:00:00 2001 From: g-ortuno Date: Sun, 8 Mar 2026 10:59:21 -0400 Subject: [PATCH 2/7] forfmat and lint --- crates/biome_lsp/src/server.tests.rs | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/crates/biome_lsp/src/server.tests.rs b/crates/biome_lsp/src/server.tests.rs index 873da536381d..f8fb7b2d2d27 100644 --- a/crates/biome_lsp/src/server.tests.rs +++ b/crates/biome_lsp/src/server.tests.rs @@ -435,11 +435,7 @@ async fn wait_for_notification( } /// Basic handler for requests and notifications coming from the server for tests -async fn client_handler( - stream: I, - sink: O, - notify: Sender, -) -> Result<()> +async fn client_handler(stream: I, sink: O, notify: Sender) -> Result<()> where I: Stream + Unpin, O: Sink + Unpin, @@ -4902,10 +4898,7 @@ async fn absolute_configuration_path_resolves_outside_workspace() -> Result<()> } }"#; - fs.insert( - external_config_path.clone(), - config, - ); + fs.insert(external_config_path.clone(), config); let factory = ServerFactory::new_with_fs(Arc::new(fs)); let (service, client) = factory.create().into_inner(); @@ -4914,14 +4907,14 @@ async fn absolute_configuration_path_resolves_outside_workspace() -> Result<()> let (sender, _) = channel(CHANNEL_BUFFER_SIZE); let settings = WorkspaceSettings { - configuration_path: Some(external_config_path.to_string()), - ..Default::default()}; + configuration_path: Some(external_config_path.to_string()), + ..Default::default() + }; // To reproduce the bug, the initial settings must have // `configuration_path` set. This matches what happens when an IDE starts. let reader = tokio::spawn(client_handler_with_settings(stream, sink, sender, settings)); - server.initialize().await?; server.initialized().await?; @@ -4973,7 +4966,7 @@ async fn absolute_configuration_path_resolves_outside_workspace() -> Result<()> character: 13, }, }, - new_text: "".to_string(), + new_text: String::new(), }] ); From c95ea963a5ab99277b98fae02db52cec1f74ae91 Mon Sep 17 00:00:00 2001 From: g-ortuno Date: Sun, 8 Mar 2026 11:04:15 -0400 Subject: [PATCH 3/7] add changesets --- .changeset/evil-snails-arrive.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .changeset/evil-snails-arrive.md diff --git a/.changeset/evil-snails-arrive.md b/.changeset/evil-snails-arrive.md new file mode 100644 index 000000000000..af855c8f2ba9 --- /dev/null +++ b/.changeset/evil-snails-arrive.md @@ -0,0 +1,13 @@ +--- +"@biomejs/biome": minor +"@biomejs/cli-darwin-arm64": minor +"@biomejs/cli-darwin-x64": minor +"@biomejs/cli-linux-arm64": minor +"@biomejs/cli-linux-arm64-musl": minor +"@biomejs/cli-linux-x64": minor +"@biomejs/cli-linux-x64-musl": minor +"@biomejs/cli-win32-arm64": minor +"@biomejs/cli-win32-x64": minor +--- + +fix(lsp): Fix incorrect project dir when configurationPath points to config outside of workspace From 2aaaff3f267d48a472c78baf5a2e714ee03aa278 Mon Sep 17 00:00:00 2001 From: g-ortuno Date: Sun, 8 Mar 2026 11:33:43 -0400 Subject: [PATCH 4/7] address code rabbits comment --- .changeset/evil-snails-arrive.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.changeset/evil-snails-arrive.md b/.changeset/evil-snails-arrive.md index af855c8f2ba9..14ee95580126 100644 --- a/.changeset/evil-snails-arrive.md +++ b/.changeset/evil-snails-arrive.md @@ -1,13 +1,13 @@ --- -"@biomejs/biome": minor -"@biomejs/cli-darwin-arm64": minor -"@biomejs/cli-darwin-x64": minor -"@biomejs/cli-linux-arm64": minor -"@biomejs/cli-linux-arm64-musl": minor -"@biomejs/cli-linux-x64": minor -"@biomejs/cli-linux-x64-musl": minor -"@biomejs/cli-win32-arm64": minor -"@biomejs/cli-win32-x64": minor +"@biomejs/biome": patch +"@biomejs/cli-darwin-arm64": patch +"@biomejs/cli-darwin-x64": patch +"@biomejs/cli-linux-arm64": patch +"@biomejs/cli-linux-arm64-musl": patch +"@biomejs/cli-linux-x64": patch +"@biomejs/cli-linux-x64-musl": patch +"@biomejs/cli-win32-arm64": patch +"@biomejs/cli-win32-x64": patch --- -fix(lsp): Fix incorrect project dir when configurationPath points to config outside of workspace +Fixed [biomejs/biome-vscode#959](https://github.com/biomejs/biome-vscode/issues/959): LSP now correctly resolves project directory when `configurationPath` points to a configuration file outside the workspace. From cab99de773f44644299e2ac66eac1a6ca5b6931c Mon Sep 17 00:00:00 2001 From: g-ortuno Date: Sun, 8 Mar 2026 18:49:11 -0400 Subject: [PATCH 5/7] only include main package --- .changeset/evil-snails-arrive.md | 8 -------- 1 file changed, 8 deletions(-) diff --git a/.changeset/evil-snails-arrive.md b/.changeset/evil-snails-arrive.md index 14ee95580126..9d426687311b 100644 --- a/.changeset/evil-snails-arrive.md +++ b/.changeset/evil-snails-arrive.md @@ -1,13 +1,5 @@ --- "@biomejs/biome": patch -"@biomejs/cli-darwin-arm64": patch -"@biomejs/cli-darwin-x64": patch -"@biomejs/cli-linux-arm64": patch -"@biomejs/cli-linux-arm64-musl": patch -"@biomejs/cli-linux-x64": patch -"@biomejs/cli-linux-x64-musl": patch -"@biomejs/cli-win32-arm64": patch -"@biomejs/cli-win32-x64": patch --- Fixed [biomejs/biome-vscode#959](https://github.com/biomejs/biome-vscode/issues/959): LSP now correctly resolves project directory when `configurationPath` points to a configuration file outside the workspace. From c38ac27a13e45d03b290a8562e560612c4853308 Mon Sep 17 00:00:00 2001 From: g-ortuno Date: Mon, 9 Mar 2026 21:20:39 -0400 Subject: [PATCH 6/7] implement new approach --- crates/biome_configuration/src/lib.rs | 13 ++- .../biome_lsp/src/handlers/text_document.rs | 4 +- crates/biome_lsp/src/session.rs | 88 ++++++++----------- crates/biome_service/src/configuration.rs | 4 +- 4 files changed, 51 insertions(+), 58 deletions(-) diff --git a/crates/biome_configuration/src/lib.rs b/crates/biome_configuration/src/lib.rs index dd6a92aae92d..592132f4dbec 100644 --- a/crates/biome_configuration/src/lib.rs +++ b/crates/biome_configuration/src/lib.rs @@ -701,6 +701,10 @@ pub enum ConfigurationPathHint { /// The path can either be a directory path or a file path. /// Throws any kind of I/O errors. FromUser(Utf8PathBuf), + + /// Very similar to [ConfigurationPathHint::FromUser]. However, this variant is used to indicate + /// that the configuration is outside of the current workspace. + FromUserExternal(Utf8PathBuf), } impl Display for ConfigurationPathHint { @@ -713,7 +717,7 @@ impl Display for ConfigurationPathHint { Self::FromLsp(path) => { write!(fmt, "Configuration path provided from the LSP: {path}",) } - Self::FromUser(path) => { + Self::FromUser(path) | Self::FromUserExternal(path) => { write!(fmt, "Configuration path provided by the user: {path}",) } } @@ -731,9 +735,10 @@ impl ConfigurationPathHint { pub fn to_path_buf(&self) -> Option { match self { Self::None => None, - Self::FromWorkspace(path) | Self::FromLsp(path) | Self::FromUser(path) => { - Some(path.to_path_buf()) - } + Self::FromWorkspace(path) + | Self::FromLsp(path) + | Self::FromUser(path) + | Self::FromUserExternal(path) => Some(path.to_path_buf()), } } } diff --git a/crates/biome_lsp/src/handlers/text_document.rs b/crates/biome_lsp/src/handlers/text_document.rs index 67c57a372e73..ada728c3389c 100644 --- a/crates/biome_lsp/src/handlers/text_document.rs +++ b/crates/biome_lsp/src/handlers/text_document.rs @@ -45,12 +45,12 @@ pub(crate) async fn did_open( let status = if let Some(config_path) = session.resolve_configuration_path(Some(&path)) { info!( - "Loading user configuration from text_document {}", + "Loading user configuration from text_document {:?}", &config_path ); session .load_biome_configuration_file( - ConfigurationPathHint::FromUser(config_path), + config_path, false, ) .await diff --git a/crates/biome_lsp/src/session.rs b/crates/biome_lsp/src/session.rs index a21931ea8ca9..deecad36993a 100644 --- a/crates/biome_lsp/src/session.rs +++ b/crates/biome_lsp/src/session.rs @@ -700,13 +700,9 @@ impl Session { pub(crate) fn resolve_configuration_path( &self, file_path: Option<&Utf8PathBuf>, - ) -> Option { + ) -> Option { let config_path = self.get_settings_configuration_path()?; - if config_path.is_absolute() { - return Some(config_path); - } - // Collect workspace folder roots as absolute Utf8PathBufs. let workspace_roots: Vec = self .get_workspace_folders() @@ -720,6 +716,16 @@ impl Session { }) .collect(); + if config_path.is_absolute() { + return Some( + if workspace_roots.iter().any(|root| config_path.starts_with(root)) { + ConfigurationPathHint::FromUser(config_path) + } else { + ConfigurationPathHint::FromUserExternal(config_path) + }, + ) + } + if let Some(file_path) = file_path { // Find the workspace folder that contains this file and resolve // the relative config path against that folder. @@ -728,7 +734,12 @@ impl Session { .filter(|root| file_path.starts_with(*root)) .max_by_key(|root| root.as_str().len()) { - return Some(root.join(&config_path)); + let joined = root.join(config_path); + return Some(if workspace_roots.iter().any(|r| joined.starts_with(r)) { + ConfigurationPathHint::FromUser(joined) + } else { + ConfigurationPathHint::FromUserExternal(joined) + }) } } @@ -739,17 +750,17 @@ impl Session { // correct per-file resolution happens in `did_open` where the file // path is available. if let Some(root) = workspace_roots.first() { - return Some(root.join(&config_path)); + return Some(ConfigurationPathHint::FromUserExternal(root.join(&config_path))); } // Fall back to the (deprecated) root_uri base path. if let Some(base) = self.base_path() { - return Some(base.join(&config_path)); + return Some(ConfigurationPathHint::FromUserExternal(base.join(&config_path))); } // Nothing to resolve against; return the path unchanged and let the // downstream loader produce a meaningful error. - Some(config_path) + Some(ConfigurationPathHint::FromUserExternal(config_path)) } /// This function attempts to read the `biome.json` configuration file from @@ -761,7 +772,7 @@ impl Session { self.set_configuration_status(ConfigurationStatus::Loading); let status = self - .load_biome_configuration_file(ConfigurationPathHint::FromUser(config_path), reload) + .load_biome_configuration_file(config_path, reload) .await; debug!("Configuration status: {:?}", status); self.set_configuration_status(status); @@ -1022,58 +1033,33 @@ impl Session { // the working directory. Otherwise, the base path of the session is used, then the current // working directory is used as the last resort. debug!("Configuration path provided {:?}", &base_path); - let path = match &base_path { + let project_path = match &base_path { ConfigurationPathHint::FromLsp(path) | ConfigurationPathHint::FromWorkspace(path) => { path.to_path_buf() } ConfigurationPathHint::FromUser(path) => { - // Collect workspace folder roots as absolute Utf8PathBufs. - let workspace_roots: Vec = self - .get_workspace_folders() - .unwrap_or_default() - .into_iter() - .filter_map(|folder| { - folder - .uri - .to_file_path() - .and_then(|p| Utf8PathBuf::from_path_buf(p.to_path_buf()).ok()) - }) - .collect(); - - // Find the workspace folder that contains this config file. - // If it's outside all workspace folders (e.g. an external shared config), - // we should NOT use the config file's directory as the project root, - // because then workspace files won't match the project. - // Instead, we fallback to the first workspace root or the session's base_path. - workspace_roots - .iter() - .filter(|root| path.starts_with(*root)) - .max_by_key(|root| root.as_str().len()) - .cloned() - .or_else(|| workspace_roots.first().cloned()) - .or_else(|| self.base_path()) - .unwrap_or_else(|| { - if fs.path_is_file(path) { - path.parent() - .map_or(fs.working_directory().unwrap_or_default(), |p| { - p.to_path_buf() - }) - } else { - path.to_path_buf() - } - }) + if fs.path_is_file(path) { + path.parent() + .map_or(fs.working_directory().unwrap_or_default(), |p| { + p.to_path_buf() + }) + } else { + path.to_path_buf() + } } - _ => self + ConfigurationPathHint::FromUserExternal(_) | _ => { + self .base_path() .or_else(|| fs.working_directory()) - .unwrap_or_default(), + .unwrap_or_default() + } }; - let project_key = match self.project_for_path(&path) { + let project_key = match self.project_for_path(&project_path) { Some(project_key) => project_key, None => { let register_result = self.workspace.open_project(OpenProjectParams { - path: path.as_path().into(), + path: project_path.as_path().into(), open_uninitialized: true, }); let OpenProjectResult { project_key } = match register_result { @@ -1106,7 +1092,7 @@ impl Session { module_graph_resolution_kind: ModuleGraphResolutionKind::from(&scan_kind), }); - self.insert_and_scan_project(project_key, path.into(), scan_kind, force) + self.insert_and_scan_project(project_key, project_path.into(), scan_kind, force) .await; if let Err(WorkspaceError::PluginErrors(error)) = result { diff --git a/crates/biome_service/src/configuration.rs b/crates/biome_service/src/configuration.rs index af05a98012c1..cccad909cfc6 100644 --- a/crates/biome_service/src/configuration.rs +++ b/crates/biome_service/src/configuration.rs @@ -259,6 +259,7 @@ pub fn read_config( ConfigurationPathHint::FromLsp(path) => path.clone(), ConfigurationPathHint::FromWorkspace(path) => path.clone(), ConfigurationPathHint::FromUser(path) => path.clone(), + ConfigurationPathHint::FromUserExternal(path) => path.clone(), ConfigurationPathHint::None => fs.working_directory().unwrap_or_default(), }; @@ -267,7 +268,8 @@ pub fn read_config( let configuration_directory = match path_hint { ConfigurationPathHint::FromLsp(path) => path, ConfigurationPathHint::FromWorkspace(path) => path, - ConfigurationPathHint::FromUser(ref config_file_path) => { + ConfigurationPathHint::FromUser(ref config_file_path) + | ConfigurationPathHint::FromUserExternal(ref config_file_path) => { // If the configuration path hint is from the user, we'll load it // directly. return load_user_config(fs, config_file_path, external_resolution_base_path); From 8bcd6971079a4a0e59d8c24c38e40377d7db4a1d Mon Sep 17 00:00:00 2001 From: g-ortuno Date: Tue, 10 Mar 2026 20:54:54 -0400 Subject: [PATCH 7/7] handle external relative paths --- .../biome_lsp/src/handlers/text_document.rs | 5 +- crates/biome_lsp/src/server.tests.rs | 102 ++++++++++++++++++ crates/biome_lsp/src/session.rs | 38 ++++--- 3 files changed, 121 insertions(+), 24 deletions(-) diff --git a/crates/biome_lsp/src/handlers/text_document.rs b/crates/biome_lsp/src/handlers/text_document.rs index ada728c3389c..0e0e46d030af 100644 --- a/crates/biome_lsp/src/handlers/text_document.rs +++ b/crates/biome_lsp/src/handlers/text_document.rs @@ -49,10 +49,7 @@ pub(crate) async fn did_open( &config_path ); session - .load_biome_configuration_file( - config_path, - false, - ) + .load_biome_configuration_file(config_path, false) .await } else { let project_path = path diff --git a/crates/biome_lsp/src/server.tests.rs b/crates/biome_lsp/src/server.tests.rs index f8fb7b2d2d27..c688a6337bfc 100644 --- a/crates/biome_lsp/src/server.tests.rs +++ b/crates/biome_lsp/src/server.tests.rs @@ -4977,6 +4977,108 @@ async fn absolute_configuration_path_resolves_outside_workspace() -> Result<()> Ok(()) } +/// Verifies that a relative `configurationPath` (e.g. `../shared-config/biome.json`) +/// that resolves to a file outside the workspace roots properly attributes the registered +/// project to the workspace root, so that files opened inside the workspace are +/// matched correctly. +/// +/// Same scenario as [absolute_configuration_path_resolves_outside_workspace] but with +/// a relative path instead of an absolute one. +#[tokio::test] +async fn relative_configuration_path_resolves_outside_workspace() -> Result<()> { + let fs = MemoryFileSystem::default(); + + let absolute_external_config_path = to_utf8_file_path_buf( + lsp::Uri::from_str(if cfg!(windows) { + "file:///z%3A/shared-config/biome.json" + } else { + "file:///shared-config/biome.json" + }) + .unwrap(), + ); + + let config = r#"{ + "formatter": { + "enabled": true + } + }"#; + fs.insert(absolute_external_config_path, config); + + let factory = ServerFactory::new_with_fs(Arc::new(fs)); + let (service, client) = factory.create().into_inner(); + let (stream, sink) = client.split(); + let mut server = Server::new(service); + + let (sender, _) = channel(CHANNEL_BUFFER_SIZE); + let settings = WorkspaceSettings { + configuration_path: Some("../shared-config/biome.json".to_string()), + ..Default::default() + }; + + let reader = tokio::spawn(client_handler_with_settings(stream, sink, sender, settings)); + + server.initialize().await?; + server.initialized().await?; + + // Open a document inside the workspace. + server.open_document("statement( );\n").await?; + + // The document has extra whitespace, so there should be formatting changes. + let res: Option> = server + .request( + "textDocument/formatting", + "formatting", + DocumentFormattingParams { + text_document: TextDocumentIdentifier { + uri: uri!("document.js"), + }, + options: FormattingOptions { + tab_size: 4, + insert_spaces: false, + properties: HashMap::default(), + trim_trailing_whitespace: None, + insert_final_newline: None, + trim_final_newlines: None, + }, + work_done_progress_params: WorkDoneProgressParams { + work_done_token: None, + }, + }, + ) + .await? + .context("formatting returned None")?; + + assert!( + res.is_some(), + "Expected formatting edits because the external config is enabled and there's extra spaces. \ + If this is None, the relative configurationPath caused the project to be created with the wrong path." + ); + + let edits = res.unwrap(); + assert_eq!( + edits, + vec![TextEdit { + range: Range { + start: Position { + line: 0, + character: 10, + }, + end: Position { + line: 0, + character: 13, + }, + }, + new_text: String::new(), + }] + ); + + server.close_document().await?; + server.shutdown().await?; + reader.abort(); + + Ok(()) +} + // #endregion // #region TEST UTILS diff --git a/crates/biome_lsp/src/session.rs b/crates/biome_lsp/src/session.rs index deecad36993a..6bb78e7d700d 100644 --- a/crates/biome_lsp/src/session.rs +++ b/crates/biome_lsp/src/session.rs @@ -8,7 +8,7 @@ use biome_configuration::{Configuration, ConfigurationPathHint}; use biome_console::markup; use biome_deserialize::Merge; use biome_diagnostics::PrintDescription; -use biome_fs::BiomePath; +use biome_fs::{BiomePath, normalize_path}; use biome_line_index::WideEncoding; use biome_lsp_converters::{PositionEncoding, negotiated_encoding}; use biome_service::Workspace; @@ -716,14 +716,16 @@ impl Session { }) .collect(); + let hint_for_path = |path: Utf8PathBuf| { + Some(if workspace_roots.iter().any(|r| path.starts_with(r)) { + ConfigurationPathHint::FromUser(path) + } else { + ConfigurationPathHint::FromUserExternal(path) + }) + }; + if config_path.is_absolute() { - return Some( - if workspace_roots.iter().any(|root| config_path.starts_with(root)) { - ConfigurationPathHint::FromUser(config_path) - } else { - ConfigurationPathHint::FromUserExternal(config_path) - }, - ) + return hint_for_path(config_path); } if let Some(file_path) = file_path { @@ -734,12 +736,8 @@ impl Session { .filter(|root| file_path.starts_with(*root)) .max_by_key(|root| root.as_str().len()) { - let joined = root.join(config_path); - return Some(if workspace_roots.iter().any(|r| joined.starts_with(r)) { - ConfigurationPathHint::FromUser(joined) - } else { - ConfigurationPathHint::FromUserExternal(joined) - }) + let joined = normalize_path(&root.join(config_path)); + return hint_for_path(joined); } } @@ -750,12 +748,14 @@ impl Session { // correct per-file resolution happens in `did_open` where the file // path is available. if let Some(root) = workspace_roots.first() { - return Some(ConfigurationPathHint::FromUserExternal(root.join(&config_path))); + let joined = normalize_path(&root.join(&config_path)); + return hint_for_path(joined); } // Fall back to the (deprecated) root_uri base path. if let Some(base) = self.base_path() { - return Some(ConfigurationPathHint::FromUserExternal(base.join(&config_path))); + let joined = normalize_path(&base.join(&config_path)); + return hint_for_path(joined); } // Nothing to resolve against; return the path unchanged and let the @@ -1047,12 +1047,10 @@ impl Session { path.to_path_buf() } } - ConfigurationPathHint::FromUserExternal(_) | _ => { - self + _ => self .base_path() .or_else(|| fs.working_directory()) - .unwrap_or_default() - } + .unwrap_or_default(), }; let project_key = match self.project_for_path(&project_path) {