Switch to upstream protocol for resolving code action

Note that we have to maintain custom implementation on the client
side: I don't see how to marry bulitin resolve support with groups and
snippets.
This commit is contained in:
Aleksey Kladov 2020-11-10 18:20:01 +01:00
parent ada5a88f8f
commit 7d2eb000b0
15 changed files with 86 additions and 91 deletions

View file

@ -16,8 +16,6 @@ use serde_json::json;
use crate::semantic_tokens;
pub fn server_capabilities(client_caps: &ClientCapabilities) -> ServerCapabilities {
let code_action_provider = code_action_capabilities(client_caps);
ServerCapabilities {
text_document_sync: Some(TextDocumentSyncCapability::Options(TextDocumentSyncOptions {
open_close: Some(true),
@ -49,7 +47,7 @@ pub fn server_capabilities(client_caps: &ClientCapabilities) -> ServerCapabiliti
document_highlight_provider: Some(OneOf::Left(true)),
document_symbol_provider: Some(OneOf::Left(true)),
workspace_symbol_provider: Some(OneOf::Left(true)),
code_action_provider: Some(code_action_provider),
code_action_provider: Some(code_action_capabilities(client_caps)),
code_lens_provider: Some(CodeLensOptions { resolve_provider: Some(true) }),
document_formatting_provider: Some(OneOf::Left(true)),
document_range_formatting_provider: None,
@ -113,7 +111,7 @@ fn code_action_capabilities(client_caps: &ClientCapabilities) -> CodeActionProvi
CodeActionKind::REFACTOR_INLINE,
CodeActionKind::REFACTOR_REWRITE,
]),
resolve_provider: None,
resolve_provider: Some(true),
work_done_progress_options: Default::default(),
})
})

View file

@ -144,7 +144,7 @@ pub struct ClientCapsConfig {
pub code_action_literals: bool,
pub work_done_progress: bool,
pub code_action_group: bool,
pub resolve_code_action: bool,
pub code_action_resolve: bool,
pub hover_actions: bool,
pub status_notification: bool,
pub signature_help_label_offsets: bool,
@ -383,6 +383,14 @@ impl Config {
}
}
}
if let Some(code_action) = &doc_caps.code_action {
if let Some(resolve_support) = &code_action.resolve_support {
if resolve_support.properties.iter().any(|it| it == "edit") {
self.client_caps.code_action_resolve = true;
}
}
}
}
if let Some(window_caps) = caps.window.as_ref() {
@ -400,7 +408,6 @@ impl Config {
self.assist.allow_snippets(snippet_text_edit);
self.client_caps.code_action_group = get_bool("codeActionGroup");
self.client_caps.resolve_code_action = get_bool("resolveCodeAction");
self.client_caps.hover_actions = get_bool("hoverActions");
self.client_caps.status_notification = get_bool("statusNotification");
}

View file

@ -36,7 +36,6 @@
fixes: [
CodeAction {
title: "consider prefixing with an underscore",
id: None,
group: None,
kind: Some(
CodeActionKind(
@ -70,6 +69,7 @@
is_preferred: Some(
true,
),
data: None,
},
],
},

View file

@ -36,7 +36,6 @@
fixes: [
CodeAction {
title: "consider prefixing with an underscore",
id: None,
group: None,
kind: Some(
CodeActionKind(
@ -70,6 +69,7 @@
is_preferred: Some(
true,
),
data: None,
},
],
},

View file

@ -36,7 +36,6 @@
fixes: [
CodeAction {
title: "consider prefixing with an underscore",
id: None,
group: None,
kind: Some(
CodeActionKind(
@ -70,6 +69,7 @@
is_preferred: Some(
true,
),
data: None,
},
],
},

View file

@ -51,7 +51,6 @@
fixes: [
CodeAction {
title: "return the expression directly",
id: None,
group: None,
kind: Some(
CodeActionKind(
@ -98,6 +97,7 @@
is_preferred: Some(
true,
),
data: None,
},
],
},

View file

@ -110,7 +110,6 @@ fn map_rust_child_diagnostic(
} else {
MappedRustChildDiagnostic::SuggestedFix(lsp_ext::CodeAction {
title: rd.message.clone(),
id: None,
group: None,
kind: Some(lsp_types::CodeActionKind::QUICKFIX),
edit: Some(lsp_ext::SnippetWorkspaceEdit {
@ -119,6 +118,7 @@ fn map_rust_child_diagnostic(
document_changes: None,
}),
is_preferred: Some(true),
data: None,
})
}
}

View file

@ -806,11 +806,11 @@ fn handle_fixes(
let edit = to_proto::snippet_workspace_edit(&snap, fix.source_change)?;
let action = lsp_ext::CodeAction {
title: fix.label.to_string(),
id: None,
group: None,
kind: Some(CodeActionKind::QUICKFIX),
edit: Some(edit),
is_preferred: Some(false),
data: None,
};
res.push(action);
}
@ -852,11 +852,11 @@ pub(crate) fn handle_code_action(
handle_fixes(&snap, &params, &mut res)?;
if snap.config.client_caps.resolve_code_action {
if snap.config.client_caps.code_action_resolve {
for (index, assist) in
snap.analysis.unresolved_assists(&snap.config.assist, frange)?.into_iter().enumerate()
{
res.push(to_proto::unresolved_code_action(&snap, assist, index)?);
res.push(to_proto::unresolved_code_action(&snap, params.clone(), assist, index)?);
}
} else {
for assist in snap.analysis.resolved_assists(&snap.config.assist, frange)?.into_iter() {
@ -867,11 +867,16 @@ pub(crate) fn handle_code_action(
Ok(Some(res))
}
pub(crate) fn handle_resolve_code_action(
pub(crate) fn handle_code_action_resolve(
mut snap: GlobalStateSnapshot,
params: lsp_ext::ResolveCodeActionParams,
) -> Result<Option<lsp_ext::SnippetWorkspaceEdit>> {
let _p = profile::span("handle_resolve_code_action");
mut code_action: lsp_ext::CodeAction,
) -> Result<lsp_ext::CodeAction> {
let _p = profile::span("handle_code_action_resolve");
let params = match code_action.data.take() {
Some(it) => it,
None => Err("can't resolve code action without data")?,
};
let file_id = from_proto::file_id(&snap, &params.code_action_params.text_document.uri)?;
let line_index = snap.analysis.file_line_index(file_id)?;
let range = from_proto::text_range(&line_index, params.code_action_params.range);
@ -888,7 +893,9 @@ pub(crate) fn handle_resolve_code_action(
let index = index.parse::<usize>().unwrap();
let assist = &assists[index];
assert!(assist.assist.id.0 == id);
Ok(to_proto::resolved_code_action(&snap, assist.clone())?.edit)
let edit = to_proto::resolved_code_action(&snap, assist.clone())?.edit;
code_action.edit = edit;
Ok(code_action)
}
pub(crate) fn handle_code_lens(

View file

@ -113,22 +113,6 @@ pub struct JoinLinesParams {
pub ranges: Vec<Range>,
}
pub enum ResolveCodeActionRequest {}
impl Request for ResolveCodeActionRequest {
type Params = ResolveCodeActionParams;
type Result = Option<SnippetWorkspaceEdit>;
const METHOD: &'static str = "experimental/resolveCodeAction";
}
/// Params for the ResolveCodeActionRequest
#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ResolveCodeActionParams {
pub code_action_params: lsp_types::CodeActionParams,
pub id: String,
}
pub enum OnEnter {}
impl Request for OnEnter {
@ -265,13 +249,18 @@ impl Request for CodeActionRequest {
const METHOD: &'static str = "textDocument/codeAction";
}
pub enum CodeActionResolveRequest {}
impl Request for CodeActionResolveRequest {
type Params = CodeAction;
type Result = CodeAction;
const METHOD: &'static str = "codeAction/resolve";
}
#[derive(Debug, PartialEq, Clone, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CodeAction {
pub title: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub group: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub kind: Option<CodeActionKind>,
@ -282,6 +271,16 @@ pub struct CodeAction {
pub edit: Option<SnippetWorkspaceEdit>,
#[serde(skip_serializing_if = "Option::is_none")]
pub is_preferred: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<CodeActionData>,
}
#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CodeActionData {
pub code_action_params: lsp_types::CodeActionParams,
pub id: String,
}
#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)]

View file

@ -435,7 +435,7 @@ impl GlobalState {
.on::<lsp_ext::Runnables>(handlers::handle_runnables)
.on::<lsp_ext::InlayHints>(handlers::handle_inlay_hints)
.on::<lsp_ext::CodeActionRequest>(handlers::handle_code_action)
.on::<lsp_ext::ResolveCodeActionRequest>(handlers::handle_resolve_code_action)
.on::<lsp_ext::CodeActionResolveRequest>(handlers::handle_code_action_resolve)
.on::<lsp_ext::HoverRequest>(handlers::handle_hover)
.on::<lsp_ext::ExternalDocs>(handlers::handle_open_docs)
.on::<lsp_types::request::OnTypeFormatting>(handlers::handle_on_type_formatting)

View file

@ -735,16 +735,20 @@ pub(crate) fn code_action_kind(kind: AssistKind) -> lsp_types::CodeActionKind {
pub(crate) fn unresolved_code_action(
snap: &GlobalStateSnapshot,
code_action_params: lsp_types::CodeActionParams,
assist: Assist,
index: usize,
) -> Result<lsp_ext::CodeAction> {
let res = lsp_ext::CodeAction {
title: assist.label.to_string(),
id: Some(format!("{}:{}", assist.id.0, index.to_string())),
group: assist.group.filter(|_| snap.config.client_caps.code_action_group).map(|gr| gr.0),
kind: Some(code_action_kind(assist.id.1)),
edit: None,
is_preferred: None,
data: Some(lsp_ext::CodeActionData {
id: format!("{}:{}", assist.id.0, index.to_string()),
code_action_params,
}),
};
Ok(res)
}
@ -754,13 +758,19 @@ pub(crate) fn resolved_code_action(
assist: ResolvedAssist,
) -> Result<lsp_ext::CodeAction> {
let change = assist.source_change;
unresolved_code_action(snap, assist.assist, 0).and_then(|it| {
Ok(lsp_ext::CodeAction {
id: None,
let res = lsp_ext::CodeAction {
edit: Some(snippet_workspace_edit(snap, change)?),
..it
})
})
title: assist.assist.label.to_string(),
group: assist
.assist
.group
.filter(|_| snap.config.client_caps.code_action_group)
.map(|gr| gr.0),
kind: Some(code_action_kind(assist.assist.id.1)),
is_preferred: None,
data: None,
};
Ok(res)
}
pub(crate) fn runnable(

View file

@ -1,5 +1,5 @@
<!---
lsp_ext.rs hash: 286f8bbac885531a
lsp_ext.rs hash: 4f86fb54e4b2870e
If you need to change the above hash to make the test pass, please check if you
need to adjust this doc as well and ping this issue:
@ -109,30 +109,6 @@ Invoking code action at this position will yield two code actions for importing
* Is a fixed two-level structure enough?
* Should we devise a general way to encode custom interaction protocols for GUI refactorings?
## Lazy assists with `ResolveCodeAction`
**Issue:** https://github.com/microsoft/language-server-protocol/issues/787
**Client Capability** `{ "resolveCodeAction": boolean }`
If this capability is set, the assists will be computed lazily. Thus `CodeAction` returned from the server will only contain `id` but not `edit` or `command` fields. The only exclusion from the rule is the diagnostic edits.
After the client got the id, it should then call `experimental/resolveCodeAction` command on the server and provide the following payload:
```typescript
interface ResolveCodeActionParams {
id: string;
codeActionParams: lc.CodeActionParams;
}
```
As a result of the command call the client will get the respective workspace edit (`lc.WorkspaceEdit`).
### Unresolved Questions
* Apply smarter filtering for ids?
* Upon `resolveCodeAction` command only call the assits which should be resolved and not all of them?
## Parent Module
**Issue:** https://github.com/microsoft/language-server-protocol/issues/1002

View file

@ -4,6 +4,7 @@ import * as ra from '../src/lsp_ext';
import * as Is from 'vscode-languageclient/lib/common/utils/is';
import { DocumentSemanticsTokensSignature, DocumentSemanticsTokensEditsSignature, DocumentRangeSemanticTokensSignature } from 'vscode-languageclient/lib/common/semanticTokens';
import { assert } from './util';
import { WorkspaceEdit } from 'vscode';
function renderCommand(cmd: ra.CommandLink) {
return `[${cmd.title}](command:${cmd.command}?${encodeURIComponent(JSON.stringify(cmd.arguments))} '${cmd.tooltip!}')`;
@ -75,8 +76,8 @@ export function createClient(serverPath: string, cwd: string): lc.LanguageClient
return Promise.resolve(null);
});
},
// Using custom handling of CodeActions where each code action is resolved lazily
// That's why we are not waiting for any command or edits
// Using custom handling of CodeActions to support action groups and snippet edits.
// Note that this means we have to re-implement lazy edit resolving ourselves as well.
async provideCodeActions(document: vscode.TextDocument, range: vscode.Range, context: vscode.CodeActionContext, token: vscode.CancellationToken, _next: lc.ProvideCodeActionsSignature) {
const params: lc.CodeActionParams = {
textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(document),
@ -99,16 +100,15 @@ export function createClient(serverPath: string, cwd: string): lc.LanguageClient
const kind = client.protocol2CodeConverter.asCodeActionKind((item as any).kind);
const action = new vscode.CodeAction(item.title, kind);
const group = (item as any).group;
const id = (item as any).id;
const resolveParams: ra.ResolveCodeActionParams = {
id: id,
codeActionParams: params
};
action.command = {
command: "rust-analyzer.resolveCodeAction",
title: item.title,
arguments: [resolveParams],
arguments: [item],
};
// Set a dummy edit, so that VS Code doesn't try to resolve this.
action.edit = new WorkspaceEdit();
if (group) {
let entry = groups.get(group);
if (!entry) {
@ -134,6 +134,10 @@ export function createClient(serverPath: string, cwd: string): lc.LanguageClient
return { label: item.title, arguments: item.command!!.arguments!![0] };
})],
};
// Set a dummy edit, so that VS Code doesn't try to resolve this.
action.edit = new WorkspaceEdit();
result[index] = action;
}
}
@ -164,7 +168,6 @@ class ExperimentalFeatures implements lc.StaticFeature {
const caps: any = capabilities.experimental ?? {};
caps.snippetTextEdit = true;
caps.codeActionGroup = true;
caps.resolveCodeAction = true;
caps.hoverActions = true;
caps.statusNotification = true;
capabilities.experimental = caps;

View file

@ -395,7 +395,7 @@ export function showReferences(ctx: Ctx): Cmd {
}
export function applyActionGroup(_ctx: Ctx): Cmd {
return async (actions: { label: string; arguments: ra.ResolveCodeActionParams }[]) => {
return async (actions: { label: string; arguments: lc.CodeAction }[]) => {
const selectedAction = await vscode.window.showQuickPick(actions);
if (!selectedAction) return;
vscode.commands.executeCommand(
@ -442,12 +442,13 @@ export function openDocs(ctx: Ctx): Cmd {
export function resolveCodeAction(ctx: Ctx): Cmd {
const client = ctx.client;
return async (params: ra.ResolveCodeActionParams) => {
const item: lc.WorkspaceEdit = await client.sendRequest(ra.resolveCodeAction, params);
if (!item) {
return async (params: lc.CodeAction) => {
params.command = undefined;
const item = await client.sendRequest(lc.CodeActionResolveRequest.type, params);
if (!item.edit) {
return;
}
const edit = client.protocol2CodeConverter.asWorkspaceEdit(item);
const edit = client.protocol2CodeConverter.asWorkspaceEdit(item.edit);
await applySnippetWorkspaceEdit(edit);
};
}

View file

@ -43,12 +43,6 @@ export const matchingBrace = new lc.RequestType<MatchingBraceParams, lc.Position
export const parentModule = new lc.RequestType<lc.TextDocumentPositionParams, lc.LocationLink[], void>("experimental/parentModule");
export interface ResolveCodeActionParams {
id: string;
codeActionParams: lc.CodeActionParams;
}
export const resolveCodeAction = new lc.RequestType<ResolveCodeActionParams, lc.WorkspaceEdit, unknown>('experimental/resolveCodeAction');
export interface JoinLinesParams {
textDocument: lc.TextDocumentIdentifier;
ranges: lc.Range[];