aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--crates/rust-analyzer/src/config.rs9
-rw-r--r--crates/rust-analyzer/src/diagnostics/snapshots/rust_analyzer__diagnostics__to_proto__tests__snap_multi_line_fix.snap1
-rw-r--r--crates/rust-analyzer/src/diagnostics/snapshots/rust_analyzer__diagnostics__to_proto__tests__snap_rustc_unused_variable.snap1
-rw-r--r--crates/rust-analyzer/src/diagnostics/to_proto.rs1
-rw-r--r--crates/rust-analyzer/src/lsp_ext.rs10
-rw-r--r--crates/rust-analyzer/src/main_loop/handlers.rs54
-rw-r--r--crates/rust-analyzer/src/to_proto.rs10
-rw-r--r--docs/dev/lsp-extensions.md49
-rw-r--r--editors/code/src/client.ts41
-rw-r--r--editors/code/src/commands/index.ts14
-rw-r--r--editors/code/src/main.ts2
11 files changed, 109 insertions, 83 deletions
diff --git a/crates/rust-analyzer/src/config.rs b/crates/rust-analyzer/src/config.rs
index d75c48597..0e4412ade 100644
--- a/crates/rust-analyzer/src/config.rs
+++ b/crates/rust-analyzer/src/config.rs
@@ -102,6 +102,7 @@ pub struct ClientCapsConfig {
102 pub hierarchical_symbols: bool, 102 pub hierarchical_symbols: bool,
103 pub code_action_literals: bool, 103 pub code_action_literals: bool,
104 pub work_done_progress: bool, 104 pub work_done_progress: bool,
105 pub code_action_group: bool,
105} 106}
106 107
107impl Default for Config { 108impl Default for Config {
@@ -294,9 +295,13 @@ impl Config {
294 295
295 self.assist.allow_snippets(false); 296 self.assist.allow_snippets(false);
296 if let Some(experimental) = &caps.experimental { 297 if let Some(experimental) = &caps.experimental {
297 let enable = 298 let snippet_text_edit =
298 experimental.get("snippetTextEdit").and_then(|it| it.as_bool()) == Some(true); 299 experimental.get("snippetTextEdit").and_then(|it| it.as_bool()) == Some(true);
299 self.assist.allow_snippets(enable); 300 self.assist.allow_snippets(snippet_text_edit);
301
302 let code_action_group =
303 experimental.get("codeActionGroup").and_then(|it| it.as_bool()) == Some(true);
304 self.client_caps.code_action_group = code_action_group
300 } 305 }
301 } 306 }
302} 307}
diff --git a/crates/rust-analyzer/src/diagnostics/snapshots/rust_analyzer__diagnostics__to_proto__tests__snap_multi_line_fix.snap b/crates/rust-analyzer/src/diagnostics/snapshots/rust_analyzer__diagnostics__to_proto__tests__snap_multi_line_fix.snap
index 96466b5c9..c40cfdcdc 100644
--- a/crates/rust-analyzer/src/diagnostics/snapshots/rust_analyzer__diagnostics__to_proto__tests__snap_multi_line_fix.snap
+++ b/crates/rust-analyzer/src/diagnostics/snapshots/rust_analyzer__diagnostics__to_proto__tests__snap_multi_line_fix.snap
@@ -65,6 +65,7 @@ expression: diag
65 fixes: [ 65 fixes: [
66 CodeAction { 66 CodeAction {
67 title: "return the expression directly", 67 title: "return the expression directly",
68 group: None,
68 kind: Some( 69 kind: Some(
69 "quickfix", 70 "quickfix",
70 ), 71 ),
diff --git a/crates/rust-analyzer/src/diagnostics/snapshots/rust_analyzer__diagnostics__to_proto__tests__snap_rustc_unused_variable.snap b/crates/rust-analyzer/src/diagnostics/snapshots/rust_analyzer__diagnostics__to_proto__tests__snap_rustc_unused_variable.snap
index 8f962277f..6dd3fcb2e 100644
--- a/crates/rust-analyzer/src/diagnostics/snapshots/rust_analyzer__diagnostics__to_proto__tests__snap_rustc_unused_variable.snap
+++ b/crates/rust-analyzer/src/diagnostics/snapshots/rust_analyzer__diagnostics__to_proto__tests__snap_rustc_unused_variable.snap
@@ -50,6 +50,7 @@ expression: diag
50 fixes: [ 50 fixes: [
51 CodeAction { 51 CodeAction {
52 title: "consider prefixing with an underscore", 52 title: "consider prefixing with an underscore",
53 group: None,
53 kind: Some( 54 kind: Some(
54 "quickfix", 55 "quickfix",
55 ), 56 ),
diff --git a/crates/rust-analyzer/src/diagnostics/to_proto.rs b/crates/rust-analyzer/src/diagnostics/to_proto.rs
index afea59525..a500d670a 100644
--- a/crates/rust-analyzer/src/diagnostics/to_proto.rs
+++ b/crates/rust-analyzer/src/diagnostics/to_proto.rs
@@ -145,6 +145,7 @@ fn map_rust_child_diagnostic(
145 } else { 145 } else {
146 MappedRustChildDiagnostic::SuggestedFix(lsp_ext::CodeAction { 146 MappedRustChildDiagnostic::SuggestedFix(lsp_ext::CodeAction {
147 title: rd.message.clone(), 147 title: rd.message.clone(),
148 group: None,
148 kind: Some("quickfix".to_string()), 149 kind: Some("quickfix".to_string()),
149 edit: Some(lsp_ext::SnippetWorkspaceEdit { 150 edit: Some(lsp_ext::SnippetWorkspaceEdit {
150 // FIXME: there's no good reason to use edit_map here.... 151 // FIXME: there's no good reason to use edit_map here....
diff --git a/crates/rust-analyzer/src/lsp_ext.rs b/crates/rust-analyzer/src/lsp_ext.rs
index 0fd60caf4..c25d90a50 100644
--- a/crates/rust-analyzer/src/lsp_ext.rs
+++ b/crates/rust-analyzer/src/lsp_ext.rs
@@ -133,14 +133,6 @@ pub struct Runnable {
133 pub cwd: Option<PathBuf>, 133 pub cwd: Option<PathBuf>,
134} 134}
135 135
136#[derive(Deserialize, Serialize, Debug)]
137#[serde(rename_all = "camelCase")]
138pub struct SourceChange {
139 pub label: String,
140 pub workspace_edit: SnippetWorkspaceEdit,
141 pub cursor_position: Option<lsp_types::TextDocumentPositionParams>,
142}
143
144pub enum InlayHints {} 136pub enum InlayHints {}
145 137
146impl Request for InlayHints { 138impl Request for InlayHints {
@@ -196,6 +188,8 @@ impl Request for CodeActionRequest {
196pub struct CodeAction { 188pub struct CodeAction {
197 pub title: String, 189 pub title: String,
198 #[serde(skip_serializing_if = "Option::is_none")] 190 #[serde(skip_serializing_if = "Option::is_none")]
191 pub group: Option<String>,
192 #[serde(skip_serializing_if = "Option::is_none")]
199 pub kind: Option<String>, 193 pub kind: Option<String>,
200 #[serde(skip_serializing_if = "Option::is_none")] 194 #[serde(skip_serializing_if = "Option::is_none")]
201 pub command: Option<lsp_types::Command>, 195 pub command: Option<lsp_types::Command>,
diff --git a/crates/rust-analyzer/src/main_loop/handlers.rs b/crates/rust-analyzer/src/main_loop/handlers.rs
index 25e660bd5..89144f743 100644
--- a/crates/rust-analyzer/src/main_loop/handlers.rs
+++ b/crates/rust-analyzer/src/main_loop/handlers.rs
@@ -18,7 +18,7 @@ use lsp_types::{
18 SemanticTokensResult, SymbolInformation, TextDocumentIdentifier, Url, WorkspaceEdit, 18 SemanticTokensResult, SymbolInformation, TextDocumentIdentifier, Url, WorkspaceEdit,
19}; 19};
20use ra_ide::{ 20use ra_ide::{
21 Assist, FileId, FilePosition, FileRange, Query, RangeInfo, Runnable, RunnableKind, SearchScope, 21 FileId, FilePosition, FileRange, Query, RangeInfo, Runnable, RunnableKind, SearchScope,
22 TextEdit, 22 TextEdit,
23}; 23};
24use ra_prof::profile; 24use ra_prof::profile;
@@ -720,6 +720,7 @@ pub fn handle_code_action(
720 let file_id = from_proto::file_id(&world, &params.text_document.uri)?; 720 let file_id = from_proto::file_id(&world, &params.text_document.uri)?;
721 let line_index = world.analysis().file_line_index(file_id)?; 721 let line_index = world.analysis().file_line_index(file_id)?;
722 let range = from_proto::text_range(&line_index, params.range); 722 let range = from_proto::text_range(&line_index, params.range);
723 let frange = FileRange { file_id, range };
723 724
724 let diagnostics = world.analysis().diagnostics(file_id)?; 725 let diagnostics = world.analysis().diagnostics(file_id)?;
725 let mut res: Vec<lsp_ext::CodeAction> = Vec::new(); 726 let mut res: Vec<lsp_ext::CodeAction> = Vec::new();
@@ -733,7 +734,8 @@ pub fn handle_code_action(
733 for source_edit in fixes_from_diagnostics { 734 for source_edit in fixes_from_diagnostics {
734 let title = source_edit.label.clone(); 735 let title = source_edit.label.clone();
735 let edit = to_proto::snippet_workspace_edit(&world, source_edit)?; 736 let edit = to_proto::snippet_workspace_edit(&world, source_edit)?;
736 let action = lsp_ext::CodeAction { title, kind: None, edit: Some(edit), command: None }; 737 let action =
738 lsp_ext::CodeAction { title, group: None, kind: None, edit: Some(edit), command: None };
737 res.push(action); 739 res.push(action);
738 } 740 }
739 741
@@ -745,53 +747,9 @@ pub fn handle_code_action(
745 res.push(fix.action.clone()); 747 res.push(fix.action.clone());
746 } 748 }
747 749
748 let mut grouped_assists: FxHashMap<String, (usize, Vec<Assist>)> = FxHashMap::default(); 750 for assist in world.analysis().assists(&world.config.assist, frange)?.into_iter() {
749 for assist in 751 res.push(to_proto::code_action(&world, assist)?.into());
750 world.analysis().assists(&world.config.assist, FileRange { file_id, range })?.into_iter()
751 {
752 match &assist.group_label {
753 Some(label) => grouped_assists
754 .entry(label.to_owned())
755 .or_insert_with(|| {
756 let idx = res.len();
757 let dummy = lsp_ext::CodeAction {
758 title: String::new(),
759 kind: None,
760 command: None,
761 edit: None,
762 };
763 res.push(dummy);
764 (idx, Vec::new())
765 })
766 .1
767 .push(assist),
768 None => {
769 res.push(to_proto::code_action(&world, assist)?.into());
770 }
771 }
772 }
773
774 for (group_label, (idx, assists)) in grouped_assists {
775 if assists.len() == 1 {
776 res[idx] = to_proto::code_action(&world, assists.into_iter().next().unwrap())?.into();
777 } else {
778 let title = group_label;
779
780 let mut arguments = Vec::with_capacity(assists.len());
781 for assist in assists {
782 let source_change = to_proto::source_change(&world, assist.source_change)?;
783 arguments.push(to_value(source_change)?);
784 }
785
786 let command = Some(Command {
787 title: title.clone(),
788 command: "rust-analyzer.selectAndApplySourceChange".to_string(),
789 arguments: Some(vec![serde_json::Value::Array(arguments)]),
790 });
791 res[idx] = lsp_ext::CodeAction { title, kind: None, edit: None, command };
792 }
793 } 752 }
794
795 Ok(Some(res)) 753 Ok(Some(res))
796} 754}
797 755
diff --git a/crates/rust-analyzer/src/to_proto.rs b/crates/rust-analyzer/src/to_proto.rs
index f6f4bb134..461944ada 100644
--- a/crates/rust-analyzer/src/to_proto.rs
+++ b/crates/rust-analyzer/src/to_proto.rs
@@ -478,15 +478,6 @@ pub(crate) fn resource_op(
478 Ok(res) 478 Ok(res)
479} 479}
480 480
481pub(crate) fn source_change(
482 world: &WorldSnapshot,
483 source_change: SourceChange,
484) -> Result<lsp_ext::SourceChange> {
485 let label = source_change.label.clone();
486 let workspace_edit = self::snippet_workspace_edit(world, source_change)?;
487 Ok(lsp_ext::SourceChange { label, workspace_edit, cursor_position: None })
488}
489
490pub(crate) fn snippet_workspace_edit( 481pub(crate) fn snippet_workspace_edit(
491 world: &WorldSnapshot, 482 world: &WorldSnapshot,
492 source_change: SourceChange, 483 source_change: SourceChange,
@@ -606,6 +597,7 @@ fn main() <fold>{
606pub(crate) fn code_action(world: &WorldSnapshot, assist: Assist) -> Result<lsp_ext::CodeAction> { 597pub(crate) fn code_action(world: &WorldSnapshot, assist: Assist) -> Result<lsp_ext::CodeAction> {
607 let res = lsp_ext::CodeAction { 598 let res = lsp_ext::CodeAction {
608 title: assist.label, 599 title: assist.label,
600 group: if world.config.client_caps.code_action_group { assist.group_label } else { None },
609 kind: Some(String::new()), 601 kind: Some(String::new()),
610 edit: Some(snippet_workspace_edit(world, assist.source_change)?), 602 edit: Some(snippet_workspace_edit(world, assist.source_change)?),
611 command: None, 603 command: None,
diff --git a/docs/dev/lsp-extensions.md b/docs/dev/lsp-extensions.md
index 7c45aef4c..d90875f8b 100644
--- a/docs/dev/lsp-extensions.md
+++ b/docs/dev/lsp-extensions.md
@@ -5,7 +5,7 @@ It's a best effort document, when in doubt, consult the source (and send a PR wi
5We aim to upstream all non Rust-specific extensions to the protocol, but this is not a top priority. 5We aim to upstream all non Rust-specific extensions to the protocol, but this is not a top priority.
6All capabilities are enabled via `experimental` field of `ClientCapabilities`. 6All capabilities are enabled via `experimental` field of `ClientCapabilities`.
7 7
8## `SnippetTextEdit` 8## Snippet `TextEdit`
9 9
10**Client Capability:** `{ "snippetTextEdit": boolean }` 10**Client Capability:** `{ "snippetTextEdit": boolean }`
11 11
@@ -36,7 +36,7 @@ At the moment, rust-analyzer guarantees that only a single edit will have `Inser
36* Where exactly are `SnippetTextEdit`s allowed (only in code actions at the moment)? 36* Where exactly are `SnippetTextEdit`s allowed (only in code actions at the moment)?
37* Can snippets span multiple files (so far, no)? 37* Can snippets span multiple files (so far, no)?
38 38
39## `joinLines` 39## Join Lines
40 40
41**Server Capability:** `{ "joinLines": boolean }` 41**Server Capability:** `{ "joinLines": boolean }`
42 42
@@ -119,3 +119,48 @@ SSR with query `foo($a:expr, $b:expr) ==>> ($a).foo($b)` will transform, eg `foo
119 119
120* Probably needs search without replace mode 120* Probably needs search without replace mode
121* Needs a way to limit the scope to certain files. 121* Needs a way to limit the scope to certain files.
122
123## `CodeAction` Groups
124
125**Client Capability:** `{ "codeActionGroup": boolean }`
126
127If this capability is set, `CodeAction` returned from the server contain an additional field, `group`:
128
129```typescript
130interface CodeAction {
131 title: string;
132 group?: string;
133 ...
134}
135```
136
137All code-actions with the same `group` should be grouped under single (extendable) entry in lightbulb menu.
138The set of actions `[ { title: "foo" }, { group: "frobnicate", title: "bar" }, { group: "frobnicate", title: "baz" }]` should be rendered as
139
140```
141💡
142 +-------------+
143 | foo |
144 +-------------+-----+
145 | frobnicate >| bar |
146 +-------------+-----+
147 | baz |
148 +-----+
149```
150
151Alternatively, selecting `frobnicate` could present a user with an additional menu to choose between `bar` and `baz`.
152
153### Example
154
155```rust
156fn main() {
157 let x: Entry/*cursor here*/ = todo!();
158}
159```
160
161Invoking code action at this position will yield two code actions for importing `Entry` from either `collections::HashMap` or `collection::BTreeMap`, grouped under a single "import" group.
162
163### Unresolved Questions
164
165* Is a fixed two-level structure enough?
166* Should we devise a general way to encode custom interaction protocols for GUI refactorings?
diff --git a/editors/code/src/client.ts b/editors/code/src/client.ts
index fac1a0be3..d64f9a3f9 100644
--- a/editors/code/src/client.ts
+++ b/editors/code/src/client.ts
@@ -41,10 +41,12 @@ export function createClient(serverPath: string, cwd: string): lc.LanguageClient
41 return client.sendRequest(lc.CodeActionRequest.type, params, token).then((values) => { 41 return client.sendRequest(lc.CodeActionRequest.type, params, token).then((values) => {
42 if (values === null) return undefined; 42 if (values === null) return undefined;
43 const result: (vscode.CodeAction | vscode.Command)[] = []; 43 const result: (vscode.CodeAction | vscode.Command)[] = [];
44 const groups = new Map<string, { index: number; items: vscode.CodeAction[] }>();
44 for (const item of values) { 45 for (const item of values) {
45 if (lc.CodeAction.is(item)) { 46 if (lc.CodeAction.is(item)) {
46 const action = client.protocol2CodeConverter.asCodeAction(item); 47 const action = client.protocol2CodeConverter.asCodeAction(item);
47 if (isSnippetEdit(item)) { 48 const group = actionGroup(item);
49 if (isSnippetEdit(item) || group) {
48 action.command = { 50 action.command = {
49 command: "rust-analyzer.applySnippetWorkspaceEdit", 51 command: "rust-analyzer.applySnippetWorkspaceEdit",
50 title: "", 52 title: "",
@@ -52,12 +54,38 @@ export function createClient(serverPath: string, cwd: string): lc.LanguageClient
52 }; 54 };
53 action.edit = undefined; 55 action.edit = undefined;
54 } 56 }
55 result.push(action); 57
58 if (group) {
59 let entry = groups.get(group);
60 if (!entry) {
61 entry = { index: result.length, items: [] };
62 groups.set(group, entry);
63 result.push(action);
64 }
65 entry.items.push(action);
66 } else {
67 result.push(action);
68 }
56 } else { 69 } else {
57 const command = client.protocol2CodeConverter.asCommand(item); 70 const command = client.protocol2CodeConverter.asCommand(item);
58 result.push(command); 71 result.push(command);
59 } 72 }
60 } 73 }
74 for (const [group, { index, items }] of groups) {
75 if (items.length === 1) {
76 result[index] = items[0];
77 } else {
78 const action = new vscode.CodeAction(group);
79 action.command = {
80 command: "rust-analyzer.applyActionGroup",
81 title: "",
82 arguments: [items.map((item) => {
83 return { label: item.title, edit: item.command!!.arguments!![0] };
84 })],
85 };
86 result[index] = action;
87 }
88 }
61 return result; 89 return result;
62 }, 90 },
63 (_error) => undefined 91 (_error) => undefined
@@ -81,15 +109,16 @@ export function createClient(serverPath: string, cwd: string): lc.LanguageClient
81 // implementations are still in the "proposed" category for 3.16. 109 // implementations are still in the "proposed" category for 3.16.
82 client.registerFeature(new CallHierarchyFeature(client)); 110 client.registerFeature(new CallHierarchyFeature(client));
83 client.registerFeature(new SemanticTokensFeature(client)); 111 client.registerFeature(new SemanticTokensFeature(client));
84 client.registerFeature(new SnippetTextEditFeature()); 112 client.registerFeature(new ExperimentalFeatures());
85 113
86 return client; 114 return client;
87} 115}
88 116
89class SnippetTextEditFeature implements lc.StaticFeature { 117class ExperimentalFeatures implements lc.StaticFeature {
90 fillClientCapabilities(capabilities: lc.ClientCapabilities): void { 118 fillClientCapabilities(capabilities: lc.ClientCapabilities): void {
91 const caps: any = capabilities.experimental ?? {}; 119 const caps: any = capabilities.experimental ?? {};
92 caps.snippetTextEdit = true; 120 caps.snippetTextEdit = true;
121 caps.codeActionGroup = true;
93 capabilities.experimental = caps; 122 capabilities.experimental = caps;
94 } 123 }
95 initialize(_capabilities: lc.ServerCapabilities<any>, _documentSelector: lc.DocumentSelector | undefined): void { 124 initialize(_capabilities: lc.ServerCapabilities<any>, _documentSelector: lc.DocumentSelector | undefined): void {
@@ -107,3 +136,7 @@ function isSnippetEdit(action: lc.CodeAction): boolean {
107 } 136 }
108 return false; 137 return false;
109} 138}
139
140function actionGroup(action: lc.CodeAction): string | undefined {
141 return (action as any).group;
142}
diff --git a/editors/code/src/commands/index.ts b/editors/code/src/commands/index.ts
index e5ed77e32..abb53a248 100644
--- a/editors/code/src/commands/index.ts
+++ b/editors/code/src/commands/index.ts
@@ -41,15 +41,11 @@ export function applySourceChange(ctx: Ctx): Cmd {
41 }; 41 };
42} 42}
43 43
44export function selectAndApplySourceChange(ctx: Ctx): Cmd { 44export function applyActionGroup(_ctx: Ctx): Cmd {
45 return async (changes: ra.SourceChange[]) => { 45 return async (actions: { label: string; edit: vscode.WorkspaceEdit }[]) => {
46 if (changes.length === 1) { 46 const selectedAction = await vscode.window.showQuickPick(actions);
47 await sourceChange.applySourceChange(ctx, changes[0]); 47 if (!selectedAction) return;
48 } else if (changes.length > 0) { 48 await applySnippetWorkspaceEdit(selectedAction.edit);
49 const selectedChange = await vscode.window.showQuickPick(changes);
50 if (!selectedChange) return;
51 await sourceChange.applySourceChange(ctx, selectedChange);
52 }
53 }; 49 };
54} 50}
55 51
diff --git a/editors/code/src/main.ts b/editors/code/src/main.ts
index 8b0a9d870..4d4513869 100644
--- a/editors/code/src/main.ts
+++ b/editors/code/src/main.ts
@@ -92,7 +92,7 @@ export async function activate(context: vscode.ExtensionContext) {
92 ctx.registerCommand('showReferences', commands.showReferences); 92 ctx.registerCommand('showReferences', commands.showReferences);
93 ctx.registerCommand('applySourceChange', commands.applySourceChange); 93 ctx.registerCommand('applySourceChange', commands.applySourceChange);
94 ctx.registerCommand('applySnippetWorkspaceEdit', commands.applySnippetWorkspaceEditCommand); 94 ctx.registerCommand('applySnippetWorkspaceEdit', commands.applySnippetWorkspaceEditCommand);
95 ctx.registerCommand('selectAndApplySourceChange', commands.selectAndApplySourceChange); 95 ctx.registerCommand('applyActionGroup', commands.applyActionGroup);
96 96
97 ctx.pushCleanup(activateTaskProvider(workspaceFolder)); 97 ctx.pushCleanup(activateTaskProvider(workspaceFolder));
98 98