Skip to content

Commit

Permalink
CodeAction groups
Browse files Browse the repository at this point in the history
  • Loading branch information
matklad committed May 22, 2020
1 parent 5ef4ebf commit ae1af8e
Show file tree
Hide file tree
Showing 9 changed files with 107 additions and 75 deletions.
9 changes: 7 additions & 2 deletions crates/rust-analyzer/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ pub struct ClientCapsConfig {
pub hierarchical_symbols: bool,
pub code_action_literals: bool,
pub work_done_progress: bool,
pub code_action_group: bool,
}

impl Default for Config {
Expand Down Expand Up @@ -294,9 +295,13 @@ impl Config {

self.assist.allow_snippets(false);
if let Some(experimental) = &caps.experimental {
let enable =
let snippet_text_edit =
experimental.get("snippetTextEdit").and_then(|it| it.as_bool()) == Some(true);
self.assist.allow_snippets(enable);
self.assist.allow_snippets(snippet_text_edit);

let code_action_group =
experimental.get("codeActionGroup").and_then(|it| it.as_bool()) == Some(true);
self.client_caps.code_action_group = code_action_group
}
}
}
1 change: 1 addition & 0 deletions crates/rust-analyzer/src/diagnostics/to_proto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ fn map_rust_child_diagnostic(
} else {
MappedRustChildDiagnostic::SuggestedFix(lsp_ext::CodeAction {
title: rd.message.clone(),
group: None,
kind: Some("quickfix".to_string()),
edit: Some(lsp_ext::SnippetWorkspaceEdit {
// FIXME: there's no good reason to use edit_map here....
Expand Down
2 changes: 2 additions & 0 deletions crates/rust-analyzer/src/lsp_ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,8 @@ impl Request for CodeActionRequest {
pub struct CodeAction {
pub title: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub group: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub kind: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub command: Option<lsp_types::Command>,
Expand Down
54 changes: 6 additions & 48 deletions crates/rust-analyzer/src/main_loop/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use lsp_types::{
SemanticTokensResult, SymbolInformation, TextDocumentIdentifier, Url, WorkspaceEdit,
};
use ra_ide::{
Assist, FileId, FilePosition, FileRange, Query, RangeInfo, Runnable, RunnableKind, SearchScope,
FileId, FilePosition, FileRange, Query, RangeInfo, Runnable, RunnableKind, SearchScope,
TextEdit,
};
use ra_prof::profile;
Expand Down Expand Up @@ -720,6 +720,7 @@ pub fn handle_code_action(
let file_id = from_proto::file_id(&world, &params.text_document.uri)?;
let line_index = world.analysis().file_line_index(file_id)?;
let range = from_proto::text_range(&line_index, params.range);
let frange = FileRange { file_id, range };

let diagnostics = world.analysis().diagnostics(file_id)?;
let mut res: Vec<lsp_ext::CodeAction> = Vec::new();
Expand All @@ -733,7 +734,8 @@ pub fn handle_code_action(
for source_edit in fixes_from_diagnostics {
let title = source_edit.label.clone();
let edit = to_proto::snippet_workspace_edit(&world, source_edit)?;
let action = lsp_ext::CodeAction { title, kind: None, edit: Some(edit), command: None };
let action =
lsp_ext::CodeAction { title, group: None, kind: None, edit: Some(edit), command: None };
res.push(action);
}

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

let mut grouped_assists: FxHashMap<String, (usize, Vec<Assist>)> = FxHashMap::default();
for assist in
world.analysis().assists(&world.config.assist, FileRange { file_id, range })?.into_iter()
{
match &assist.group_label {
Some(label) => grouped_assists
.entry(label.to_owned())
.or_insert_with(|| {
let idx = res.len();
let dummy = lsp_ext::CodeAction {
title: String::new(),
kind: None,
command: None,
edit: None,
};
res.push(dummy);
(idx, Vec::new())
})
.1
.push(assist),
None => {
res.push(to_proto::code_action(&world, assist)?.into());
}
}
}

for (group_label, (idx, assists)) in grouped_assists {
if assists.len() == 1 {
res[idx] = to_proto::code_action(&world, assists.into_iter().next().unwrap())?.into();
} else {
let title = group_label;

let mut arguments = Vec::with_capacity(assists.len());
for assist in assists {
let source_change = to_proto::source_change(&world, assist.source_change)?;
arguments.push(to_value(source_change)?);
}

let command = Some(Command {
title: title.clone(),
command: "rust-analyzer.selectAndApplySourceChange".to_string(),
arguments: Some(vec![serde_json::Value::Array(arguments)]),
});
res[idx] = lsp_ext::CodeAction { title, kind: None, edit: None, command };
}
for assist in world.analysis().assists(&world.config.assist, frange)?.into_iter() {
res.push(to_proto::code_action(&world, assist)?.into());
}

Ok(Some(res))
}

Expand Down
10 changes: 1 addition & 9 deletions crates/rust-analyzer/src/to_proto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -478,15 +478,6 @@ pub(crate) fn resource_op(
Ok(res)
}

pub(crate) fn source_change(
world: &WorldSnapshot,
source_change: SourceChange,
) -> Result<lsp_ext::SourceChange> {
let label = source_change.label.clone();
let workspace_edit = self::snippet_workspace_edit(world, source_change)?;
Ok(lsp_ext::SourceChange { label, workspace_edit, cursor_position: None })
}

pub(crate) fn snippet_workspace_edit(
world: &WorldSnapshot,
source_change: SourceChange,
Expand Down Expand Up @@ -606,6 +597,7 @@ fn main() <fold>{
pub(crate) fn code_action(world: &WorldSnapshot, assist: Assist) -> Result<lsp_ext::CodeAction> {
let res = lsp_ext::CodeAction {
title: assist.label,
group: if world.config.client_caps.code_action_group { assist.group_label } else { None },
kind: Some(String::new()),
edit: Some(snippet_workspace_edit(world, assist.source_change)?),
command: None,
Expand Down
49 changes: 47 additions & 2 deletions docs/dev/lsp-extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ It's a best effort document, when in doubt, consult the source (and send a PR wi
We aim to upstream all non Rust-specific extensions to the protocol, but this is not a top priority.
All capabilities are enabled via `experimental` field of `ClientCapabilities`.

## `SnippetTextEdit`
## Snippet `TextEdit`

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

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

## `joinLines`
## Join Lines

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

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

* Probably needs search without replace mode
* Needs a way to limit the scope to certain files.

## `CodeAction` Groups

**Client Capability:** `{ "codeActionGroup": boolean }`

If this capability is set, `CodeAction` returned from the server contain an additional field, `group`:

```typescript
interface CodeAction {
title: string;
group?: string;
...
}
```

All code-actions with the same `group` should be grouped under single (extendable) entry in lightbulb menu.
The set of actions `[ { title: "foo" }, { group: "frobnicate", title: "bar" }, { group: "frobnicate", title: "baz" }]` should be rendered as

```
💡
+-------------+
| foo |
+-------------+-----+
| frobnicate >| bar |
+-------------+-----+
| baz |
+-----+
```

Alternatively, selecting `frobnicate` could present a user with an additional menu to choose between `bar` and `baz`.

### Example

```rust
fn main() {
let x: Entry/*cursor here*/ = todo!();
}
```

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.

### Unresolved Questions

* Is a fixed two-level structure enough?
* Should we devise a general way to encode custom interaction protocols for GUI refactorings?
41 changes: 37 additions & 4 deletions editors/code/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,23 +41,51 @@ export function createClient(serverPath: string, cwd: string): lc.LanguageClient
return client.sendRequest(lc.CodeActionRequest.type, params, token).then((values) => {
if (values === null) return undefined;
const result: (vscode.CodeAction | vscode.Command)[] = [];
const groups = new Map<string, { index: number; items: vscode.CodeAction[] }>();
for (const item of values) {
if (lc.CodeAction.is(item)) {
const action = client.protocol2CodeConverter.asCodeAction(item);
if (isSnippetEdit(item)) {
const group = actionGroup(item);
if (isSnippetEdit(item) || group) {
action.command = {
command: "rust-analyzer.applySnippetWorkspaceEdit",
title: "",
arguments: [action.edit],
};
action.edit = undefined;
}
result.push(action);

if (group) {
let entry = groups.get(group);
if (!entry) {
entry = { index: result.length, items: [] };
groups.set(group, entry);
result.push(action);
}
entry.items.push(action);
} else {
result.push(action);
}
} else {
const command = client.protocol2CodeConverter.asCommand(item);
result.push(command);
}
}
for (const [group, { index, items }] of groups) {
if (items.length === 1) {
result[index] = items[0];
} else {
const action = new vscode.CodeAction(group);
action.command = {
command: "rust-analyzer.applyActionGroup",
title: "",
arguments: [items.map((item) => {
return { label: item.title, edit: item.command!!.arguments!![0] };
})],
};
result[index] = action;
}
}
return result;
},
(_error) => undefined
Expand All @@ -81,15 +109,16 @@ export function createClient(serverPath: string, cwd: string): lc.LanguageClient
// implementations are still in the "proposed" category for 3.16.
client.registerFeature(new CallHierarchyFeature(client));
client.registerFeature(new SemanticTokensFeature(client));
client.registerFeature(new SnippetTextEditFeature());
client.registerFeature(new ExperimentalFeatures());

return client;
}

class SnippetTextEditFeature implements lc.StaticFeature {
class ExperimentalFeatures implements lc.StaticFeature {
fillClientCapabilities(capabilities: lc.ClientCapabilities): void {
const caps: any = capabilities.experimental ?? {};
caps.snippetTextEdit = true;
caps.codeActionGroup = true;
capabilities.experimental = caps;
}
initialize(_capabilities: lc.ServerCapabilities<any>, _documentSelector: lc.DocumentSelector | undefined): void {
Expand All @@ -107,3 +136,7 @@ function isSnippetEdit(action: lc.CodeAction): boolean {
}
return false;
}

function actionGroup(action: lc.CodeAction): string | undefined {
return (action as any).group;
}
14 changes: 5 additions & 9 deletions editors/code/src/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,11 @@ export function applySourceChange(ctx: Ctx): Cmd {
};
}

export function selectAndApplySourceChange(ctx: Ctx): Cmd {
return async (changes: ra.SourceChange[]) => {
if (changes.length === 1) {
await sourceChange.applySourceChange(ctx, changes[0]);
} else if (changes.length > 0) {
const selectedChange = await vscode.window.showQuickPick(changes);
if (!selectedChange) return;
await sourceChange.applySourceChange(ctx, selectedChange);
}
export function applyActionGroup(_ctx: Ctx): Cmd {
return async (actions: { label: string; edit: vscode.WorkspaceEdit }[]) => {
const selectedAction = await vscode.window.showQuickPick(actions);
if (!selectedAction) return;
await applySnippetWorkspaceEdit(selectedAction.edit);
};
}

Expand Down
2 changes: 1 addition & 1 deletion editors/code/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export async function activate(context: vscode.ExtensionContext) {
ctx.registerCommand('showReferences', commands.showReferences);
ctx.registerCommand('applySourceChange', commands.applySourceChange);
ctx.registerCommand('applySnippetWorkspaceEdit', commands.applySnippetWorkspaceEditCommand);
ctx.registerCommand('selectAndApplySourceChange', commands.selectAndApplySourceChange);
ctx.registerCommand('applyActionGroup', commands.applyActionGroup);

ctx.pushCleanup(activateTaskProvider(workspaceFolder));

Expand Down

0 comments on commit ae1af8e

Please sign in to comment.