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

View file

@ -144,7 +144,7 @@ pub struct ClientCapsConfig {
pub code_action_literals: bool, pub code_action_literals: bool,
pub work_done_progress: bool, pub work_done_progress: bool,
pub code_action_group: bool, pub code_action_group: bool,
pub resolve_code_action: bool, pub code_action_resolve: bool,
pub hover_actions: bool, pub hover_actions: bool,
pub status_notification: bool, pub status_notification: bool,
pub signature_help_label_offsets: 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() { if let Some(window_caps) = caps.window.as_ref() {
@ -400,7 +408,6 @@ impl Config {
self.assist.allow_snippets(snippet_text_edit); self.assist.allow_snippets(snippet_text_edit);
self.client_caps.code_action_group = get_bool("codeActionGroup"); 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.hover_actions = get_bool("hoverActions");
self.client_caps.status_notification = get_bool("statusNotification"); self.client_caps.status_notification = get_bool("statusNotification");
} }

View file

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

View file

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

View file

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

View file

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

View file

@ -110,7 +110,6 @@ fn map_rust_child_diagnostic(
} else { } else {
MappedRustChildDiagnostic::SuggestedFix(lsp_ext::CodeAction { MappedRustChildDiagnostic::SuggestedFix(lsp_ext::CodeAction {
title: rd.message.clone(), title: rd.message.clone(),
id: None,
group: None, group: None,
kind: Some(lsp_types::CodeActionKind::QUICKFIX), kind: Some(lsp_types::CodeActionKind::QUICKFIX),
edit: Some(lsp_ext::SnippetWorkspaceEdit { edit: Some(lsp_ext::SnippetWorkspaceEdit {
@ -119,6 +118,7 @@ fn map_rust_child_diagnostic(
document_changes: None, document_changes: None,
}), }),
is_preferred: Some(true), 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 edit = to_proto::snippet_workspace_edit(&snap, fix.source_change)?;
let action = lsp_ext::CodeAction { let action = lsp_ext::CodeAction {
title: fix.label.to_string(), title: fix.label.to_string(),
id: None,
group: None, group: None,
kind: Some(CodeActionKind::QUICKFIX), kind: Some(CodeActionKind::QUICKFIX),
edit: Some(edit), edit: Some(edit),
is_preferred: Some(false), is_preferred: Some(false),
data: None,
}; };
res.push(action); res.push(action);
} }
@ -852,11 +852,11 @@ pub(crate) fn handle_code_action(
handle_fixes(&snap, &params, &mut res)?; 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 for (index, assist) in
snap.analysis.unresolved_assists(&snap.config.assist, frange)?.into_iter().enumerate() 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 { } else {
for assist in snap.analysis.resolved_assists(&snap.config.assist, frange)?.into_iter() { 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)) Ok(Some(res))
} }
pub(crate) fn handle_resolve_code_action( pub(crate) fn handle_code_action_resolve(
mut snap: GlobalStateSnapshot, mut snap: GlobalStateSnapshot,
params: lsp_ext::ResolveCodeActionParams, mut code_action: lsp_ext::CodeAction,
) -> Result<Option<lsp_ext::SnippetWorkspaceEdit>> { ) -> Result<lsp_ext::CodeAction> {
let _p = profile::span("handle_resolve_code_action"); 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 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 line_index = snap.analysis.file_line_index(file_id)?;
let range = from_proto::text_range(&line_index, params.code_action_params.range); 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 index = index.parse::<usize>().unwrap();
let assist = &assists[index]; let assist = &assists[index];
assert!(assist.assist.id.0 == id); 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( pub(crate) fn handle_code_lens(

View file

@ -113,22 +113,6 @@ pub struct JoinLinesParams {
pub ranges: Vec<Range>, 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 {} pub enum OnEnter {}
impl Request for OnEnter { impl Request for OnEnter {
@ -265,13 +249,18 @@ impl Request for CodeActionRequest {
const METHOD: &'static str = "textDocument/codeAction"; 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)] #[derive(Debug, PartialEq, Clone, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct CodeAction { pub struct CodeAction {
pub title: String, pub title: String,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub group: Option<String>, pub group: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub kind: Option<CodeActionKind>, pub kind: Option<CodeActionKind>,
@ -282,6 +271,16 @@ pub struct CodeAction {
pub edit: Option<SnippetWorkspaceEdit>, pub edit: Option<SnippetWorkspaceEdit>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub is_preferred: Option<bool>, 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)] #[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::Runnables>(handlers::handle_runnables)
.on::<lsp_ext::InlayHints>(handlers::handle_inlay_hints) .on::<lsp_ext::InlayHints>(handlers::handle_inlay_hints)
.on::<lsp_ext::CodeActionRequest>(handlers::handle_code_action) .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::HoverRequest>(handlers::handle_hover)
.on::<lsp_ext::ExternalDocs>(handlers::handle_open_docs) .on::<lsp_ext::ExternalDocs>(handlers::handle_open_docs)
.on::<lsp_types::request::OnTypeFormatting>(handlers::handle_on_type_formatting) .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( pub(crate) fn unresolved_code_action(
snap: &GlobalStateSnapshot, snap: &GlobalStateSnapshot,
code_action_params: lsp_types::CodeActionParams,
assist: Assist, assist: Assist,
index: usize, index: usize,
) -> Result<lsp_ext::CodeAction> { ) -> Result<lsp_ext::CodeAction> {
let res = lsp_ext::CodeAction { let res = lsp_ext::CodeAction {
title: assist.label.to_string(), 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), group: assist.group.filter(|_| snap.config.client_caps.code_action_group).map(|gr| gr.0),
kind: Some(code_action_kind(assist.id.1)), kind: Some(code_action_kind(assist.id.1)),
edit: None, edit: None,
is_preferred: None, is_preferred: None,
data: Some(lsp_ext::CodeActionData {
id: format!("{}:{}", assist.id.0, index.to_string()),
code_action_params,
}),
}; };
Ok(res) Ok(res)
} }
@ -754,13 +758,19 @@ pub(crate) fn resolved_code_action(
assist: ResolvedAssist, assist: ResolvedAssist,
) -> Result<lsp_ext::CodeAction> { ) -> Result<lsp_ext::CodeAction> {
let change = assist.source_change; let change = assist.source_change;
unresolved_code_action(snap, assist.assist, 0).and_then(|it| { let res = lsp_ext::CodeAction {
Ok(lsp_ext::CodeAction { edit: Some(snippet_workspace_edit(snap, change)?),
id: None, title: assist.assist.label.to_string(),
edit: Some(snippet_workspace_edit(snap, change)?), group: assist
..it .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( 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 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: 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? * Is a fixed two-level structure enough?
* Should we devise a general way to encode custom interaction protocols for GUI refactorings? * 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 ## Parent Module
**Issue:** https://github.com/microsoft/language-server-protocol/issues/1002 **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 * as Is from 'vscode-languageclient/lib/common/utils/is';
import { DocumentSemanticsTokensSignature, DocumentSemanticsTokensEditsSignature, DocumentRangeSemanticTokensSignature } from 'vscode-languageclient/lib/common/semanticTokens'; import { DocumentSemanticsTokensSignature, DocumentSemanticsTokensEditsSignature, DocumentRangeSemanticTokensSignature } from 'vscode-languageclient/lib/common/semanticTokens';
import { assert } from './util'; import { assert } from './util';
import { WorkspaceEdit } from 'vscode';
function renderCommand(cmd: ra.CommandLink) { function renderCommand(cmd: ra.CommandLink) {
return `[${cmd.title}](command:${cmd.command}?${encodeURIComponent(JSON.stringify(cmd.arguments))} '${cmd.tooltip!}')`; 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); return Promise.resolve(null);
}); });
}, },
// Using custom handling of CodeActions where each code action is resolved lazily // Using custom handling of CodeActions to support action groups and snippet edits.
// That's why we are not waiting for any command or 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) { async provideCodeActions(document: vscode.TextDocument, range: vscode.Range, context: vscode.CodeActionContext, token: vscode.CancellationToken, _next: lc.ProvideCodeActionsSignature) {
const params: lc.CodeActionParams = { const params: lc.CodeActionParams = {
textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(document), 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 kind = client.protocol2CodeConverter.asCodeActionKind((item as any).kind);
const action = new vscode.CodeAction(item.title, kind); const action = new vscode.CodeAction(item.title, kind);
const group = (item as any).group; const group = (item as any).group;
const id = (item as any).id;
const resolveParams: ra.ResolveCodeActionParams = {
id: id,
codeActionParams: params
};
action.command = { action.command = {
command: "rust-analyzer.resolveCodeAction", command: "rust-analyzer.resolveCodeAction",
title: item.title, 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) { if (group) {
let entry = groups.get(group); let entry = groups.get(group);
if (!entry) { if (!entry) {
@ -134,6 +134,10 @@ export function createClient(serverPath: string, cwd: string): lc.LanguageClient
return { label: item.title, arguments: item.command!!.arguments!![0] }; 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; result[index] = action;
} }
} }
@ -164,7 +168,6 @@ class ExperimentalFeatures implements lc.StaticFeature {
const caps: any = capabilities.experimental ?? {}; const caps: any = capabilities.experimental ?? {};
caps.snippetTextEdit = true; caps.snippetTextEdit = true;
caps.codeActionGroup = true; caps.codeActionGroup = true;
caps.resolveCodeAction = true;
caps.hoverActions = true; caps.hoverActions = true;
caps.statusNotification = true; caps.statusNotification = true;
capabilities.experimental = caps; capabilities.experimental = caps;

View file

@ -395,7 +395,7 @@ export function showReferences(ctx: Ctx): Cmd {
} }
export function applyActionGroup(_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); const selectedAction = await vscode.window.showQuickPick(actions);
if (!selectedAction) return; if (!selectedAction) return;
vscode.commands.executeCommand( vscode.commands.executeCommand(
@ -442,12 +442,13 @@ export function openDocs(ctx: Ctx): Cmd {
export function resolveCodeAction(ctx: Ctx): Cmd { export function resolveCodeAction(ctx: Ctx): Cmd {
const client = ctx.client; const client = ctx.client;
return async (params: ra.ResolveCodeActionParams) => { return async (params: lc.CodeAction) => {
const item: lc.WorkspaceEdit = await client.sendRequest(ra.resolveCodeAction, params); params.command = undefined;
if (!item) { const item = await client.sendRequest(lc.CodeActionResolveRequest.type, params);
if (!item.edit) {
return; return;
} }
const edit = client.protocol2CodeConverter.asWorkspaceEdit(item); const edit = client.protocol2CodeConverter.asWorkspaceEdit(item.edit);
await applySnippetWorkspaceEdit(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 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 { export interface JoinLinesParams {
textDocument: lc.TextDocumentIdentifier; textDocument: lc.TextDocumentIdentifier;
ranges: lc.Range[]; ranges: lc.Range[];