Skip to content

Commit 2a36a2a

Browse files
bors[bot]matklad
andauthored
Merge #4569
4569: CodeAction groups r=matklad a=matklad bors r+ 🤖 Co-authored-by: Aleksey Kladov <[email protected]>
2 parents 0fb7134 + 2075e77 commit 2a36a2a

File tree

11 files changed

+109
-83
lines changed

11 files changed

+109
-83
lines changed

crates/rust-analyzer/src/config.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ pub struct ClientCapsConfig {
102102
pub hierarchical_symbols: bool,
103103
pub code_action_literals: bool,
104104
pub work_done_progress: bool,
105+
pub code_action_group: bool,
105106
}
106107

107108
impl Default for Config {
@@ -294,9 +295,13 @@ impl Config {
294295

295296
self.assist.allow_snippets(false);
296297
if let Some(experimental) = &caps.experimental {
297-
let enable =
298+
let snippet_text_edit =
298299
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
300305
}
301306
}
302307
}

crates/rust-analyzer/src/diagnostics/snapshots/rust_analyzer__diagnostics__to_proto__tests__snap_multi_line_fix.snap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ expression: diag
6565
fixes: [
6666
CodeAction {
6767
title: "return the expression directly",
68+
group: None,
6869
kind: Some(
6970
"quickfix",
7071
),

crates/rust-analyzer/src/diagnostics/snapshots/rust_analyzer__diagnostics__to_proto__tests__snap_rustc_unused_variable.snap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ expression: diag
5050
fixes: [
5151
CodeAction {
5252
title: "consider prefixing with an underscore",
53+
group: None,
5354
kind: Some(
5455
"quickfix",
5556
),

crates/rust-analyzer/src/diagnostics/to_proto.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ fn map_rust_child_diagnostic(
145145
} else {
146146
MappedRustChildDiagnostic::SuggestedFix(lsp_ext::CodeAction {
147147
title: rd.message.clone(),
148+
group: None,
148149
kind: Some("quickfix".to_string()),
149150
edit: Some(lsp_ext::SnippetWorkspaceEdit {
150151
// FIXME: there's no good reason to use edit_map here....

crates/rust-analyzer/src/lsp_ext.rs

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -133,14 +133,6 @@ pub struct Runnable {
133133
pub cwd: Option<PathBuf>,
134134
}
135135

136-
#[derive(Deserialize, Serialize, Debug)]
137-
#[serde(rename_all = "camelCase")]
138-
pub struct SourceChange {
139-
pub label: String,
140-
pub workspace_edit: SnippetWorkspaceEdit,
141-
pub cursor_position: Option<lsp_types::TextDocumentPositionParams>,
142-
}
143-
144136
pub enum InlayHints {}
145137

146138
impl Request for InlayHints {
@@ -196,6 +188,8 @@ impl Request for CodeActionRequest {
196188
pub struct CodeAction {
197189
pub title: String,
198190
#[serde(skip_serializing_if = "Option::is_none")]
191+
pub group: Option<String>,
192+
#[serde(skip_serializing_if = "Option::is_none")]
199193
pub kind: Option<String>,
200194
#[serde(skip_serializing_if = "Option::is_none")]
201195
pub command: Option<lsp_types::Command>,

crates/rust-analyzer/src/main_loop/handlers.rs

Lines changed: 6 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ use lsp_types::{
1818
SemanticTokensResult, SymbolInformation, TextDocumentIdentifier, Url, WorkspaceEdit,
1919
};
2020
use ra_ide::{
21-
Assist, FileId, FilePosition, FileRange, Query, RangeInfo, Runnable, RunnableKind, SearchScope,
21+
FileId, FilePosition, FileRange, Query, RangeInfo, Runnable, RunnableKind, SearchScope,
2222
TextEdit,
2323
};
2424
use ra_prof::profile;
@@ -720,6 +720,7 @@ pub fn handle_code_action(
720720
let file_id = from_proto::file_id(&world, &params.text_document.uri)?;
721721
let line_index = world.analysis().file_line_index(file_id)?;
722722
let range = from_proto::text_range(&line_index, params.range);
723+
let frange = FileRange { file_id, range };
723724

724725
let diagnostics = world.analysis().diagnostics(file_id)?;
725726
let mut res: Vec<lsp_ext::CodeAction> = Vec::new();
@@ -733,7 +734,8 @@ pub fn handle_code_action(
733734
for source_edit in fixes_from_diagnostics {
734735
let title = source_edit.label.clone();
735736
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 };
737739
res.push(action);
738740
}
739741

@@ -745,53 +747,9 @@ pub fn handle_code_action(
745747
res.push(fix.action.clone());
746748
}
747749

748-
let mut grouped_assists: FxHashMap<String, (usize, Vec<Assist>)> = FxHashMap::default();
749-
for assist in
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-
}
750+
for assist in world.analysis().assists(&world.config.assist, frange)?.into_iter() {
751+
res.push(to_proto::code_action(&world, assist)?.into());
793752
}
794-
795753
Ok(Some(res))
796754
}
797755

crates/rust-analyzer/src/to_proto.rs

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -478,15 +478,6 @@ pub(crate) fn resource_op(
478478
Ok(res)
479479
}
480480

481-
pub(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-
490481
pub(crate) fn snippet_workspace_edit(
491482
world: &WorldSnapshot,
492483
source_change: SourceChange,
@@ -606,6 +597,7 @@ fn main() <fold>{
606597
pub(crate) fn code_action(world: &WorldSnapshot, assist: Assist) -> Result<lsp_ext::CodeAction> {
607598
let res = lsp_ext::CodeAction {
608599
title: assist.label,
600+
group: if world.config.client_caps.code_action_group { assist.group_label } else { None },
609601
kind: Some(String::new()),
610602
edit: Some(snippet_workspace_edit(world, assist.source_change)?),
611603
command: None,

docs/dev/lsp-extensions.md

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ It's a best effort document, when in doubt, consult the source (and send a PR wi
55
We aim to upstream all non Rust-specific extensions to the protocol, but this is not a top priority.
66
All capabilities are enabled via `experimental` field of `ClientCapabilities`.
77

8-
## `SnippetTextEdit`
8+
## Snippet `TextEdit`
99

1010
**Client Capability:** `{ "snippetTextEdit": boolean }`
1111

@@ -36,7 +36,7 @@ At the moment, rust-analyzer guarantees that only a single edit will have `Inser
3636
* Where exactly are `SnippetTextEdit`s allowed (only in code actions at the moment)?
3737
* Can snippets span multiple files (so far, no)?
3838

39-
## `joinLines`
39+
## Join Lines
4040

4141
**Server Capability:** `{ "joinLines": boolean }`
4242

@@ -119,3 +119,48 @@ SSR with query `foo($a:expr, $b:expr) ==>> ($a).foo($b)` will transform, eg `foo
119119

120120
* Probably needs search without replace mode
121121
* Needs a way to limit the scope to certain files.
122+
123+
## `CodeAction` Groups
124+
125+
**Client Capability:** `{ "codeActionGroup": boolean }`
126+
127+
If this capability is set, `CodeAction` returned from the server contain an additional field, `group`:
128+
129+
```typescript
130+
interface CodeAction {
131+
title: string;
132+
group?: string;
133+
...
134+
}
135+
```
136+
137+
All code-actions with the same `group` should be grouped under single (extendable) entry in lightbulb menu.
138+
The 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+
151+
Alternatively, selecting `frobnicate` could present a user with an additional menu to choose between `bar` and `baz`.
152+
153+
### Example
154+
155+
```rust
156+
fn main() {
157+
let x: Entry/*cursor here*/ = todo!();
158+
}
159+
```
160+
161+
Invoking 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?

editors/code/src/client.ts

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,23 +41,51 @@ export function createClient(serverPath: string, cwd: string): lc.LanguageClient
4141
return client.sendRequest(lc.CodeActionRequest.type, params, token).then((values) => {
4242
if (values === null) return undefined;
4343
const result: (vscode.CodeAction | vscode.Command)[] = [];
44+
const groups = new Map<string, { index: number; items: vscode.CodeAction[] }>();
4445
for (const item of values) {
4546
if (lc.CodeAction.is(item)) {
4647
const action = client.protocol2CodeConverter.asCodeAction(item);
47-
if (isSnippetEdit(item)) {
48+
const group = actionGroup(item);
49+
if (isSnippetEdit(item) || group) {
4850
action.command = {
4951
command: "rust-analyzer.applySnippetWorkspaceEdit",
5052
title: "",
5153
arguments: [action.edit],
5254
};
5355
action.edit = undefined;
5456
}
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+
}
5669
} else {
5770
const command = client.protocol2CodeConverter.asCommand(item);
5871
result.push(command);
5972
}
6073
}
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+
}
6189
return result;
6290
},
6391
(_error) => undefined
@@ -81,15 +109,16 @@ export function createClient(serverPath: string, cwd: string): lc.LanguageClient
81109
// implementations are still in the "proposed" category for 3.16.
82110
client.registerFeature(new CallHierarchyFeature(client));
83111
client.registerFeature(new SemanticTokensFeature(client));
84-
client.registerFeature(new SnippetTextEditFeature());
112+
client.registerFeature(new ExperimentalFeatures());
85113

86114
return client;
87115
}
88116

89-
class SnippetTextEditFeature implements lc.StaticFeature {
117+
class ExperimentalFeatures implements lc.StaticFeature {
90118
fillClientCapabilities(capabilities: lc.ClientCapabilities): void {
91119
const caps: any = capabilities.experimental ?? {};
92120
caps.snippetTextEdit = true;
121+
caps.codeActionGroup = true;
93122
capabilities.experimental = caps;
94123
}
95124
initialize(_capabilities: lc.ServerCapabilities<any>, _documentSelector: lc.DocumentSelector | undefined): void {
@@ -107,3 +136,7 @@ function isSnippetEdit(action: lc.CodeAction): boolean {
107136
}
108137
return false;
109138
}
139+
140+
function actionGroup(action: lc.CodeAction): string | undefined {
141+
return (action as any).group;
142+
}

editors/code/src/commands/index.ts

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -41,15 +41,11 @@ export function applySourceChange(ctx: Ctx): Cmd {
4141
};
4242
}
4343

44-
export function selectAndApplySourceChange(ctx: Ctx): Cmd {
45-
return async (changes: ra.SourceChange[]) => {
46-
if (changes.length === 1) {
47-
await sourceChange.applySourceChange(ctx, changes[0]);
48-
} else if (changes.length > 0) {
49-
const selectedChange = await vscode.window.showQuickPick(changes);
50-
if (!selectedChange) return;
51-
await sourceChange.applySourceChange(ctx, selectedChange);
52-
}
44+
export function applyActionGroup(_ctx: Ctx): Cmd {
45+
return async (actions: { label: string; edit: vscode.WorkspaceEdit }[]) => {
46+
const selectedAction = await vscode.window.showQuickPick(actions);
47+
if (!selectedAction) return;
48+
await applySnippetWorkspaceEdit(selectedAction.edit);
5349
};
5450
}
5551

editors/code/src/main.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ export async function activate(context: vscode.ExtensionContext) {
9292
ctx.registerCommand('showReferences', commands.showReferences);
9393
ctx.registerCommand('applySourceChange', commands.applySourceChange);
9494
ctx.registerCommand('applySnippetWorkspaceEdit', commands.applySnippetWorkspaceEditCommand);
95-
ctx.registerCommand('selectAndApplySourceChange', commands.selectAndApplySourceChange);
95+
ctx.registerCommand('applyActionGroup', commands.applyActionGroup);
9696

9797
ctx.pushCleanup(activateTaskProvider(workspaceFolder));
9898

0 commit comments

Comments
 (0)