Skip to content

Commit fd68458

Browse files
ematipicoarendjr
andauthored
fix(lsp): require config and code actions (#6557)
Co-authored-by: Arend van Beelen jr. <[email protected]>
1 parent 4869619 commit fd68458

8 files changed

Lines changed: 110 additions & 147 deletions

File tree

.changeset/busy-zebras-smash.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
Fixed a bug where Biome didn't provide all the available code actions when requested by the editor.

.changeset/hot-dragons-count.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
Fixed [#6287](https://github.com/biomejs/biome/issues/6287) where Biome Language Server didn't adhere to the `settings.requireConfiguration` option when pulling diagnostics and code actions.
6+
Note that for this configuration be correctly applied, your editor must support dynamic registration capabilities.

crates/biome_lsp/src/capabilities.rs

Lines changed: 35 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,24 @@ use tower_lsp_server::lsp_types::{
77
WorkspaceServerCapabilities,
88
};
99

10+
pub(crate) const DEFAULT_CODE_ACTION_CAPABILITIES: &[&str] = &[
11+
"quickfix.biome",
12+
// quickfix.suppressRule
13+
SUPPRESSION_TOP_LEVEL_ACTION_CATEGORY,
14+
SUPPRESSION_INLINE_ACTION_CATEGORY,
15+
// import sorting
16+
"source.organizeImports.biome",
17+
// fix all
18+
"source.fixAll.biome",
19+
// general refactors
20+
"refactor.biome",
21+
"refactor.extract.biome",
22+
"refactor.inline.biome",
23+
"refactor.rewrite.biome",
24+
// source actions
25+
"source.biome",
26+
];
27+
1028
/// The capabilities to send from server as part of [`InitializeResult`]
1129
///
1230
/// [`InitializeResult`]: tower_lsp_server::lsp::InitializeResult
@@ -57,31 +75,24 @@ pub(crate) fn server_capabilities(capabilities: &ClientCapabilities) -> ServerCa
5775
.text_document
5876
.as_ref()
5977
.and_then(|text_document| text_document.code_action.as_ref())
60-
.and_then(|code_action| code_action.code_action_literal_support.as_ref())
61-
.map(|_| {
62-
CodeActionOptions {
63-
code_action_kinds: Some(vec![
64-
CodeActionKind::from("quickfix.biome"),
65-
// quickfix.suppressRule
66-
CodeActionKind::from(SUPPRESSION_TOP_LEVEL_ACTION_CATEGORY),
67-
CodeActionKind::from(SUPPRESSION_INLINE_ACTION_CATEGORY),
68-
// import sorting
69-
CodeActionKind::from("source.organizeImports.biome"),
70-
// fix all
71-
CodeActionKind::from("source.fixAll.biome"),
72-
// general refactors
73-
CodeActionKind::from("refactor.biome"),
74-
CodeActionKind::from("refactor.extract.biome"),
75-
CodeActionKind::from("refactor.inline.biome"),
76-
CodeActionKind::from("refactor.rewrite.biome"),
77-
// source actions
78-
CodeActionKind::from("source.biome"),
79-
]),
80-
..Default::default()
78+
.and_then(|code_action| {
79+
if code_action.dynamic_registration.unwrap_or(false) {
80+
None
81+
} else if code_action.code_action_literal_support.as_ref().is_some() {
82+
Some(CodeActionProviderCapability::from(CodeActionOptions {
83+
code_action_kinds: Some(
84+
DEFAULT_CODE_ACTION_CAPABILITIES
85+
.iter()
86+
.map(|item| CodeActionKind::from(*item))
87+
.collect::<Vec<_>>(),
88+
),
89+
..Default::default()
90+
}))
91+
} else {
92+
Some(CodeActionProviderCapability::Simple(true))
8193
}
82-
.into()
83-
})
84-
.or(Some(CodeActionProviderCapability::Simple(true)));
94+
});
95+
8596
ServerCapabilities {
8697
position_encoding: Some(match negotiated_encoding(capabilities) {
8798
PositionEncoding::Utf8 => PositionEncodingKind::UTF8,

crates/biome_lsp/src/handlers/analysis.rs

Lines changed: 17 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use biome_analyze::{
88
SUPPRESSION_TOP_LEVEL_ACTION_CATEGORY, SourceActionKind,
99
};
1010
use biome_configuration::analyzer::RuleSelector;
11-
use biome_diagnostics::{Applicability, Error};
11+
use biome_diagnostics::Error;
1212
use biome_fs::BiomePath;
1313
use biome_lsp_converters::from_proto;
1414
use biome_lsp_converters::line_index::LineIndex;
@@ -96,16 +96,12 @@ pub(crate) fn code_actions(
9696
}
9797

9898
let mut has_fix_all = false;
99-
let mut has_quick_fix = false;
10099
let mut filters = Vec::new();
101100
if let Some(filter) = &params.context.only {
102101
for kind in filter {
103102
let kind = kind.as_str();
104103
if FIX_ALL_CATEGORY.matches(kind) {
105104
has_fix_all = true;
106-
} else if ActionCategory::QuickFix(Cow::Borrowed("")).to_str() == kind {
107-
// The action is a on-save quick-fixes
108-
has_quick_fix = true;
109105
}
110106
filters.push(kind);
111107
}
@@ -190,39 +186,31 @@ pub(crate) fn code_actions(
190186
.actions
191187
.into_iter()
192188
.filter_map(|action| {
193-
debug!("Action: {:?}", &action.category);
194-
// Don't apply unsafe fixes when the code action is on-save quick-fixes
195-
if has_quick_fix && action.suggestion.applicability == Applicability::MaybeIncorrect {
196-
return None;
197-
}
189+
debug!(
190+
"Action: {:?}, and applicability {:?}",
191+
&action.category, &action.suggestion.applicability
192+
);
198193
// Filter out source.organizeImports.biome action when assist is not supported.
199194
if action.category.matches("source.organizeImports.biome")
200195
&& !file_features.supports_assist()
201196
{
202197
return None;
203198
}
204-
// Filter out quickfix.biome action when lint is not supported.
205-
if action.category.matches("quickfix.biome") && !file_features.supports_lint() {
199+
// Filter out quickfix.biome action when lint and assist aren't
200+
if action.category.matches("quickfix.biome")
201+
&& !file_features.supports_lint()
202+
&& !file_features.supports_assist()
203+
{
206204
return None;
207205
}
208206

209-
// Filter out suppressions if the linter isn't supported
207+
// Filter out suppressions if the linter and assist aren't supported
210208
if (action.category.matches(SUPPRESSION_INLINE_ACTION_CATEGORY)
211209
|| action
212210
.category
213211
.matches(SUPPRESSION_TOP_LEVEL_ACTION_CATEGORY))
214212
&& !file_features.supports_lint()
215-
{
216-
return None;
217-
}
218-
219-
// Filter out the suppressions if the client is requesting a fix all signal.
220-
// Fix all should apply only the safe changes.
221-
if has_fix_all
222-
&& (action.category.matches(SUPPRESSION_INLINE_ACTION_CATEGORY)
223-
|| action
224-
.category
225-
.matches(SUPPRESSION_TOP_LEVEL_ACTION_CATEGORY))
213+
&& !file_features.supports_assist()
226214
{
227215
return None;
228216
}
@@ -280,7 +268,7 @@ pub(crate) fn code_actions(
280268
Ok(Some(actions))
281269
}
282270

283-
/// Generate a "fix all" code action for the given document
271+
/// Generate the code action `source.fixAll.biome` for the current document
284272
#[tracing::instrument(level = "debug", skip(session, url))]
285273
fn fix_all(
286274
session: &Session,
@@ -293,7 +281,7 @@ fn fix_all(
293281
let Some(doc) = session.document(url) else {
294282
return Ok(None);
295283
};
296-
let features = FeaturesBuilder::new().with_linter().with_assist().build();
284+
let analyzer_features = FeaturesBuilder::new().with_linter().with_assist().build();
297285

298286
if !session.workspace.file_exists(path.clone().into())? {
299287
return Ok(None);
@@ -302,7 +290,7 @@ fn fix_all(
302290
if session.workspace.is_path_ignored(IsPathIgnoredParams {
303291
path: path.clone(),
304292
project_key: doc.project_key,
305-
features,
293+
features: analyzer_features,
306294
})? {
307295
return Ok(None);
308296
}
@@ -318,11 +306,10 @@ fn fix_all(
318306
})?;
319307
let should_format = file_features.supports_format();
320308

321-
let features = FeaturesBuilder::new().with_linter().with_assist().build();
322309
if session.workspace.is_path_ignored(IsPathIgnoredParams {
323310
path: path.clone(),
324311
project_key: doc.project_key,
325-
features,
312+
features: analyzer_features,
326313
})? {
327314
return Ok(None);
328315
}
@@ -424,7 +411,7 @@ fn fix_all(
424411
};
425412

426413
Ok(Some(CodeActionOrCommand::CodeAction(lsp::CodeAction {
427-
title: String::from("Fix all auto-fixable issues"),
414+
title: String::from("Apply all safe fixes (Biome)"),
428415
kind: Some(fix_all_kind()),
429416
diagnostics: Some(diagnostics),
430417
edit: Some(edit),

crates/biome_lsp/src/server.rs

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::capabilities::server_capabilities;
1+
use crate::capabilities::{DEFAULT_CODE_ACTION_CAPABILITIES, server_capabilities};
22
use crate::diagnostics::{LspError, handle_lsp_error};
33
use crate::requests::syntax_tree::{SYNTAX_TREE_REQUEST, SyntaxTreePayload};
44
use crate::session::{
@@ -30,7 +30,7 @@ use tokio::task::spawn_blocking;
3030
use tower_lsp_server::jsonrpc::Result as LspResult;
3131
use tower_lsp_server::{ClientSocket, UriExt, lsp_types::*};
3232
use tower_lsp_server::{LanguageServer, LspService, Server};
33-
use tracing::{error, info, instrument, warn};
33+
use tracing::{debug, error, info, instrument, warn};
3434

3535
pub struct LSPServer {
3636
pub(crate) session: SessionHandle,
@@ -222,6 +222,28 @@ impl LSPServer {
222222
},
223223
);
224224

225+
let f = self.session.is_linting_and_formatting_disabled();
226+
debug!("Requires configuration: {f}");
227+
capabilities.add_capability(
228+
"biome_code_action",
229+
"textDocument/codeAction",
230+
if self.session.is_linting_and_formatting_disabled() {
231+
CapabilityStatus::Disable
232+
} else {
233+
CapabilityStatus::Enable(Some(json!(CodeActionProviderCapability::from(
234+
CodeActionOptions {
235+
code_action_kinds: Some(
236+
DEFAULT_CODE_ACTION_CAPABILITIES
237+
.iter()
238+
.map(|item| CodeActionKind::from(*item))
239+
.collect::<Vec<_>>(),
240+
),
241+
..Default::default()
242+
}
243+
))))
244+
},
245+
);
246+
225247
self.session.register_capabilities(capabilities).await;
226248
}
227249

crates/biome_lsp/src/server.tests.rs

Lines changed: 7 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1385,88 +1385,6 @@ async fn pull_quick_fixes() -> Result<()> {
13851385
Ok(())
13861386
}
13871387

1388-
#[tokio::test]
1389-
async fn pull_biome_quick_fixes_ignore_unsafe() -> Result<()> {
1390-
let factory = ServerFactory::default();
1391-
let (service, client) = factory.create().into_inner();
1392-
let (stream, sink) = client.split();
1393-
let mut server = Server::new(service);
1394-
1395-
let unsafe_fixable = Diagnostic {
1396-
range: Range {
1397-
start: Position {
1398-
line: 0,
1399-
character: 6,
1400-
},
1401-
end: Position {
1402-
line: 0,
1403-
character: 9,
1404-
},
1405-
},
1406-
severity: Some(DiagnosticSeverity::ERROR),
1407-
code: Some(NumberOrString::String(String::from(
1408-
"lint/suspicious/noDoubleEquals",
1409-
))),
1410-
code_description: None,
1411-
source: Some(String::from("biome")),
1412-
message: String::from("Use === instead of ==."),
1413-
related_information: None,
1414-
tags: None,
1415-
data: None,
1416-
};
1417-
1418-
let (sender, _) = channel(CHANNEL_BUFFER_SIZE);
1419-
let reader = tokio::spawn(client_handler(stream, sink, sender));
1420-
1421-
server.initialize().await?;
1422-
server.initialized().await?;
1423-
1424-
server.open_document("if(a == 0) {}").await?;
1425-
1426-
let res: CodeActionResponse = server
1427-
.request(
1428-
"textDocument/codeAction",
1429-
"pull_code_actions",
1430-
CodeActionParams {
1431-
text_document: TextDocumentIdentifier {
1432-
uri: uri!("document.js"),
1433-
},
1434-
range: Range {
1435-
start: Position {
1436-
line: 0,
1437-
character: 6,
1438-
},
1439-
end: Position {
1440-
line: 0,
1441-
character: 6,
1442-
},
1443-
},
1444-
context: CodeActionContext {
1445-
diagnostics: vec![unsafe_fixable.clone()],
1446-
only: Some(vec![CodeActionKind::new("quickfix.biome")]),
1447-
..Default::default()
1448-
},
1449-
work_done_progress_params: WorkDoneProgressParams {
1450-
work_done_token: None,
1451-
},
1452-
partial_result_params: PartialResultParams {
1453-
partial_result_token: None,
1454-
},
1455-
},
1456-
)
1457-
.await?
1458-
.context("codeAction returned None")?;
1459-
1460-
assert_eq!(res, vec![]);
1461-
1462-
server.close_document().await?;
1463-
1464-
server.shutdown().await?;
1465-
reader.abort();
1466-
1467-
Ok(())
1468-
}
1469-
14701388
#[tokio::test]
14711389
async fn pull_biome_quick_fixes() -> Result<()> {
14721390
let factory = ServerFactory::default();
@@ -1621,7 +1539,12 @@ async fn pull_quick_fixes_include_unsafe() -> Result<()> {
16211539
},
16221540
context: CodeActionContext {
16231541
diagnostics: vec![unsafe_fixable.clone()],
1624-
only: Some(vec![]),
1542+
only: Some(
1543+
DEFAULT_CODE_ACTION_CAPABILITIES
1544+
.iter()
1545+
.map(|s| CodeActionKind::from(*s))
1546+
.collect::<Vec<_>>(),
1547+
),
16251548
..Default::default()
16261549
},
16271550
work_done_progress_params: WorkDoneProgressParams {
@@ -2565,7 +2488,7 @@ async fn pull_fix_all() -> Result<()> {
25652488
);
25662489

25672490
let expected_action = CodeActionOrCommand::CodeAction(CodeAction {
2568-
title: String::from("Fix all auto-fixable issues"),
2491+
title: String::from("Apply all safe fixes (Biome)"),
25692492
kind: Some(CodeActionKind::new("source.fixAll.biome")),
25702493
diagnostics: Some(vec![
25712494
fixable_diagnostic(0)?,

0 commit comments

Comments
 (0)