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 {
&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")
}
}
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));
}
}
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>,
#[cfg(test)]
mod tests {
+ use super::*;
+
use ra_db::FileLoader;
use ra_syntax::TextRange;
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());
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) {
#[test]
fn test_hover_through_macro() {
- let hover_on = check_hover_result(
+ let (hover_on, _) = check_hover_result(
"
//- /lib.rs
macro_rules! id {
#[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 {
#[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 {
#[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 {
#[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 {
#[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]
#[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]
&["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);
+ }
+
}
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},
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),
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;
pub assist: AssistConfig,
pub call_info_full: bool,
pub lens: LensConfig,
+ pub hover: HoverConfig,
pub with_sysroot: bool,
pub linked_projects: Vec<LinkedProject>,
pub work_done_progress: bool,
pub code_action_group: bool,
pub resolve_code_action: bool,
+ pub hover_actions: bool,
}
impl Default for Config {
assist: AssistConfig::default(),
call_info_full: true,
lens: LensConfig::default(),
+ hover: HoverConfig::default(),
linked_projects: Vec::new(),
}
}
}
}
+ 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> {
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);
- self.assist.allow_snippets(snippet_text_edit);
+ let get_bool = |index: &str| experimental.get(index).and_then(|it| it.as_bool()) == Some(true);
- 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 snippet_text_edit = get_bool("snippetTextEdit");
+ self.assist.allow_snippets(snippet_text_edit);
- 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");
}
}
}
#[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>>,
+}
.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)?
.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)?
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;
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)? {
};
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))
}
_ => 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 {
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()
+}
"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
},
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).
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) {
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 {
"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}`);
implementations: this.get<boolean>("lens.implementations"),
};
}
+
+ get hoverActions() {
+ return {
+ enable: this.get<boolean>("hoverActions.enable"),
+ implementations: this.get<boolean>("hoverActions.implementations"),
+ };
+ }
}
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[];
+}