From be84f85c1dab75c053e94712bc1028b548206c2f Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Fri, 30 Jul 2021 19:16:33 +0300 Subject: [PATCH] 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. --- crates/rust-analyzer/src/config.rs | 36 ++++++++- crates/rust-analyzer/src/handlers.rs | 55 +++++++------ crates/rust-analyzer/src/lsp_ext.rs | 5 ++ crates/rust-analyzer/src/to_proto.rs | 113 ++++----------------------- docs/dev/lsp-extensions.md | 22 +++++- docs/user/generated_config.adoc | 6 ++ editors/code/package.json | 5 ++ editors/code/src/client.ts | 9 +++ 8 files changed, 127 insertions(+), 124 deletions(-) diff --git a/crates/rust-analyzer/src/config.rs b/crates/rust-analyzer/src/config.rs index 99a29b43d2..e71513034c 100644 --- a/crates/rust-analyzer/src/config.rs +++ b/crates/rust-analyzer/src/config.rs @@ -25,9 +25,12 @@ use serde::{de::DeserializeOwned, Deserialize}; use vfs::AbsPathBuf; use crate::{ - caps::completion_item_edit_resolve, diagnostics::DiagnosticsMapConfig, - line_index::OffsetEncoding, lsp_ext::supports_utf8, lsp_ext::WorkspaceSymbolSearchKind, + caps::completion_item_edit_resolve, + diagnostics::DiagnosticsMapConfig, + line_index::OffsetEncoding, + lsp_ext::supports_utf8, lsp_ext::WorkspaceSymbolSearchScope, + lsp_ext::{self, WorkspaceSymbolSearchKind}, }; // Defines the server-side configuration of the rust-analyzer. We generate @@ -221,6 +224,9 @@ config_data! { /// Whether to show `References` lens. Only applies when /// `#rust-analyzer.lens.enable#` is set. lens_references: bool = "false", + /// Internal config: use custom client-side commands even when the + /// client doesn't set the corresponding capability. + lens_forceCustomCommands: bool = "true", /// Disable project auto-discovery in favor of explicitly specified set /// of projects. @@ -405,6 +411,14 @@ pub struct WorkspaceSymbolConfig { pub search_kind: WorkspaceSymbolSearchKind, } +pub struct ClientCommandsConfig { + pub run_single: bool, + pub debug_single: bool, + pub show_reference: bool, + pub goto_location: bool, + pub trigger_parameter_hints: bool, +} + impl Config { pub fn new(root_path: AbsPathBuf, caps: ClientCapabilities) -> Self { Config { @@ -858,6 +872,24 @@ impl Config { false ) } + pub fn client_commands(&self) -> ClientCommandsConfig { + let commands = + try_or!(self.caps.experimental.as_ref()?.get("commands")?, &serde_json::Value::Null); + let commands: Option = + serde_json::from_value(commands.clone()).ok(); + let force = commands.is_none() && self.data.lens_forceCustomCommands; + let commands = commands.map(|it| it.commands).unwrap_or_default(); + + let get = |name: &str| commands.iter().any(|it| it == name) || force; + + ClientCommandsConfig { + run_single: get("rust-analyzer.runSingle"), + debug_single: get("rust-analyzer.debugSingle"), + show_reference: get("rust-analyzer.showReferences"), + goto_location: get("rust-analyzer.gotoLocation"), + trigger_parameter_hints: get("editor.action.triggerParameterHints"), + } + } pub fn highlight_related(&self) -> HighlightRelatedConfig { HighlightRelatedConfig { diff --git a/crates/rust-analyzer/src/handlers.rs b/crates/rust-analyzer/src/handlers.rs index 949a77a4e8..c9a25e086a 100644 --- a/crates/rust-analyzer/src/handlers.rs +++ b/crates/rust-analyzer/src/handlers.rs @@ -768,13 +768,8 @@ pub(crate) fn handle_completion( }; let line_index = snap.file_line_index(position.file_id)?; - let items = to_proto::completion_items( - snap.config.insert_replace_support(), - completion_config.enable_imports_on_the_fly, - &line_index, - text_document_position, - items, - ); + let items = + to_proto::completion_items(&snap.config, &line_index, text_document_position, items); let completion_list = lsp_types::CompletionList { is_incomplete: true, items }; Ok(Some(completion_list.into())) @@ -1503,7 +1498,7 @@ fn show_impl_command_link( snap: &GlobalStateSnapshot, position: &FilePosition, ) -> Option { - if snap.config.hover_actions().implementations { + if snap.config.hover_actions().implementations && snap.config.client_commands().show_reference { if let Some(nav_data) = snap.analysis.goto_implementation(*position).unwrap_or(None) { let uri = to_proto::url(snap, position.file_id); let line_index = snap.file_line_index(position.file_id).ok()?; @@ -1529,7 +1524,7 @@ fn show_ref_command_link( snap: &GlobalStateSnapshot, position: &FilePosition, ) -> Option { - if snap.config.hover_actions().references { + if snap.config.hover_actions().references && snap.config.client_commands().show_reference { if let Some(ref_search_res) = snap.analysis.find_all_refs(*position, None).unwrap_or(None) { let uri = to_proto::url(snap, position.file_id); let line_index = snap.file_line_index(position.file_id).ok()?; @@ -1559,35 +1554,47 @@ fn runnable_action_links( snap: &GlobalStateSnapshot, runnable: Runnable, ) -> Option { - let cargo_spec = CargoTargetSpec::for_file(snap, runnable.nav.file_id).ok()?; let hover_actions_config = snap.config.hover_actions(); - if !hover_actions_config.runnable() || should_skip_target(&runnable, cargo_spec.as_ref()) { + if !hover_actions_config.runnable() { + return None; + } + + let cargo_spec = CargoTargetSpec::for_file(snap, runnable.nav.file_id).ok()?; + if should_skip_target(&runnable, cargo_spec.as_ref()) { + return None; + } + + let client_commands_config = snap.config.client_commands(); + if !(client_commands_config.run_single || client_commands_config.debug_single) { return None; } let title = runnable.title(); - to_proto::runnable(snap, runnable).ok().map(|r| { - let mut group = lsp_ext::CommandLinkGroup::default(); + let r = to_proto::runnable(snap, runnable).ok()?; - if hover_actions_config.run { - let run_command = to_proto::command::run_single(&r, &title); - group.commands.push(to_command_link(run_command, r.label.clone())); - } + let mut group = lsp_ext::CommandLinkGroup::default(); - if hover_actions_config.debug { - let dbg_command = to_proto::command::debug_single(&r); - group.commands.push(to_command_link(dbg_command, r.label)); - } + if hover_actions_config.run && client_commands_config.run_single { + let run_command = to_proto::command::run_single(&r, &title); + group.commands.push(to_command_link(run_command, r.label.clone())); + } - group - }) + if hover_actions_config.debug && client_commands_config.debug_single { + let dbg_command = to_proto::command::debug_single(&r); + group.commands.push(to_command_link(dbg_command, r.label)); + } + + Some(group) } fn goto_type_action_links( snap: &GlobalStateSnapshot, nav_targets: &[HoverGotoTypeData], ) -> Option { - if !snap.config.hover_actions().goto_type_def || nav_targets.is_empty() { + if !snap.config.hover_actions().goto_type_def + || nav_targets.is_empty() + || !snap.config.client_commands().goto_location + { return None; } diff --git a/crates/rust-analyzer/src/lsp_ext.rs b/crates/rust-analyzer/src/lsp_ext.rs index d697ec44d1..521691d5ec 100644 --- a/crates/rust-analyzer/src/lsp_ext.rs +++ b/crates/rust-analyzer/src/lsp_ext.rs @@ -523,3 +523,8 @@ pub struct CompletionResolveData { pub full_import_path: String, pub imported_name: String, } + +#[derive(Debug, Deserialize, Default)] +pub struct ClientCommandOptions { + pub commands: Vec, +} diff --git a/crates/rust-analyzer/src/to_proto.rs b/crates/rust-analyzer/src/to_proto.rs index 8bd3f7a9eb..906259b098 100644 --- a/crates/rust-analyzer/src/to_proto.rs +++ b/crates/rust-analyzer/src/to_proto.rs @@ -18,6 +18,7 @@ use vfs::AbsPath; use crate::{ cargo_target_spec::CargoTargetSpec, + config::Config, global_state::GlobalStateSnapshot, line_index::{LineEndings, LineIndex, OffsetEncoding}, lsp_ext, semantic_tokens, Result, @@ -190,8 +191,7 @@ pub(crate) fn snippet_text_edit_vec( } pub(crate) fn completion_items( - insert_replace_support: bool, - enable_imports_on_the_fly: bool, + config: &Config, line_index: &LineIndex, tdpp: lsp_types::TextDocumentPositionParams, items: Vec, @@ -199,23 +199,14 @@ pub(crate) fn completion_items( let max_relevance = items.iter().map(|it| it.relevance().score()).max().unwrap_or_default(); let mut res = Vec::with_capacity(items.len()); for item in items { - completion_item( - &mut res, - insert_replace_support, - enable_imports_on_the_fly, - line_index, - &tdpp, - max_relevance, - item, - ) + completion_item(&mut res, config, line_index, &tdpp, max_relevance, item) } res } fn completion_item( acc: &mut Vec, - insert_replace_support: bool, - enable_imports_on_the_fly: bool, + config: &Config, line_index: &LineIndex, tdpp: &lsp_types::TextDocumentPositionParams, max_relevance: u32, @@ -230,7 +221,7 @@ fn completion_item( let source_range = item.source_range(); for indel in item.text_edit().iter() { if indel.delete.contains_range(source_range) { - let insert_replace_support = insert_replace_support.then(|| tdpp.position); + let insert_replace_support = config.insert_replace_support().then(|| tdpp.position); text_edit = Some(if indel.delete == source_range { self::completion_text_edit(line_index, insert_replace_support, indel.clone()) } else { @@ -269,14 +260,14 @@ fn completion_item( lsp_item.tags = Some(vec![lsp_types::CompletionItemTag::Deprecated]) } - if item.trigger_call_info() { + if item.trigger_call_info() && config.client_commands().trigger_parameter_hints { lsp_item.command = Some(command::trigger_parameter_hints()); } if item.is_snippet() { lsp_item.insert_text_format = Some(lsp_types::InsertTextFormat::Snippet); } - if enable_imports_on_the_fly { + if config.completion().enable_imports_on_the_fly { if let Some(import_edit) = item.import_to_add() { let import_path = &import_edit.import.import_path; if let Some(import_name) = import_path.segments().last() { @@ -992,6 +983,7 @@ pub(crate) fn code_lens( snap: &GlobalStateSnapshot, annotation: Annotation, ) -> Result<()> { + let client_commands_config = snap.config.client_commands(); match annotation.kind { AnnotationKind::Runnable(run) => { let line_index = snap.file_line_index(run.nav.file_id)?; @@ -1008,7 +1000,7 @@ pub(crate) fn code_lens( let r = runnable(snap, run)?; let lens_config = snap.config.lens(); - if lens_config.run { + if lens_config.run && client_commands_config.run_single { let command = command::run_single(&r, &title); acc.push(lsp_types::CodeLens { range: annotation_range, @@ -1016,7 +1008,7 @@ pub(crate) fn code_lens( data: None, }) } - if lens_config.debug && can_debug { + if lens_config.debug && can_debug && client_commands_config.debug_single { let command = command::debug_single(&r); acc.push(lsp_types::CodeLens { range: annotation_range, @@ -1026,6 +1018,9 @@ pub(crate) fn code_lens( } } AnnotationKind::HasImpls { position: file_position, data } => { + if !client_commands_config.show_reference { + return Ok(()); + } let line_index = snap.file_line_index(file_position.file_id)?; let annotation_range = range(&line_index, annotation.range); let url = url(snap, file_position.file_id); @@ -1069,6 +1064,9 @@ pub(crate) fn code_lens( }) } AnnotationKind::HasReferences { position: file_position, data } => { + if !client_commands_config.show_reference { + return Ok(()); + } let line_index = snap.file_line_index(file_position.file_id)?; let annotation_range = range(&line_index, annotation.range); let url = url(snap, file_position.file_id); @@ -1207,88 +1205,9 @@ mod tests { use std::sync::Arc; use ide::Analysis; - use ide_db::helpers::{ - insert_use::{ImportGranularity, InsertUseConfig, PrefixKind}, - SnippetCap, - }; use super::*; - #[test] - fn test_completion_with_ref() { - let fixture = r#" - struct Foo; - fn foo(arg: &Foo) {} - fn main() { - let arg = Foo; - foo($0) - }"#; - - let (offset, text) = test_utils::extract_offset(fixture); - let line_index = LineIndex { - index: Arc::new(ide::LineIndex::new(&text)), - endings: LineEndings::Unix, - encoding: OffsetEncoding::Utf16, - }; - let (analysis, file_id) = Analysis::from_single_file(text); - - let file_position = ide_db::base_db::FilePosition { file_id, offset }; - let mut items = analysis - .completions( - &ide::CompletionConfig { - enable_postfix_completions: true, - enable_imports_on_the_fly: true, - enable_self_on_the_fly: true, - add_call_parenthesis: true, - add_call_argument_snippets: true, - snippet_cap: SnippetCap::new(true), - insert_use: InsertUseConfig { - granularity: ImportGranularity::Item, - prefix_kind: PrefixKind::Plain, - enforce_granularity: true, - group: true, - skip_glob_imports: true, - }, - }, - file_position, - ) - .unwrap() - .unwrap(); - items.retain(|c| c.label().ends_with("arg")); - let items = completion_items( - false, - false, - &line_index, - lsp_types::TextDocumentPositionParams { - text_document: lsp_types::TextDocumentIdentifier { - uri: "file://main.rs".parse().unwrap(), - }, - position: position(&line_index, file_position.offset), - }, - items, - ); - let items: Vec<(String, Option)> = - items.into_iter().map(|c| (c.label, c.sort_text)).collect(); - - expect_test::expect![[r#" - [ - ( - "&arg", - Some( - "fffffff9", - ), - ), - ( - "arg", - Some( - "fffffffd", - ), - ), - ] - "#]] - .assert_debug_eq(&items); - } - #[test] fn conv_fold_line_folding_only_fixup() { let text = r#"mod a; diff --git a/docs/dev/lsp-extensions.md b/docs/dev/lsp-extensions.md index 1fb5237ba7..98098b69a9 100644 --- a/docs/dev/lsp-extensions.md +++ b/docs/dev/lsp-extensions.md @@ -1,5 +1,5 @@