Skip to content

Commit be84f85

Browse files
committed
feat: gate custom clint-side commands behind capabilities
Some features of rust-analyzer requires support for custom commands on the client side. Specifically, hover & code lens need this. Stock LSP doesn't have a way for the server to know which client-side commands are available. For that reason, we historically were just sending the commands, not worrying whether the client supports then or not. That's not really great though, so in this PR we add infrastructure for the client to explicitly opt-into custom commands, via `extensions` field of the ClientCapabilities. To preserve backwards compatability, if the client doesn't set the field, we assume that it does support all custom commands. In the future, we'll start treating that case as if the client doesn't support commands. So, if you maintain a rust-analyzer client and implement `rust-analyzer/runSingle` and such, please also advertise this via a capability.
1 parent 956e205 commit be84f85

File tree

8 files changed

+127
-124
lines changed

8 files changed

+127
-124
lines changed

crates/rust-analyzer/src/config.rs

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,12 @@ use serde::{de::DeserializeOwned, Deserialize};
2525
use vfs::AbsPathBuf;
2626

2727
use crate::{
28-
caps::completion_item_edit_resolve, diagnostics::DiagnosticsMapConfig,
29-
line_index::OffsetEncoding, lsp_ext::supports_utf8, lsp_ext::WorkspaceSymbolSearchKind,
28+
caps::completion_item_edit_resolve,
29+
diagnostics::DiagnosticsMapConfig,
30+
line_index::OffsetEncoding,
31+
lsp_ext::supports_utf8,
3032
lsp_ext::WorkspaceSymbolSearchScope,
33+
lsp_ext::{self, WorkspaceSymbolSearchKind},
3134
};
3235

3336
// Defines the server-side configuration of the rust-analyzer. We generate
@@ -221,6 +224,9 @@ config_data! {
221224
/// Whether to show `References` lens. Only applies when
222225
/// `#rust-analyzer.lens.enable#` is set.
223226
lens_references: bool = "false",
227+
/// Internal config: use custom client-side commands even when the
228+
/// client doesn't set the corresponding capability.
229+
lens_forceCustomCommands: bool = "true",
224230

225231
/// Disable project auto-discovery in favor of explicitly specified set
226232
/// of projects.
@@ -405,6 +411,14 @@ pub struct WorkspaceSymbolConfig {
405411
pub search_kind: WorkspaceSymbolSearchKind,
406412
}
407413

414+
pub struct ClientCommandsConfig {
415+
pub run_single: bool,
416+
pub debug_single: bool,
417+
pub show_reference: bool,
418+
pub goto_location: bool,
419+
pub trigger_parameter_hints: bool,
420+
}
421+
408422
impl Config {
409423
pub fn new(root_path: AbsPathBuf, caps: ClientCapabilities) -> Self {
410424
Config {
@@ -858,6 +872,24 @@ impl Config {
858872
false
859873
)
860874
}
875+
pub fn client_commands(&self) -> ClientCommandsConfig {
876+
let commands =
877+
try_or!(self.caps.experimental.as_ref()?.get("commands")?, &serde_json::Value::Null);
878+
let commands: Option<lsp_ext::ClientCommandOptions> =
879+
serde_json::from_value(commands.clone()).ok();
880+
let force = commands.is_none() && self.data.lens_forceCustomCommands;
881+
let commands = commands.map(|it| it.commands).unwrap_or_default();
882+
883+
let get = |name: &str| commands.iter().any(|it| it == name) || force;
884+
885+
ClientCommandsConfig {
886+
run_single: get("rust-analyzer.runSingle"),
887+
debug_single: get("rust-analyzer.debugSingle"),
888+
show_reference: get("rust-analyzer.showReferences"),
889+
goto_location: get("rust-analyzer.gotoLocation"),
890+
trigger_parameter_hints: get("editor.action.triggerParameterHints"),
891+
}
892+
}
861893

862894
pub fn highlight_related(&self) -> HighlightRelatedConfig {
863895
HighlightRelatedConfig {

crates/rust-analyzer/src/handlers.rs

Lines changed: 31 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -768,13 +768,8 @@ pub(crate) fn handle_completion(
768768
};
769769
let line_index = snap.file_line_index(position.file_id)?;
770770

771-
let items = to_proto::completion_items(
772-
snap.config.insert_replace_support(),
773-
completion_config.enable_imports_on_the_fly,
774-
&line_index,
775-
text_document_position,
776-
items,
777-
);
771+
let items =
772+
to_proto::completion_items(&snap.config, &line_index, text_document_position, items);
778773

779774
let completion_list = lsp_types::CompletionList { is_incomplete: true, items };
780775
Ok(Some(completion_list.into()))
@@ -1503,7 +1498,7 @@ fn show_impl_command_link(
15031498
snap: &GlobalStateSnapshot,
15041499
position: &FilePosition,
15051500
) -> Option<lsp_ext::CommandLinkGroup> {
1506-
if snap.config.hover_actions().implementations {
1501+
if snap.config.hover_actions().implementations && snap.config.client_commands().show_reference {
15071502
if let Some(nav_data) = snap.analysis.goto_implementation(*position).unwrap_or(None) {
15081503
let uri = to_proto::url(snap, position.file_id);
15091504
let line_index = snap.file_line_index(position.file_id).ok()?;
@@ -1529,7 +1524,7 @@ fn show_ref_command_link(
15291524
snap: &GlobalStateSnapshot,
15301525
position: &FilePosition,
15311526
) -> Option<lsp_ext::CommandLinkGroup> {
1532-
if snap.config.hover_actions().references {
1527+
if snap.config.hover_actions().references && snap.config.client_commands().show_reference {
15331528
if let Some(ref_search_res) = snap.analysis.find_all_refs(*position, None).unwrap_or(None) {
15341529
let uri = to_proto::url(snap, position.file_id);
15351530
let line_index = snap.file_line_index(position.file_id).ok()?;
@@ -1559,35 +1554,47 @@ fn runnable_action_links(
15591554
snap: &GlobalStateSnapshot,
15601555
runnable: Runnable,
15611556
) -> Option<lsp_ext::CommandLinkGroup> {
1562-
let cargo_spec = CargoTargetSpec::for_file(snap, runnable.nav.file_id).ok()?;
15631557
let hover_actions_config = snap.config.hover_actions();
1564-
if !hover_actions_config.runnable() || should_skip_target(&runnable, cargo_spec.as_ref()) {
1558+
if !hover_actions_config.runnable() {
1559+
return None;
1560+
}
1561+
1562+
let cargo_spec = CargoTargetSpec::for_file(snap, runnable.nav.file_id).ok()?;
1563+
if should_skip_target(&runnable, cargo_spec.as_ref()) {
1564+
return None;
1565+
}
1566+
1567+
let client_commands_config = snap.config.client_commands();
1568+
if !(client_commands_config.run_single || client_commands_config.debug_single) {
15651569
return None;
15661570
}
15671571

15681572
let title = runnable.title();
1569-
to_proto::runnable(snap, runnable).ok().map(|r| {
1570-
let mut group = lsp_ext::CommandLinkGroup::default();
1573+
let r = to_proto::runnable(snap, runnable).ok()?;
15711574

1572-
if hover_actions_config.run {
1573-
let run_command = to_proto::command::run_single(&r, &title);
1574-
group.commands.push(to_command_link(run_command, r.label.clone()));
1575-
}
1575+
let mut group = lsp_ext::CommandLinkGroup::default();
15761576

1577-
if hover_actions_config.debug {
1578-
let dbg_command = to_proto::command::debug_single(&r);
1579-
group.commands.push(to_command_link(dbg_command, r.label));
1580-
}
1577+
if hover_actions_config.run && client_commands_config.run_single {
1578+
let run_command = to_proto::command::run_single(&r, &title);
1579+
group.commands.push(to_command_link(run_command, r.label.clone()));
1580+
}
15811581

1582-
group
1583-
})
1582+
if hover_actions_config.debug && client_commands_config.debug_single {
1583+
let dbg_command = to_proto::command::debug_single(&r);
1584+
group.commands.push(to_command_link(dbg_command, r.label));
1585+
}
1586+
1587+
Some(group)
15841588
}
15851589

15861590
fn goto_type_action_links(
15871591
snap: &GlobalStateSnapshot,
15881592
nav_targets: &[HoverGotoTypeData],
15891593
) -> Option<lsp_ext::CommandLinkGroup> {
1590-
if !snap.config.hover_actions().goto_type_def || nav_targets.is_empty() {
1594+
if !snap.config.hover_actions().goto_type_def
1595+
|| nav_targets.is_empty()
1596+
|| !snap.config.client_commands().goto_location
1597+
{
15911598
return None;
15921599
}
15931600

crates/rust-analyzer/src/lsp_ext.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -523,3 +523,8 @@ pub struct CompletionResolveData {
523523
pub full_import_path: String,
524524
pub imported_name: String,
525525
}
526+
527+
#[derive(Debug, Deserialize, Default)]
528+
pub struct ClientCommandOptions {
529+
pub commands: Vec<String>,
530+
}

crates/rust-analyzer/src/to_proto.rs

Lines changed: 16 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ use vfs::AbsPath;
1818

1919
use crate::{
2020
cargo_target_spec::CargoTargetSpec,
21+
config::Config,
2122
global_state::GlobalStateSnapshot,
2223
line_index::{LineEndings, LineIndex, OffsetEncoding},
2324
lsp_ext, semantic_tokens, Result,
@@ -190,32 +191,22 @@ pub(crate) fn snippet_text_edit_vec(
190191
}
191192

192193
pub(crate) fn completion_items(
193-
insert_replace_support: bool,
194-
enable_imports_on_the_fly: bool,
194+
config: &Config,
195195
line_index: &LineIndex,
196196
tdpp: lsp_types::TextDocumentPositionParams,
197197
items: Vec<CompletionItem>,
198198
) -> Vec<lsp_types::CompletionItem> {
199199
let max_relevance = items.iter().map(|it| it.relevance().score()).max().unwrap_or_default();
200200
let mut res = Vec::with_capacity(items.len());
201201
for item in items {
202-
completion_item(
203-
&mut res,
204-
insert_replace_support,
205-
enable_imports_on_the_fly,
206-
line_index,
207-
&tdpp,
208-
max_relevance,
209-
item,
210-
)
202+
completion_item(&mut res, config, line_index, &tdpp, max_relevance, item)
211203
}
212204
res
213205
}
214206

215207
fn completion_item(
216208
acc: &mut Vec<lsp_types::CompletionItem>,
217-
insert_replace_support: bool,
218-
enable_imports_on_the_fly: bool,
209+
config: &Config,
219210
line_index: &LineIndex,
220211
tdpp: &lsp_types::TextDocumentPositionParams,
221212
max_relevance: u32,
@@ -230,7 +221,7 @@ fn completion_item(
230221
let source_range = item.source_range();
231222
for indel in item.text_edit().iter() {
232223
if indel.delete.contains_range(source_range) {
233-
let insert_replace_support = insert_replace_support.then(|| tdpp.position);
224+
let insert_replace_support = config.insert_replace_support().then(|| tdpp.position);
234225
text_edit = Some(if indel.delete == source_range {
235226
self::completion_text_edit(line_index, insert_replace_support, indel.clone())
236227
} else {
@@ -269,14 +260,14 @@ fn completion_item(
269260
lsp_item.tags = Some(vec![lsp_types::CompletionItemTag::Deprecated])
270261
}
271262

272-
if item.trigger_call_info() {
263+
if item.trigger_call_info() && config.client_commands().trigger_parameter_hints {
273264
lsp_item.command = Some(command::trigger_parameter_hints());
274265
}
275266

276267
if item.is_snippet() {
277268
lsp_item.insert_text_format = Some(lsp_types::InsertTextFormat::Snippet);
278269
}
279-
if enable_imports_on_the_fly {
270+
if config.completion().enable_imports_on_the_fly {
280271
if let Some(import_edit) = item.import_to_add() {
281272
let import_path = &import_edit.import.import_path;
282273
if let Some(import_name) = import_path.segments().last() {
@@ -992,6 +983,7 @@ pub(crate) fn code_lens(
992983
snap: &GlobalStateSnapshot,
993984
annotation: Annotation,
994985
) -> Result<()> {
986+
let client_commands_config = snap.config.client_commands();
995987
match annotation.kind {
996988
AnnotationKind::Runnable(run) => {
997989
let line_index = snap.file_line_index(run.nav.file_id)?;
@@ -1008,15 +1000,15 @@ pub(crate) fn code_lens(
10081000
let r = runnable(snap, run)?;
10091001

10101002
let lens_config = snap.config.lens();
1011-
if lens_config.run {
1003+
if lens_config.run && client_commands_config.run_single {
10121004
let command = command::run_single(&r, &title);
10131005
acc.push(lsp_types::CodeLens {
10141006
range: annotation_range,
10151007
command: Some(command),
10161008
data: None,
10171009
})
10181010
}
1019-
if lens_config.debug && can_debug {
1011+
if lens_config.debug && can_debug && client_commands_config.debug_single {
10201012
let command = command::debug_single(&r);
10211013
acc.push(lsp_types::CodeLens {
10221014
range: annotation_range,
@@ -1026,6 +1018,9 @@ pub(crate) fn code_lens(
10261018
}
10271019
}
10281020
AnnotationKind::HasImpls { position: file_position, data } => {
1021+
if !client_commands_config.show_reference {
1022+
return Ok(());
1023+
}
10291024
let line_index = snap.file_line_index(file_position.file_id)?;
10301025
let annotation_range = range(&line_index, annotation.range);
10311026
let url = url(snap, file_position.file_id);
@@ -1069,6 +1064,9 @@ pub(crate) fn code_lens(
10691064
})
10701065
}
10711066
AnnotationKind::HasReferences { position: file_position, data } => {
1067+
if !client_commands_config.show_reference {
1068+
return Ok(());
1069+
}
10721070
let line_index = snap.file_line_index(file_position.file_id)?;
10731071
let annotation_range = range(&line_index, annotation.range);
10741072
let url = url(snap, file_position.file_id);
@@ -1207,88 +1205,9 @@ mod tests {
12071205
use std::sync::Arc;
12081206

12091207
use ide::Analysis;
1210-
use ide_db::helpers::{
1211-
insert_use::{ImportGranularity, InsertUseConfig, PrefixKind},
1212-
SnippetCap,
1213-
};
12141208

12151209
use super::*;
12161210

1217-
#[test]
1218-
fn test_completion_with_ref() {
1219-
let fixture = r#"
1220-
struct Foo;
1221-
fn foo(arg: &Foo) {}
1222-
fn main() {
1223-
let arg = Foo;
1224-
foo($0)
1225-
}"#;
1226-
1227-
let (offset, text) = test_utils::extract_offset(fixture);
1228-
let line_index = LineIndex {
1229-
index: Arc::new(ide::LineIndex::new(&text)),
1230-
endings: LineEndings::Unix,
1231-
encoding: OffsetEncoding::Utf16,
1232-
};
1233-
let (analysis, file_id) = Analysis::from_single_file(text);
1234-
1235-
let file_position = ide_db::base_db::FilePosition { file_id, offset };
1236-
let mut items = analysis
1237-
.completions(
1238-
&ide::CompletionConfig {
1239-
enable_postfix_completions: true,
1240-
enable_imports_on_the_fly: true,
1241-
enable_self_on_the_fly: true,
1242-
add_call_parenthesis: true,
1243-
add_call_argument_snippets: true,
1244-
snippet_cap: SnippetCap::new(true),
1245-
insert_use: InsertUseConfig {
1246-
granularity: ImportGranularity::Item,
1247-
prefix_kind: PrefixKind::Plain,
1248-
enforce_granularity: true,
1249-
group: true,
1250-
skip_glob_imports: true,
1251-
},
1252-
},
1253-
file_position,
1254-
)
1255-
.unwrap()
1256-
.unwrap();
1257-
items.retain(|c| c.label().ends_with("arg"));
1258-
let items = completion_items(
1259-
false,
1260-
false,
1261-
&line_index,
1262-
lsp_types::TextDocumentPositionParams {
1263-
text_document: lsp_types::TextDocumentIdentifier {
1264-
uri: "file://main.rs".parse().unwrap(),
1265-
},
1266-
position: position(&line_index, file_position.offset),
1267-
},
1268-
items,
1269-
);
1270-
let items: Vec<(String, Option<String>)> =
1271-
items.into_iter().map(|c| (c.label, c.sort_text)).collect();
1272-
1273-
expect_test::expect![[r#"
1274-
[
1275-
(
1276-
"&arg",
1277-
Some(
1278-
"fffffff9",
1279-
),
1280-
),
1281-
(
1282-
"arg",
1283-
Some(
1284-
"fffffffd",
1285-
),
1286-
),
1287-
]
1288-
"#]]
1289-
.assert_debug_eq(&items);
1290-
}
1291-
12921211
#[test]
12931212
fn conv_fold_line_folding_only_fixup() {
12941213
let text = r#"mod a;

0 commit comments

Comments
 (0)