Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/evil-snails-arrive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@biomejs/biome": 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.
13 changes: 9 additions & 4 deletions crates/biome_configuration/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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}",)
}
}
Expand All @@ -731,9 +735,10 @@ impl ConfigurationPathHint {
pub fn to_path_buf(&self) -> Option<Utf8PathBuf> {
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()),
}
}
}
Expand Down
7 changes: 2 additions & 5 deletions crates/biome_lsp/src/handlers/text_document.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,11 @@ 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),
false,
)
.load_biome_configuration_file(config_path, false)
.await
} else {
let project_path = path
Expand Down
220 changes: 218 additions & 2 deletions crates/biome_lsp/src/server.tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -435,10 +435,20 @@ async fn wait_for_notification(
}

/// Basic handler for requests and notifications coming from the server for tests
async fn client_handler<I, O>(
async fn client_handler<I, O>(stream: I, sink: O, notify: Sender<ServerNotification>) -> Result<()>
where
I: Stream<Item = Request> + Unpin,
O: Sink<Response> + 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<I, O>(
mut stream: I,
mut sink: O,
mut notify: Sender<ServerNotification>,
settings: WorkspaceSettings,
) -> Result<()>
where
// This function has to be generic as `RequestStream` and `ResponseSink`
Expand Down Expand Up @@ -470,7 +480,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")?;

Expand Down Expand Up @@ -4863,6 +4872,213 @@ 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<Vec<TextEdit>> = 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: String::new(),
}]
);

server.close_document().await?;
server.shutdown().await?;
reader.abort();

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<Vec<TextEdit>> = 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
Expand Down
Loading