Add hover actions as LSP extension

This commit is contained in:
vsrs 2020-06-03 14:15:54 +03:00
parent 913a623281
commit 7d0dd17b09
11 changed files with 351 additions and 56 deletions

View file

@ -13,14 +13,43 @@ use ra_ide_db::{
use ra_syntax::{ast, match_ast, AstNode, SyntaxKind::*, SyntaxToken, TokenAtOffset};
use crate::{
display::{macro_label, rust_code_markup, rust_code_markup_with_doc, ShortLabel},
FilePosition, RangeInfo,
display::{macro_label, rust_code_markup, rust_code_markup_with_doc, ShortLabel, ToNav},
FilePosition, RangeInfo, NavigationTarget,
};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct HoverConfig {
pub implementations: bool,
}
impl Default for HoverConfig {
fn default() -> Self {
Self { implementations: true }
}
}
impl HoverConfig {
pub const NO_ACTIONS: Self = Self { implementations: false };
pub fn any(&self) -> bool {
self.implementations
}
pub fn none(&self) -> bool {
!self.any()
}
}
#[derive(Debug, Clone)]
pub enum HoverAction {
Implementaion(FilePosition),
}
/// Contains the results when hovering over an item
#[derive(Debug, Default)]
pub struct HoverResult {
results: Vec<String>,
actions: Vec<HoverAction>,
}
impl HoverResult {
@ -48,10 +77,20 @@ impl HoverResult {
&self.results
}
pub fn actions(&self) -> &[HoverAction] {
&self.actions
}
pub fn push_action(&mut self, action: HoverAction) {
self.actions.push(action);
}
/// Returns the results converted into markup
/// for displaying in a UI
///
/// Does not process actions!
pub fn to_markup(&self) -> String {
self.results.join("\n\n---\n")
self.results.join("\n\n___\n")
}
}
@ -82,6 +121,10 @@ pub(crate) fn hover(db: &RootDatabase, position: FilePosition) -> Option<RangeIn
res.extend(hover_text_from_name_kind(db, name_kind));
if !res.is_empty() {
if let Some(action) = show_implementations_action(db, name_kind) {
res.push_action(action);
}
return Some(RangeInfo::new(range, res));
}
}
@ -112,6 +155,26 @@ pub(crate) fn hover(db: &RootDatabase, position: FilePosition) -> Option<RangeIn
Some(RangeInfo::new(range, res))
}
fn show_implementations_action(db: &RootDatabase, def: Definition) -> Option<HoverAction> {
fn to_action(nav_target: NavigationTarget) -> HoverAction {
HoverAction::Implementaion(FilePosition {
file_id: nav_target.file_id(),
offset: nav_target.range().start(),
})
}
match def {
Definition::ModuleDef(it) => match it {
ModuleDef::Adt(Adt::Struct(it)) => Some(to_action(it.to_nav(db))),
ModuleDef::Adt(Adt::Union(it)) => Some(to_action(it.to_nav(db))),
ModuleDef::Adt(Adt::Enum(it)) => Some(to_action(it.to_nav(db))),
ModuleDef::Trait(it) => Some(to_action(it.to_nav(db))),
_ => None,
},
_ => None,
}
}
fn hover_text(
docs: Option<String>,
desc: Option<String>,
@ -228,6 +291,8 @@ fn pick_best(tokens: TokenAtOffset<SyntaxToken>) -> Option<SyntaxToken> {
#[cfg(test)]
mod tests {
use super::*;
use ra_db::FileLoader;
use ra_syntax::TextRange;
@ -241,7 +306,14 @@ mod tests {
s.map(trim_markup)
}
fn check_hover_result(fixture: &str, expected: &[&str]) -> String {
fn assert_impl_action(action: &HoverAction, position: u32) {
let offset = match action {
HoverAction::Implementaion(pos) => pos.offset
};
assert_eq!(offset, position.into());
}
fn check_hover_result(fixture: &str, expected: &[&str]) -> (String, Vec<HoverAction>) {
let (analysis, position) = analysis_and_position(fixture);
let hover = analysis.hover(position).unwrap().unwrap();
let mut results = Vec::from(hover.info.results());
@ -256,7 +328,7 @@ mod tests {
assert_eq!(hover.info.len(), expected.len());
let content = analysis.db.file_text(position.file_id);
content[hover.range].to_string()
(content[hover.range].to_string(), hover.info.actions().to_vec())
}
fn check_hover_no_result(fixture: &str) {
@ -746,7 +818,7 @@ fn func(foo: i32) { if true { <|>foo; }; }
#[test]
fn test_hover_through_macro() {
let hover_on = check_hover_result(
let (hover_on, _) = check_hover_result(
"
//- /lib.rs
macro_rules! id {
@ -767,7 +839,7 @@ fn func(foo: i32) { if true { <|>foo; }; }
#[test]
fn test_hover_through_expr_in_macro() {
let hover_on = check_hover_result(
let (hover_on, _) = check_hover_result(
"
//- /lib.rs
macro_rules! id {
@ -785,7 +857,7 @@ fn func(foo: i32) { if true { <|>foo; }; }
#[test]
fn test_hover_through_expr_in_macro_recursive() {
let hover_on = check_hover_result(
let (hover_on, _) = check_hover_result(
"
//- /lib.rs
macro_rules! id_deep {
@ -806,7 +878,7 @@ fn func(foo: i32) { if true { <|>foo; }; }
#[test]
fn test_hover_through_func_in_macro_recursive() {
let hover_on = check_hover_result(
let (hover_on, _) = check_hover_result(
"
//- /lib.rs
macro_rules! id_deep {
@ -830,7 +902,7 @@ fn func(foo: i32) { if true { <|>foo; }; }
#[test]
fn test_hover_through_literal_string_in_macro() {
let hover_on = check_hover_result(
let (hover_on, _) = check_hover_result(
r#"
//- /lib.rs
macro_rules! arr {
@ -849,7 +921,7 @@ fn func(foo: i32) { if true { <|>foo; }; }
#[test]
fn test_hover_through_assert_macro() {
let hover_on = check_hover_result(
let (hover_on, _) = check_hover_result(
r#"
//- /lib.rs
#[rustc_builtin_macro]
@ -925,13 +997,14 @@ fn func(foo: i32) { if true { <|>foo; }; }
#[test]
fn test_hover_trait_show_qualifiers() {
check_hover_result(
let (_, actions) = check_hover_result(
"
//- /lib.rs
unsafe trait foo<|>() {}
",
&["unsafe trait foo"],
);
assert_impl_action(&actions[0], 13);
}
#[test]
@ -1052,4 +1125,41 @@ fn func(foo: i32) { if true { <|>foo; }; }
&["Bar\n```\n\n```rust\nfn foo(&self)\n```\n___\n\nDo the foo"],
);
}
#[test]
fn test_hover_trait_hash_impl_action() {
let (_, actions) = check_hover_result(
"
//- /lib.rs
trait foo<|>() {}
",
&["trait foo"],
);
assert_impl_action(&actions[0], 6);
}
#[test]
fn test_hover_struct_hash_impl_action() {
let (_, actions) = check_hover_result(
"
//- /lib.rs
struct foo<|>() {}
",
&["struct foo"],
);
assert_impl_action(&actions[0], 7);
}
#[test]
fn test_hover_union_hash_impl_action() {
let (_, actions) = check_hover_result(
"
//- /lib.rs
union foo<|>() {}
",
&["union foo"],
);
assert_impl_action(&actions[0], 6);
}
}

View file

@ -66,7 +66,7 @@ pub use crate::{
display::{file_structure, FunctionSignature, NavigationTarget, StructureNode},
expand_macro::ExpandedMacro,
folding_ranges::{Fold, FoldKind},
hover::HoverResult,
hover::{HoverResult, HoverAction, HoverConfig},
inlay_hints::{InlayHint, InlayHintsConfig, InlayKind},
references::{Declaration, Reference, ReferenceAccess, ReferenceKind, ReferenceSearchResult},
runnables::{Runnable, RunnableKind, TestId},

View file

@ -18,7 +18,7 @@ use ra_syntax::{
use crate::RootDatabase;
// FIXME: a more precise name would probably be `Symbol`?
#[derive(Debug, PartialEq, Eq)]
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
pub enum Definition {
Macro(MacroDef),
Field(Field),

View file

@ -11,7 +11,7 @@ use std::{ffi::OsString, path::PathBuf};
use lsp_types::ClientCapabilities;
use ra_flycheck::FlycheckConfig;
use ra_ide::{AssistConfig, CompletionConfig, InlayHintsConfig};
use ra_ide::{AssistConfig, CompletionConfig, InlayHintsConfig, HoverConfig};
use ra_project_model::{CargoConfig, JsonProject, ProjectManifest};
use serde::Deserialize;
@ -34,6 +34,7 @@ pub struct Config {
pub assist: AssistConfig,
pub call_info_full: bool,
pub lens: LensConfig,
pub hover: HoverConfig,
pub with_sysroot: bool,
pub linked_projects: Vec<LinkedProject>,
@ -124,6 +125,7 @@ pub struct ClientCapsConfig {
pub work_done_progress: bool,
pub code_action_group: bool,
pub resolve_code_action: bool,
pub hover_actions: bool,
}
impl Default for Config {
@ -162,6 +164,7 @@ impl Default for Config {
assist: AssistConfig::default(),
call_info_full: true,
lens: LensConfig::default(),
hover: HoverConfig::default(),
linked_projects: Vec::new(),
}
}
@ -278,6 +281,14 @@ impl Config {
}
}
let mut use_hover_actions = false;
set(value, "/hoverActions/enable", &mut use_hover_actions);
if use_hover_actions {
set(value, "/hoverActions/implementations", &mut self.hover.implementations);
} else {
self.hover = HoverConfig::NO_ACTIONS;
}
log::info!("Config::update() = {:#?}", self);
fn get<'a, T: Deserialize<'a>>(value: &'a serde_json::Value, pointer: &str) -> Option<T> {
@ -331,17 +342,14 @@ impl Config {
self.assist.allow_snippets(false);
if let Some(experimental) = &caps.experimental {
let snippet_text_edit =
experimental.get("snippetTextEdit").and_then(|it| it.as_bool()) == Some(true);
let get_bool = |index: &str| experimental.get(index).and_then(|it| it.as_bool()) == Some(true);
let snippet_text_edit = get_bool("snippetTextEdit");
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;
let resolve_code_action =
experimental.get("resolveCodeAction").and_then(|it| it.as_bool()) == Some(true);
self.client_caps.resolve_code_action = resolve_code_action;
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");
}
}
}

View file

@ -260,3 +260,37 @@ pub struct SnippetTextEdit {
#[serde(skip_serializing_if = "Option::is_none")]
pub insert_text_format: Option<lsp_types::InsertTextFormat>,
}
pub enum HoverRequest {}
impl Request for HoverRequest {
type Params = lsp_types::HoverParams;
type Result = Option<Hover>;
const METHOD: &'static str = "textDocument/hover";
}
#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)]
pub struct Hover {
pub contents: lsp_types::HoverContents,
#[serde(skip_serializing_if = "Option::is_none")]
pub range: Option<Range>,
#[serde(skip_serializing_if = "Option::is_none")]
pub actions: Option<Vec<CommandLinkGroup>>,
}
#[derive(Debug, PartialEq, Eq, Clone, Default, Deserialize, Serialize)]
pub struct CommandLinkGroup {
pub title: Option<String>,
pub commands: Vec<CommandLink>,
}
// LSP v3.15 Command does not have a `tooltip` field, vscode supports one.
#[derive(Debug, PartialEq, Eq, Clone, Default, Deserialize, Serialize)]
pub struct CommandLink {
pub title: String,
pub command: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub tooltip: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub arguments: Option<Vec<serde_json::Value>>,
}

View file

@ -510,6 +510,7 @@ fn on_request(
.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::HoverRequest>(handlers::handle_hover)?
.on::<lsp_types::request::OnTypeFormatting>(handlers::handle_on_type_formatting)?
.on::<lsp_types::request::DocumentSymbolRequest>(handlers::handle_document_symbol)?
.on::<lsp_types::request::WorkspaceSymbol>(handlers::handle_workspace_symbol)?
@ -521,7 +522,6 @@ fn on_request(
.on::<lsp_types::request::CodeLensResolve>(handlers::handle_code_lens_resolve)?
.on::<lsp_types::request::FoldingRangeRequest>(handlers::handle_folding_range)?
.on::<lsp_types::request::SignatureHelpRequest>(handlers::handle_signature_help)?
.on::<lsp_types::request::HoverRequest>(handlers::handle_hover)?
.on::<lsp_types::request::PrepareRenameRequest>(handlers::handle_prepare_rename)?
.on::<lsp_types::request::Rename>(handlers::handle_rename)?
.on::<lsp_types::request::References>(handlers::handle_references)?

View file

@ -12,13 +12,14 @@ use lsp_types::{
CallHierarchyIncomingCall, CallHierarchyIncomingCallsParams, CallHierarchyItem,
CallHierarchyOutgoingCall, CallHierarchyOutgoingCallsParams, CallHierarchyPrepareParams,
CodeLens, Command, CompletionItem, Diagnostic, DocumentFormattingParams, DocumentHighlight,
DocumentSymbol, FoldingRange, FoldingRangeParams, Hover, HoverContents, Location,
MarkupContent, MarkupKind, Position, PrepareRenameResponse, Range, RenameParams,
SemanticTokensParams, SemanticTokensRangeParams, SemanticTokensRangeResult,
SemanticTokensResult, SymbolInformation, TextDocumentIdentifier, Url, WorkspaceEdit,
DocumentSymbol, FoldingRange, FoldingRangeParams, HoverContents, Location, MarkupContent,
MarkupKind, Position, PrepareRenameResponse, Range, RenameParams, SemanticTokensParams,
SemanticTokensRangeParams, SemanticTokensRangeResult, SemanticTokensResult, SymbolInformation,
TextDocumentIdentifier, Url, WorkspaceEdit,
};
use ra_ide::{
FileId, FilePosition, FileRange, Query, RangeInfo, RunnableKind, SearchScope, TextEdit,
FileId, FilePosition, FileRange, HoverAction, Query, RangeInfo, RunnableKind, SearchScope,
TextEdit,
};
use ra_prof::profile;
use ra_project_model::TargetKind;
@ -537,7 +538,7 @@ pub fn handle_signature_help(
pub fn handle_hover(
snap: GlobalStateSnapshot,
params: lsp_types::HoverParams,
) -> Result<Option<Hover>> {
) -> Result<Option<lsp_ext::Hover>> {
let _p = profile("handle_hover");
let position = from_proto::file_position(&snap, params.text_document_position_params)?;
let info = match snap.analysis().hover(position)? {
@ -546,12 +547,13 @@ pub fn handle_hover(
};
let line_index = snap.analysis.file_line_index(position.file_id)?;
let range = to_proto::range(&line_index, info.range);
let res = Hover {
let res = lsp_ext::Hover {
contents: HoverContents::Markup(MarkupContent {
kind: MarkupKind::Markdown,
value: crate::markdown::format_docs(&info.info.to_markup()),
}),
range: Some(range),
actions: Some(prepare_hover_actions(&world, info.info.actions())),
};
Ok(Some(res))
}
@ -924,24 +926,13 @@ pub fn handle_code_lens_resolve(
_ => vec![],
};
let title = if locations.len() == 1 {
"1 implementation".into()
} else {
format!("{} implementations", locations.len())
};
// We cannot use the 'editor.action.showReferences' command directly
// because that command requires vscode types which we convert in the handler
// on the client side.
let cmd = Command {
let title = implementation_title(locations.len());
let cmd = show_references_command(
title,
command: "rust-analyzer.showReferences".into(),
arguments: Some(vec![
to_value(&lens_params.text_document_position_params.text_document.uri).unwrap(),
to_value(code_lens.range.start).unwrap(),
to_value(locations).unwrap(),
]),
};
&lens_params.text_document_position_params.text_document.uri,
code_lens.range.start,
locations,
);
Ok(CodeLens { range: code_lens.range, command: Some(cmd), data: None })
}
None => Ok(CodeLens {
@ -1145,3 +1136,83 @@ pub fn handle_semantic_tokens_range(
let semantic_tokens = to_proto::semantic_tokens(&text, &line_index, highlights);
Ok(Some(semantic_tokens.into()))
}
fn implementation_title(count: usize) -> String {
if count == 1 {
"1 implementation".into()
} else {
format!("{} implementations", count)
}
}
fn show_references_command(
title: String,
uri: &lsp_types::Url,
position: lsp_types::Position,
locations: Vec<lsp_types::Location>,
) -> Command {
// We cannot use the 'editor.action.showReferences' command directly
// because that command requires vscode types which we convert in the handler
// on the client side.
Command {
title,
command: "rust-analyzer.showReferences".into(),
arguments: Some(vec![
to_value(uri).unwrap(),
to_value(position).unwrap(),
to_value(locations).unwrap(),
]),
}
}
fn to_command_link(command: Command, tooltip: String) -> lsp_ext::CommandLink {
lsp_ext::CommandLink {
tooltip: Some(tooltip),
title: command.title,
command: command.command,
arguments: command.arguments,
}
}
fn show_impl_command_link(
world: &WorldSnapshot,
position: &FilePosition,
) -> Option<lsp_ext::CommandLinkGroup> {
if world.config.hover.implementations {
if let Some(nav_data) = world.analysis().goto_implementation(*position).unwrap_or(None) {
let uri = to_proto::url(world, position.file_id).ok()?;
let line_index = world.analysis().file_line_index(position.file_id).ok()?;
let position = to_proto::position(&line_index, position.offset);
let locations: Vec<_> = nav_data
.info
.iter()
.filter_map(|it| to_proto::location(world, it.file_range()).ok())
.collect();
let title = implementation_title(locations.len());
let command = show_references_command(title, &uri, position, locations);
return Some(lsp_ext::CommandLinkGroup {
commands: vec![to_command_link(command, "Go to implementations".into())],
..Default::default()
});
}
}
None
}
fn prepare_hover_actions(
world: &WorldSnapshot,
actions: &[HoverAction],
) -> Vec<lsp_ext::CommandLinkGroup> {
if world.config.hover.none() || !world.config.client_caps.hover_actions {
return Vec::new();
}
actions
.iter()
.filter_map(|it| match it {
HoverAction::Implementaion(position) => show_impl_command_link(world, position),
})
.collect()
}

View file

@ -462,17 +462,27 @@
"default": true
},
"rust-analyzer.lens.run": {
"markdownDescription": "Whether to show Run lens. Only applies when `#rust-analyzer.lens.enable#` is set.",
"markdownDescription": "Whether to show `Run` lens. Only applies when `#rust-analyzer.lens.enable#` is set.",
"type": "boolean",
"default": true
},
"rust-analyzer.lens.debug": {
"markdownDescription": "Whether to show Debug lens. Only applies when `#rust-analyzer.lens.enable#` is set.",
"markdownDescription": "Whether to show `Debug` lens. Only applies when `#rust-analyzer.lens.enable#` is set.",
"type": "boolean",
"default": true
},
"rust-analyzer.lens.implementations": {
"markdownDescription": "Whether to show Implementations lens. Only applies when `#rust-analyzer.lens.enable#` is set.",
"markdownDescription": "Whether to show `Implementations` lens. Only applies when `#rust-analyzer.lens.enable#` is set.",
"type": "boolean",
"default": true
},
"rust-analyzer.hoverActions.enable": {
"description": "Whether to show HoverActions in Rust files.",
"type": "boolean",
"default": true
},
"rust-analyzer.hoverActions.implementations": {
"markdownDescription": "Whether to show `Implementations` action. Only applies when `#rust-analyzer.hoverActions.enable#` is set.",
"type": "boolean",
"default": true
},

View file

@ -7,6 +7,29 @@ import { CallHierarchyFeature } from 'vscode-languageclient/lib/callHierarchy.pr
import { SemanticTokensFeature, DocumentSemanticsTokensSignature } from 'vscode-languageclient/lib/semanticTokens.proposed';
import { assert } from './util';
function toTrusted(obj: vscode.MarkedString): vscode.MarkedString {
const md = <vscode.MarkdownString>obj;
if (md && md.value.includes("```rust")) {
md.isTrusted = true;
return md;
}
return obj;
}
function renderCommand(cmd: CommandLink) {
return `[${cmd.title}](command:${cmd.command}?${encodeURIComponent(JSON.stringify(cmd.arguments))} '${cmd.tooltip!}')`;
}
function renderHoverActions(actions: CommandLinkGroup[]): vscode.MarkdownString {
const text = actions.map(group =>
(group.title ? (group.title + " ") : "") + group.commands.map(renderCommand).join(' | ')
).join('___');
const result = new vscode.MarkdownString(text);
result.isTrusted = true;
return result;
}
export function createClient(serverPath: string, cwd: string): lc.LanguageClient {
// '.' Is the fallback if no folder is open
// TODO?: Workspace folders support Uri's (eg: file://test.txt).
@ -35,6 +58,27 @@ export function createClient(serverPath: string, cwd: string): lc.LanguageClient
if (res === undefined) throw new Error('busy');
return res;
},
async provideHover(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken, _next: lc.ProvideHoverSignature) {
return client.sendRequest(lc.HoverRequest.type, client.code2ProtocolConverter.asTextDocumentPositionParams(document, position), token).then(
(result) => {
const hover = client.protocol2CodeConverter.asHover(result);
if (hover) {
// Workaround to support command links (trusted vscode.MarkdownString) in hovers
// https://github.com/microsoft/vscode/issues/33577
hover.contents = hover.contents.map(toTrusted);
const actions = (<any>result).actions;
if (actions) {
hover.contents.push(renderHoverActions(actions));
}
}
return hover;
},
(error) => {
client.logFailedRequest(lc.HoverRequest.type, error);
return Promise.resolve(null);
});
},
// Using custom handling of CodeActions where each code action is resloved lazily
// That's why we are not waiting for any command or edits
async provideCodeActions(document: vscode.TextDocument, range: vscode.Range, context: vscode.CodeActionContext, token: vscode.CancellationToken, _next: lc.ProvideCodeActionsSignature) {
@ -129,6 +173,7 @@ class ExperimentalFeatures implements lc.StaticFeature {
caps.snippetTextEdit = true;
caps.codeActionGroup = true;
caps.resolveCodeAction = true;
caps.hoverActions = true;
capabilities.experimental = caps;
}
initialize(_capabilities: lc.ServerCapabilities<any>, _documentSelector: lc.DocumentSelector | undefined): void {

View file

@ -16,10 +16,8 @@ export class Config {
"files",
"highlighting",
"updates.channel",
"lens.enable",
"lens.run",
"lens.debug",
"lens.implementations",
"lens", // works as lens.*
"hoverActions", // works as hoverActions.*
]
.map(opt => `${this.rootSection}.${opt}`);
@ -132,4 +130,11 @@ export class Config {
implementations: this.get<boolean>("lens.implementations"),
};
}
get hoverActions() {
return {
enable: this.get<boolean>("hoverActions.enable"),
implementations: this.get<boolean>("hoverActions.implementations"),
};
}
}

View file

@ -90,3 +90,15 @@ export interface SsrParams {
parseOnly: boolean;
}
export const ssr = new lc.RequestType<SsrParams, lc.WorkspaceEdit, void>('experimental/ssr');
export interface CommandLink extends lc.Command {
/**
* A tooltip for the command, when represented in the UI.
*/
tooltip?: string;
}
export interface CommandLinkGroup {
title?: string;
commands: CommandLink[];
}