bors[bot] 1cc73d60bb
Merge #7068
7068: Add VSCode command to view the hir of a function body r=theotherphil a=theotherphil

Will fix https://github.com/rust-analyzer/rust-analyzer/issues/7061. Very rough initial version just to work out where I needed to wire everything up.

@matklad would you be happy merging a hir visualiser of some kind? If so, do you have any thoughts on what you'd like it show, and how?

I've spent very little time on this thus far, so I'm fine with throwing away the contents of this PR, but I want to avoid taking the time to make this more polished/interactive/useful only to discover that no-one else has any interest in this functionality.

![image](https://user-images.githubusercontent.com/1974256/103236081-bb58f700-493b-11eb-9d12-55ae1b870f8f.png)


Co-authored-by: Phil Ellison <phil.j.ellison@gmail.com>
2021-01-03 09:03:15 +00:00

1700 lines
60 KiB
Rust

//! This module is responsible for implementing handlers for Language Server
//! Protocol. The majority of requests are fulfilled by calling into the
//! `ide` crate.
use std::{
io::Write as _,
process::{self, Stdio},
sync::Arc,
};
use ide::{
AssistConfig, CompletionResolveCapability, FileId, FilePosition, FileRange, HoverAction,
HoverGotoTypeData, LineIndex, NavigationTarget, Query, RangeInfo, Runnable, RunnableKind,
SearchScope, SourceChange, SymbolKind, TextEdit,
};
use itertools::Itertools;
use lsp_server::ErrorCode;
use lsp_types::{
CallHierarchyIncomingCall, CallHierarchyIncomingCallsParams, CallHierarchyItem,
CallHierarchyOutgoingCall, CallHierarchyOutgoingCallsParams, CallHierarchyPrepareParams,
CodeActionKind, CodeLens, Command, CompletionItem, Diagnostic, DiagnosticTag,
DocumentFormattingParams, DocumentHighlight, DocumentSymbol, FoldingRange, FoldingRangeParams,
HoverContents, Location, NumberOrString, Position, PrepareRenameResponse, Range, RenameParams,
SemanticTokensDeltaParams, SemanticTokensFullDeltaResult, SemanticTokensParams,
SemanticTokensRangeParams, SemanticTokensRangeResult, SemanticTokensResult, SymbolInformation,
SymbolTag, TextDocumentIdentifier, TextDocumentPositionParams, Url, WorkspaceEdit,
};
use project_model::TargetKind;
use serde::{Deserialize, Serialize};
use serde_json::to_value;
use stdx::{format_to, split_once};
use syntax::{algo, ast, AstNode, TextRange, TextSize};
use crate::diff::diff;
use crate::{
cargo_target_spec::CargoTargetSpec,
config::RustfmtConfig,
from_json, from_proto,
global_state::{GlobalState, GlobalStateSnapshot},
lsp_ext::{self, InlayHint, InlayHintsParams},
lsp_utils::all_edits_are_disjoint,
to_proto, LspError, Result,
};
pub(crate) fn handle_analyzer_status(
snap: GlobalStateSnapshot,
params: lsp_ext::AnalyzerStatusParams,
) -> Result<String> {
let _p = profile::span("handle_analyzer_status");
let mut buf = String::new();
let mut file_id = None;
if let Some(tdi) = params.text_document {
match from_proto::file_id(&snap, &tdi.uri) {
Ok(it) => file_id = Some(it),
Err(_) => format_to!(buf, "file {} not found in vfs", tdi.uri),
}
}
if snap.workspaces.is_empty() {
buf.push_str("no workspaces\n")
} else {
buf.push_str("workspaces:\n");
for w in snap.workspaces.iter() {
format_to!(buf, "{} packages loaded\n", w.n_packages());
}
}
buf.push_str("\nanalysis:\n");
buf.push_str(
&snap
.analysis
.status(file_id)
.unwrap_or_else(|_| "Analysis retrieval was cancelled".to_owned()),
);
format_to!(buf, "\n\nrequests:\n");
let requests = snap.latest_requests.read();
for (is_last, r) in requests.iter() {
let mark = if is_last { "*" } else { " " };
format_to!(buf, "{}{:4} {:<36}{}ms\n", mark, r.id, r.method, r.duration.as_millis());
}
Ok(buf)
}
pub(crate) fn handle_memory_usage(state: &mut GlobalState, _: ()) -> Result<String> {
let _p = profile::span("handle_memory_usage");
let mem = state.analysis_host.per_query_memory_usage();
let mut out = String::new();
for (name, bytes) in mem {
format_to!(out, "{:>8} {}\n", bytes, name);
}
Ok(out)
}
pub(crate) fn handle_syntax_tree(
snap: GlobalStateSnapshot,
params: lsp_ext::SyntaxTreeParams,
) -> Result<String> {
let _p = profile::span("handle_syntax_tree");
let id = from_proto::file_id(&snap, &params.text_document.uri)?;
let line_index = snap.analysis.file_line_index(id)?;
let text_range = params.range.map(|r| from_proto::text_range(&line_index, r));
let res = snap.analysis.syntax_tree(id, text_range)?;
Ok(res)
}
pub(crate) fn handle_view_hir(
snap: GlobalStateSnapshot,
params: lsp_types::TextDocumentPositionParams,
) -> Result<String> {
let _p = profile::span("handle_view_hir");
let position = from_proto::file_position(&snap, params)?;
let res = snap.analysis.view_hir(position)?;
Ok(res)
}
pub(crate) fn handle_expand_macro(
snap: GlobalStateSnapshot,
params: lsp_ext::ExpandMacroParams,
) -> Result<Option<lsp_ext::ExpandedMacro>> {
let _p = profile::span("handle_expand_macro");
let file_id = from_proto::file_id(&snap, &params.text_document.uri)?;
let line_index = snap.analysis.file_line_index(file_id)?;
let offset = from_proto::offset(&line_index, params.position);
let res = snap.analysis.expand_macro(FilePosition { file_id, offset })?;
Ok(res.map(|it| lsp_ext::ExpandedMacro { name: it.name, expansion: it.expansion }))
}
pub(crate) fn handle_selection_range(
snap: GlobalStateSnapshot,
params: lsp_types::SelectionRangeParams,
) -> Result<Option<Vec<lsp_types::SelectionRange>>> {
let _p = profile::span("handle_selection_range");
let file_id = from_proto::file_id(&snap, &params.text_document.uri)?;
let line_index = snap.analysis.file_line_index(file_id)?;
let res: Result<Vec<lsp_types::SelectionRange>> = params
.positions
.into_iter()
.map(|position| {
let offset = from_proto::offset(&line_index, position);
let mut ranges = Vec::new();
{
let mut range = TextRange::new(offset, offset);
loop {
ranges.push(range);
let frange = FileRange { file_id, range };
let next = snap.analysis.extend_selection(frange)?;
if next == range {
break;
} else {
range = next
}
}
}
let mut range = lsp_types::SelectionRange {
range: to_proto::range(&line_index, *ranges.last().unwrap()),
parent: None,
};
for &r in ranges.iter().rev().skip(1) {
range = lsp_types::SelectionRange {
range: to_proto::range(&line_index, r),
parent: Some(Box::new(range)),
}
}
Ok(range)
})
.collect();
Ok(Some(res?))
}
pub(crate) fn handle_matching_brace(
snap: GlobalStateSnapshot,
params: lsp_ext::MatchingBraceParams,
) -> Result<Vec<Position>> {
let _p = profile::span("handle_matching_brace");
let file_id = from_proto::file_id(&snap, &params.text_document.uri)?;
let line_index = snap.analysis.file_line_index(file_id)?;
let res = params
.positions
.into_iter()
.map(|position| {
let offset = from_proto::offset(&line_index, position);
let offset = match snap.analysis.matching_brace(FilePosition { file_id, offset }) {
Ok(Some(matching_brace_offset)) => matching_brace_offset,
Err(_) | Ok(None) => offset,
};
to_proto::position(&line_index, offset)
})
.collect();
Ok(res)
}
pub(crate) fn handle_join_lines(
snap: GlobalStateSnapshot,
params: lsp_ext::JoinLinesParams,
) -> Result<Vec<lsp_types::TextEdit>> {
let _p = profile::span("handle_join_lines");
let file_id = from_proto::file_id(&snap, &params.text_document.uri)?;
let line_index = snap.analysis.file_line_index(file_id)?;
let line_endings = snap.file_line_endings(file_id);
let mut res = TextEdit::default();
for range in params.ranges {
let range = from_proto::text_range(&line_index, range);
let edit = snap.analysis.join_lines(FileRange { file_id, range })?;
match res.union(edit) {
Ok(()) => (),
Err(_edit) => {
// just ignore overlapping edits
}
}
}
let res = to_proto::text_edit_vec(&line_index, line_endings, res);
Ok(res)
}
pub(crate) fn handle_on_enter(
snap: GlobalStateSnapshot,
params: lsp_types::TextDocumentPositionParams,
) -> Result<Option<Vec<lsp_ext::SnippetTextEdit>>> {
let _p = profile::span("handle_on_enter");
let position = from_proto::file_position(&snap, params)?;
let edit = match snap.analysis.on_enter(position)? {
None => return Ok(None),
Some(it) => it,
};
let line_index = snap.analysis.file_line_index(position.file_id)?;
let line_endings = snap.file_line_endings(position.file_id);
let edit = to_proto::snippet_text_edit_vec(&line_index, line_endings, true, edit);
Ok(Some(edit))
}
// Don't forget to add new trigger characters to `ServerCapabilities` in `caps.rs`.
pub(crate) fn handle_on_type_formatting(
snap: GlobalStateSnapshot,
params: lsp_types::DocumentOnTypeFormattingParams,
) -> Result<Option<Vec<lsp_types::TextEdit>>> {
let _p = profile::span("handle_on_type_formatting");
let mut position = from_proto::file_position(&snap, params.text_document_position)?;
let line_index = snap.analysis.file_line_index(position.file_id)?;
let line_endings = snap.file_line_endings(position.file_id);
// in `ide`, the `on_type` invariant is that
// `text.char_at(position) == typed_char`.
position.offset -= TextSize::of('.');
let char_typed = params.ch.chars().next().unwrap_or('\0');
assert!({
let text = snap.analysis.file_text(position.file_id)?;
text[usize::from(position.offset)..].starts_with(char_typed)
});
// We have an assist that inserts ` ` after typing `->` in `fn foo() ->{`,
// but it requires precise cursor positioning to work, and one can't
// position the cursor with on_type formatting. So, let's just toggle this
// feature off here, hoping that we'll enable it one day, 😿.
if char_typed == '>' {
return Ok(None);
}
let edit = snap.analysis.on_char_typed(position, char_typed)?;
let mut edit = match edit {
Some(it) => it,
None => return Ok(None),
};
// This should be a single-file edit
let edit = edit.source_file_edits.pop().unwrap();
let change = to_proto::text_edit_vec(&line_index, line_endings, edit.edit);
Ok(Some(change))
}
pub(crate) fn handle_document_symbol(
snap: GlobalStateSnapshot,
params: lsp_types::DocumentSymbolParams,
) -> Result<Option<lsp_types::DocumentSymbolResponse>> {
let _p = profile::span("handle_document_symbol");
let file_id = from_proto::file_id(&snap, &params.text_document.uri)?;
let line_index = snap.analysis.file_line_index(file_id)?;
let mut parents: Vec<(DocumentSymbol, Option<usize>)> = Vec::new();
for symbol in snap.analysis.file_structure(file_id)? {
let mut tags = Vec::new();
if symbol.deprecated {
tags.push(SymbolTag::Deprecated)
};
#[allow(deprecated)]
let doc_symbol = DocumentSymbol {
name: symbol.label,
detail: symbol.detail,
kind: to_proto::symbol_kind(symbol.kind),
tags: Some(tags),
deprecated: Some(symbol.deprecated),
range: to_proto::range(&line_index, symbol.node_range),
selection_range: to_proto::range(&line_index, symbol.navigation_range),
children: None,
};
parents.push((doc_symbol, symbol.parent));
}
// Builds hierarchy from a flat list, in reverse order (so that indices
// makes sense)
let document_symbols = {
let mut acc = Vec::new();
while let Some((mut node, parent_idx)) = parents.pop() {
if let Some(children) = &mut node.children {
children.reverse();
}
let parent = match parent_idx {
None => &mut acc,
Some(i) => parents[i].0.children.get_or_insert_with(Vec::new),
};
parent.push(node);
}
acc.reverse();
acc
};
let res = if snap.config.client_caps.hierarchical_symbols {
document_symbols.into()
} else {
let url = to_proto::url(&snap, file_id);
let mut symbol_information = Vec::<SymbolInformation>::new();
for symbol in document_symbols {
flatten_document_symbol(&symbol, None, &url, &mut symbol_information);
}
symbol_information.into()
};
return Ok(Some(res));
fn flatten_document_symbol(
symbol: &DocumentSymbol,
container_name: Option<String>,
url: &Url,
res: &mut Vec<SymbolInformation>,
) {
let mut tags = Vec::new();
#[allow(deprecated)]
match symbol.deprecated {
Some(true) => tags.push(SymbolTag::Deprecated),
_ => {}
}
#[allow(deprecated)]
res.push(SymbolInformation {
name: symbol.name.clone(),
kind: symbol.kind,
tags: Some(tags),
deprecated: symbol.deprecated,
location: Location::new(url.clone(), symbol.range),
container_name,
});
for child in symbol.children.iter().flatten() {
flatten_document_symbol(child, Some(symbol.name.clone()), url, res);
}
}
}
pub(crate) fn handle_workspace_symbol(
snap: GlobalStateSnapshot,
params: lsp_types::WorkspaceSymbolParams,
) -> Result<Option<Vec<SymbolInformation>>> {
let _p = profile::span("handle_workspace_symbol");
let all_symbols = params.query.contains('#');
let libs = params.query.contains('*');
let query = {
let query: String = params.query.chars().filter(|&c| c != '#' && c != '*').collect();
let mut q = Query::new(query);
if !all_symbols {
q.only_types();
}
if libs {
q.libs();
}
q.limit(128);
q
};
let mut res = exec_query(&snap, query)?;
if res.is_empty() && !all_symbols {
let mut query = Query::new(params.query);
query.limit(128);
res = exec_query(&snap, query)?;
}
return Ok(Some(res));
fn exec_query(snap: &GlobalStateSnapshot, query: Query) -> Result<Vec<SymbolInformation>> {
let mut res = Vec::new();
for nav in snap.analysis.symbol_search(query)? {
let container_name = nav.container_name.as_ref().map(|v| v.to_string());
#[allow(deprecated)]
let info = SymbolInformation {
name: nav.name.to_string(),
kind: nav
.kind
.map(to_proto::symbol_kind)
.unwrap_or(lsp_types::SymbolKind::Variable),
tags: None,
location: to_proto::location_from_nav(snap, nav)?,
container_name,
deprecated: None,
};
res.push(info);
}
Ok(res)
}
}
pub(crate) fn handle_will_rename_files(
snap: GlobalStateSnapshot,
params: lsp_types::RenameFilesParams,
) -> Result<Option<lsp_types::WorkspaceEdit>> {
let _p = profile::span("handle_will_rename_files");
let source_changes: Vec<SourceChange> = params
.files
.into_iter()
.filter_map(|file_rename| {
let from = Url::parse(&file_rename.old_uri).ok()?;
let to = Url::parse(&file_rename.new_uri).ok()?;
let from_path = from.to_file_path().ok()?;
let to_path = to.to_file_path().ok()?;
// Limit to single-level moves for now.
match (from_path.parent(), to_path.parent()) {
(Some(p1), Some(p2)) if p1 == p2 => {
let new_name = to_path.file_stem()?;
let new_name = new_name.to_str()?;
Some((snap.url_to_file_id(&from).ok()?, new_name.to_string()))
}
_ => None,
}
})
.filter_map(|(file_id, new_name)| {
snap.analysis.will_rename_file(file_id, &new_name).ok()?
})
.collect();
// Drop file system edits since we're just renaming things on the same level
let edits = source_changes.into_iter().map(|it| it.source_file_edits).flatten().collect();
let source_change = SourceChange::from_edits(edits, Vec::new());
let workspace_edit = to_proto::workspace_edit(&snap, source_change)?;
Ok(Some(workspace_edit))
}
pub(crate) fn handle_goto_definition(
snap: GlobalStateSnapshot,
params: lsp_types::GotoDefinitionParams,
) -> Result<Option<lsp_types::GotoDefinitionResponse>> {
let _p = profile::span("handle_goto_definition");
let position = from_proto::file_position(&snap, params.text_document_position_params)?;
let nav_info = match snap.analysis.goto_definition(position)? {
None => return Ok(None),
Some(it) => it,
};
let src = FileRange { file_id: position.file_id, range: nav_info.range };
let res = to_proto::goto_definition_response(&snap, Some(src), nav_info.info)?;
Ok(Some(res))
}
pub(crate) fn handle_goto_implementation(
snap: GlobalStateSnapshot,
params: lsp_types::request::GotoImplementationParams,
) -> Result<Option<lsp_types::request::GotoImplementationResponse>> {
let _p = profile::span("handle_goto_implementation");
let position = from_proto::file_position(&snap, params.text_document_position_params)?;
let nav_info = match snap.analysis.goto_implementation(position)? {
None => return Ok(None),
Some(it) => it,
};
let src = FileRange { file_id: position.file_id, range: nav_info.range };
let res = to_proto::goto_definition_response(&snap, Some(src), nav_info.info)?;
Ok(Some(res))
}
pub(crate) fn handle_goto_type_definition(
snap: GlobalStateSnapshot,
params: lsp_types::request::GotoTypeDefinitionParams,
) -> Result<Option<lsp_types::request::GotoTypeDefinitionResponse>> {
let _p = profile::span("handle_goto_type_definition");
let position = from_proto::file_position(&snap, params.text_document_position_params)?;
let nav_info = match snap.analysis.goto_type_definition(position)? {
None => return Ok(None),
Some(it) => it,
};
let src = FileRange { file_id: position.file_id, range: nav_info.range };
let res = to_proto::goto_definition_response(&snap, Some(src), nav_info.info)?;
Ok(Some(res))
}
pub(crate) fn handle_parent_module(
snap: GlobalStateSnapshot,
params: lsp_types::TextDocumentPositionParams,
) -> Result<Option<lsp_types::GotoDefinitionResponse>> {
let _p = profile::span("handle_parent_module");
let position = from_proto::file_position(&snap, params)?;
let navs = snap.analysis.parent_module(position)?;
let res = to_proto::goto_definition_response(&snap, None, navs)?;
Ok(Some(res))
}
pub(crate) fn handle_runnables(
snap: GlobalStateSnapshot,
params: lsp_ext::RunnablesParams,
) -> Result<Vec<lsp_ext::Runnable>> {
let _p = profile::span("handle_runnables");
let file_id = from_proto::file_id(&snap, &params.text_document.uri)?;
let line_index = snap.analysis.file_line_index(file_id)?;
let offset = params.position.map(|it| from_proto::offset(&line_index, it));
let cargo_spec = CargoTargetSpec::for_file(&snap, file_id)?;
let expect_test = match offset {
Some(offset) => {
let source_file = snap.analysis.parse(file_id)?;
algo::find_node_at_offset::<ast::MacroCall>(source_file.syntax(), offset)
.and_then(|it| it.path()?.segment()?.name_ref())
.map_or(false, |it| it.text() == "expect" || it.text() == "expect_file")
}
None => false,
};
let mut res = Vec::new();
for runnable in snap.analysis.runnables(file_id)? {
if let Some(offset) = offset {
if !runnable.nav.full_range.contains_inclusive(offset) {
continue;
}
}
if should_skip_target(&runnable, cargo_spec.as_ref()) {
continue;
}
let mut runnable = to_proto::runnable(&snap, file_id, runnable)?;
if expect_test {
runnable.label = format!("{} + expect", runnable.label);
runnable.args.expect_test = Some(true);
}
res.push(runnable);
}
// Add `cargo check` and `cargo test` for all targets of the whole package
let config = &snap.config.runnables;
match cargo_spec {
Some(spec) => {
for &cmd in ["check", "test"].iter() {
res.push(lsp_ext::Runnable {
label: format!("cargo {} -p {} --all-targets", cmd, spec.package),
location: None,
kind: lsp_ext::RunnableKind::Cargo,
args: lsp_ext::CargoRunnable {
workspace_root: Some(spec.workspace_root.clone().into()),
override_cargo: config.override_cargo.clone(),
cargo_args: vec![
cmd.to_string(),
"--package".to_string(),
spec.package.clone(),
"--all-targets".to_string(),
],
cargo_extra_args: config.cargo_extra_args.clone(),
executable_args: Vec::new(),
expect_test: None,
},
})
}
}
None => {
res.push(lsp_ext::Runnable {
label: "cargo check --workspace".to_string(),
location: None,
kind: lsp_ext::RunnableKind::Cargo,
args: lsp_ext::CargoRunnable {
workspace_root: None,
override_cargo: config.override_cargo.clone(),
cargo_args: vec!["check".to_string(), "--workspace".to_string()],
cargo_extra_args: config.cargo_extra_args.clone(),
executable_args: Vec::new(),
expect_test: None,
},
});
}
}
Ok(res)
}
pub(crate) fn handle_completion(
snap: GlobalStateSnapshot,
params: lsp_types::CompletionParams,
) -> Result<Option<lsp_types::CompletionResponse>> {
let _p = profile::span("handle_completion");
let text_document_position = params.text_document_position.clone();
let position = from_proto::file_position(&snap, params.text_document_position)?;
let completion_triggered_after_single_colon = {
let mut res = false;
if let Some(ctx) = params.context {
if ctx.trigger_character.unwrap_or_default() == ":" {
let source_file = snap.analysis.parse(position.file_id)?;
let syntax = source_file.syntax();
let text = syntax.text();
if let Some(next_char) = text.char_at(position.offset) {
let diff = TextSize::of(next_char) + TextSize::of(':');
let prev_char = position.offset - diff;
if text.char_at(prev_char) != Some(':') {
res = true;
}
}
}
}
res
};
if completion_triggered_after_single_colon {
return Ok(None);
}
let items = match snap.analysis.completions(&snap.config.completion, position)? {
None => return Ok(None),
Some(items) => items,
};
let line_index = snap.analysis.file_line_index(position.file_id)?;
let line_endings = snap.file_line_endings(position.file_id);
let items: Vec<CompletionItem> = items
.into_iter()
.flat_map(|item| {
let mut new_completion_items =
to_proto::completion_item(&line_index, line_endings, item.clone());
if snap.config.completion.resolve_additional_edits_lazily() {
for new_item in &mut new_completion_items {
let _ = fill_resolve_data(&mut new_item.data, &item, &text_document_position)
.take();
}
}
new_completion_items
})
.collect();
let completion_list = lsp_types::CompletionList { is_incomplete: true, items };
Ok(Some(completion_list.into()))
}
pub(crate) fn handle_completion_resolve(
snap: GlobalStateSnapshot,
mut original_completion: CompletionItem,
) -> Result<CompletionItem> {
let _p = profile::span("handle_completion_resolve");
if !all_edits_are_disjoint(&original_completion, &[]) {
return Err(LspError::new(
ErrorCode::InvalidParams as i32,
"Received a completion with overlapping edits, this is not LSP-compliant".into(),
)
.into());
}
// FIXME resolve the other capabilities also?
if !snap
.config
.completion
.active_resolve_capabilities
.contains(&CompletionResolveCapability::AdditionalTextEdits)
{
return Ok(original_completion);
}
let resolve_data = match original_completion
.data
.take()
.map(|data| serde_json::from_value::<CompletionResolveData>(data))
.transpose()?
{
Some(data) => data,
None => return Ok(original_completion),
};
let file_id = from_proto::file_id(&snap, &resolve_data.position.text_document.uri)?;
let line_index = snap.analysis.file_line_index(file_id)?;
let line_endings = snap.file_line_endings(file_id);
let offset = from_proto::offset(&line_index, resolve_data.position.position);
let additional_edits = snap
.analysis
.resolve_completion_edits(
&snap.config.completion,
FilePosition { file_id, offset },
&resolve_data.full_import_path,
resolve_data.imported_name,
)?
.into_iter()
.flat_map(|edit| {
edit.into_iter().map(|indel| to_proto::text_edit(&line_index, line_endings, indel))
})
.collect_vec();
if !all_edits_are_disjoint(&original_completion, &additional_edits) {
return Err(LspError::new(
ErrorCode::InternalError as i32,
"Import edit overlaps with the original completion edits, this is not LSP-compliant"
.into(),
)
.into());
}
if let Some(original_additional_edits) = original_completion.additional_text_edits.as_mut() {
original_additional_edits.extend(additional_edits.into_iter())
} else {
original_completion.additional_text_edits = Some(additional_edits);
}
Ok(original_completion)
}
pub(crate) fn handle_folding_range(
snap: GlobalStateSnapshot,
params: FoldingRangeParams,
) -> Result<Option<Vec<FoldingRange>>> {
let _p = profile::span("handle_folding_range");
let file_id = from_proto::file_id(&snap, &params.text_document.uri)?;
let folds = snap.analysis.folding_ranges(file_id)?;
let text = snap.analysis.file_text(file_id)?;
let line_index = snap.analysis.file_line_index(file_id)?;
let line_folding_only = snap.config.client_caps.line_folding_only;
let res = folds
.into_iter()
.map(|it| to_proto::folding_range(&*text, &line_index, line_folding_only, it))
.collect();
Ok(Some(res))
}
pub(crate) fn handle_signature_help(
snap: GlobalStateSnapshot,
params: lsp_types::SignatureHelpParams,
) -> Result<Option<lsp_types::SignatureHelp>> {
let _p = profile::span("handle_signature_help");
let position = from_proto::file_position(&snap, params.text_document_position_params)?;
let call_info = match snap.analysis.call_info(position)? {
Some(it) => it,
None => return Ok(None),
};
let concise = !snap.config.call_info_full;
let res = to_proto::signature_help(
call_info,
concise,
snap.config.client_caps.signature_help_label_offsets,
);
Ok(Some(res))
}
pub(crate) fn handle_hover(
snap: GlobalStateSnapshot,
params: lsp_types::HoverParams,
) -> Result<Option<lsp_ext::Hover>> {
let _p = profile::span("handle_hover");
let position = from_proto::file_position(&snap, params.text_document_position_params)?;
let info = match snap.analysis.hover(
position,
snap.config.hover.links_in_hover,
snap.config.hover.markdown,
)? {
None => return Ok(None),
Some(info) => info,
};
let line_index = snap.analysis.file_line_index(position.file_id)?;
let range = to_proto::range(&line_index, info.range);
let hover = lsp_ext::Hover {
hover: lsp_types::Hover {
contents: HoverContents::Markup(to_proto::markup_content(info.info.markup)),
range: Some(range),
},
actions: prepare_hover_actions(&snap, position.file_id, &info.info.actions),
};
Ok(Some(hover))
}
pub(crate) fn handle_prepare_rename(
snap: GlobalStateSnapshot,
params: lsp_types::TextDocumentPositionParams,
) -> Result<Option<PrepareRenameResponse>> {
let _p = profile::span("handle_prepare_rename");
let position = from_proto::file_position(&snap, params)?;
let change = snap.analysis.prepare_rename(position)??;
let line_index = snap.analysis.file_line_index(position.file_id)?;
let range = to_proto::range(&line_index, change.range);
Ok(Some(PrepareRenameResponse::Range(range)))
}
pub(crate) fn handle_rename(
snap: GlobalStateSnapshot,
params: RenameParams,
) -> Result<Option<WorkspaceEdit>> {
let _p = profile::span("handle_rename");
let position = from_proto::file_position(&snap, params.text_document_position)?;
if params.new_name.is_empty() {
return Err(LspError::new(
ErrorCode::InvalidParams as i32,
"New Name cannot be empty".into(),
)
.into());
}
let change = snap.analysis.rename(position, &*params.new_name)??;
let workspace_edit = to_proto::workspace_edit(&snap, change.info)?;
Ok(Some(workspace_edit))
}
pub(crate) fn handle_references(
snap: GlobalStateSnapshot,
params: lsp_types::ReferenceParams,
) -> Result<Option<Vec<Location>>> {
let _p = profile::span("handle_references");
let position = from_proto::file_position(&snap, params.text_document_position)?;
let refs = match snap.analysis.find_all_refs(position, None)? {
None => return Ok(None),
Some(refs) => refs,
};
let locations = if params.context.include_declaration {
refs.into_iter()
.filter_map(|reference| to_proto::location(&snap, reference.file_range).ok())
.collect()
} else {
// Only iterate over the references if include_declaration was false
refs.references()
.iter()
.filter_map(|reference| to_proto::location(&snap, reference.file_range).ok())
.collect()
};
Ok(Some(locations))
}
pub(crate) fn handle_formatting(
snap: GlobalStateSnapshot,
params: DocumentFormattingParams,
) -> Result<Option<Vec<lsp_types::TextEdit>>> {
let _p = profile::span("handle_formatting");
let file_id = from_proto::file_id(&snap, &params.text_document.uri)?;
let file = snap.analysis.file_text(file_id)?;
let crate_ids = snap.analysis.crate_for(file_id)?;
let file_line_index = snap.analysis.file_line_index(file_id)?;
let file_line_endings = snap.file_line_endings(file_id);
let mut rustfmt = match &snap.config.rustfmt {
RustfmtConfig::Rustfmt { extra_args } => {
let mut cmd = process::Command::new(toolchain::rustfmt());
cmd.args(extra_args);
if let Some(&crate_id) = crate_ids.first() {
// Assume all crates are in the same edition
let edition = snap.analysis.crate_edition(crate_id)?;
cmd.arg("--edition");
cmd.arg(edition.to_string());
}
cmd
}
RustfmtConfig::CustomCommand { command, args } => {
let mut cmd = process::Command::new(command);
cmd.args(args);
cmd
}
};
let mut rustfmt =
rustfmt.stdin(Stdio::piped()).stdout(Stdio::piped()).stderr(Stdio::piped()).spawn()?;
rustfmt.stdin.as_mut().unwrap().write_all(file.as_bytes())?;
let output = rustfmt.wait_with_output()?;
let captured_stdout = String::from_utf8(output.stdout)?;
let captured_stderr = String::from_utf8(output.stderr).unwrap_or_default();
if !output.status.success() {
match output.status.code() {
Some(1) if !captured_stderr.contains("not installed") => {
// While `rustfmt` doesn't have a specific exit code for parse errors this is the
// likely cause exiting with 1. Most Language Servers swallow parse errors on
// formatting because otherwise an error is surfaced to the user on top of the
// syntax error diagnostics they're already receiving. This is especially jarring
// if they have format on save enabled.
log::info!("rustfmt exited with status 1, assuming parse error and ignoring");
return Ok(None);
}
_ => {
// Something else happened - e.g. `rustfmt` is missing or caught a signal
return Err(LspError::new(
-32900,
format!(
r#"rustfmt exited with:
Status: {}
stdout: {}
stderr: {}"#,
output.status, captured_stdout, captured_stderr,
),
)
.into());
}
}
}
if *file == captured_stdout {
// The document is already formatted correctly -- no edits needed.
Ok(None)
} else {
Ok(Some(to_proto::text_edit_vec(
&file_line_index,
file_line_endings,
diff(&file, &captured_stdout),
)))
}
}
pub(crate) fn handle_code_action(
snap: GlobalStateSnapshot,
params: lsp_types::CodeActionParams,
) -> Result<Option<Vec<lsp_ext::CodeAction>>> {
let _p = profile::span("handle_code_action");
// We intentionally don't support command-based actions, as those either
// requires custom client-code anyway, or requires server-initiated edits.
// Server initiated edits break causality, so we avoid those as well.
if !snap.config.client_caps.code_action_literals {
return Ok(None);
}
let file_id = from_proto::file_id(&snap, &params.text_document.uri)?;
let line_index = snap.analysis.file_line_index(file_id)?;
let range = from_proto::text_range(&line_index, params.range);
let frange = FileRange { file_id, range };
let assists_config = AssistConfig {
allowed: params
.clone()
.context
.only
.map(|it| it.into_iter().filter_map(from_proto::assist_kind).collect()),
..snap.config.assist
};
let mut res: Vec<lsp_ext::CodeAction> = Vec::new();
let include_quick_fixes = match &params.context.only {
Some(v) => v.iter().any(|it| {
it == &lsp_types::CodeActionKind::EMPTY || it == &lsp_types::CodeActionKind::QUICKFIX
}),
None => true,
};
if include_quick_fixes {
add_quick_fixes(&snap, frange, &line_index, &mut res)?;
}
if snap.config.client_caps.code_action_resolve {
for (index, assist) in
snap.analysis.assists(&assists_config, false, frange)?.into_iter().enumerate()
{
res.push(to_proto::unresolved_code_action(&snap, params.clone(), assist, index)?);
}
} else {
for assist in snap.analysis.assists(&assists_config, true, frange)?.into_iter() {
res.push(to_proto::resolved_code_action(&snap, assist)?);
}
}
Ok(Some(res))
}
fn add_quick_fixes(
snap: &GlobalStateSnapshot,
frange: FileRange,
line_index: &Arc<LineIndex>,
acc: &mut Vec<lsp_ext::CodeAction>,
) -> Result<()> {
let diagnostics = snap.analysis.diagnostics(&snap.config.diagnostics, frange.file_id)?;
for fix in diagnostics
.into_iter()
.filter_map(|d| d.fix)
.filter(|fix| fix.fix_trigger_range.intersect(frange.range).is_some())
{
let edit = to_proto::snippet_workspace_edit(&snap, fix.source_change)?;
let action = lsp_ext::CodeAction {
title: fix.label.to_string(),
group: None,
kind: Some(CodeActionKind::QUICKFIX),
edit: Some(edit),
is_preferred: Some(false),
data: None,
};
acc.push(action);
}
for fix in snap.check_fixes.get(&frange.file_id).into_iter().flatten() {
let fix_range = from_proto::text_range(&line_index, fix.range);
if fix_range.intersect(frange.range).is_some() {
acc.push(fix.action.clone());
}
}
Ok(())
}
pub(crate) fn handle_code_action_resolve(
mut snap: GlobalStateSnapshot,
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);
let frange = FileRange { file_id, range };
snap.config.assist.allowed = params
.code_action_params
.context
.only
.map(|it| it.into_iter().filter_map(from_proto::assist_kind).collect());
let assists = snap.analysis.assists(&snap.config.assist, true, frange)?;
let (id, index) = split_once(&params.id, ':').unwrap();
let index = index.parse::<usize>().unwrap();
let assist = &assists[index];
assert!(assist.id.0 == id);
let edit = to_proto::resolved_code_action(&snap, assist.clone())?.edit;
code_action.edit = edit;
Ok(code_action)
}
pub(crate) fn handle_code_lens(
snap: GlobalStateSnapshot,
params: lsp_types::CodeLensParams,
) -> Result<Option<Vec<CodeLens>>> {
let _p = profile::span("handle_code_lens");
let mut lenses: Vec<CodeLens> = Default::default();
if snap.config.lens.none() {
// early return before any db query!
return Ok(Some(lenses));
}
let file_id = from_proto::file_id(&snap, &params.text_document.uri)?;
let line_index = snap.analysis.file_line_index(file_id)?;
let cargo_spec = CargoTargetSpec::for_file(&snap, file_id)?;
if snap.config.lens.runnable() {
// Gather runnables
for runnable in snap.analysis.runnables(file_id)? {
if should_skip_target(&runnable, cargo_spec.as_ref()) {
continue;
}
let action = runnable.action();
let range = to_proto::range(&line_index, runnable.nav.full_range);
let r = to_proto::runnable(&snap, file_id, runnable)?;
if snap.config.lens.run {
let lens = CodeLens {
range,
command: Some(run_single_command(&r, action.run_title)),
data: None,
};
lenses.push(lens);
}
if action.debugee && snap.config.lens.debug {
let debug_lens =
CodeLens { range, command: Some(debug_single_command(&r)), data: None };
lenses.push(debug_lens);
}
}
}
if snap.config.lens.implementations {
// Handle impls
lenses.extend(
snap.analysis
.file_structure(file_id)?
.into_iter()
.filter(|it| {
matches!(
it.kind,
SymbolKind::Trait
| SymbolKind::Struct
| SymbolKind::Enum
| SymbolKind::Union
)
})
.map(|it| {
let range = to_proto::range(&line_index, it.node_range);
let pos = range.start;
let lens_params = lsp_types::request::GotoImplementationParams {
text_document_position_params: lsp_types::TextDocumentPositionParams::new(
params.text_document.clone(),
pos,
),
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
};
CodeLens {
range,
command: None,
data: Some(to_value(CodeLensResolveData::Impls(lens_params)).unwrap()),
}
}),
);
}
if snap.config.lens.references() {
lenses.extend(snap.analysis.find_all_methods(file_id)?.into_iter().map(|it| {
let range = to_proto::range(&line_index, it.range);
let position = to_proto::position(&line_index, it.range.start());
let lens_params =
lsp_types::TextDocumentPositionParams::new(params.text_document.clone(), position);
CodeLens {
range,
command: None,
data: Some(to_value(CodeLensResolveData::References(lens_params)).unwrap()),
}
}));
}
Ok(Some(lenses))
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
enum CodeLensResolveData {
Impls(lsp_types::request::GotoImplementationParams),
References(lsp_types::TextDocumentPositionParams),
}
pub(crate) fn handle_code_lens_resolve(
snap: GlobalStateSnapshot,
code_lens: CodeLens,
) -> Result<CodeLens> {
let _p = profile::span("handle_code_lens_resolve");
let data = code_lens.data.unwrap();
let resolve = from_json::<Option<CodeLensResolveData>>("CodeLensResolveData", data)?;
match resolve {
Some(CodeLensResolveData::Impls(lens_params)) => {
let locations: Vec<Location> =
match handle_goto_implementation(snap, lens_params.clone())? {
Some(lsp_types::GotoDefinitionResponse::Scalar(loc)) => vec![loc],
Some(lsp_types::GotoDefinitionResponse::Array(locs)) => locs,
Some(lsp_types::GotoDefinitionResponse::Link(links)) => links
.into_iter()
.map(|link| Location::new(link.target_uri, link.target_selection_range))
.collect(),
_ => vec![],
};
let title = implementation_title(locations.len());
let cmd = show_references_command(
title,
&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 })
}
Some(CodeLensResolveData::References(doc_position)) => {
let position = from_proto::file_position(&snap, doc_position.clone())?;
let locations = snap
.analysis
.find_all_refs(position, None)
.unwrap_or(None)
.map(|r| {
r.references()
.iter()
.filter_map(|it| to_proto::location(&snap, it.file_range).ok())
.collect_vec()
})
.unwrap_or_default();
let title = reference_title(locations.len());
let cmd = if locations.is_empty() {
Command { title, command: "".into(), arguments: None }
} else {
show_references_command(
title,
&doc_position.text_document.uri,
code_lens.range.start,
locations,
)
};
Ok(CodeLens { range: code_lens.range, command: Some(cmd), data: None })
}
None => Ok(CodeLens {
range: code_lens.range,
command: Some(Command { title: "Error".into(), ..Default::default() }),
data: None,
}),
}
}
pub(crate) fn handle_document_highlight(
snap: GlobalStateSnapshot,
params: lsp_types::DocumentHighlightParams,
) -> Result<Option<Vec<DocumentHighlight>>> {
let _p = profile::span("handle_document_highlight");
let position = from_proto::file_position(&snap, params.text_document_position_params)?;
let line_index = snap.analysis.file_line_index(position.file_id)?;
let refs = match snap
.analysis
.find_all_refs(position, Some(SearchScope::single_file(position.file_id)))?
{
None => return Ok(None),
Some(refs) => refs,
};
let res = refs
.into_iter()
.filter(|reference| reference.file_range.file_id == position.file_id)
.map(|reference| DocumentHighlight {
range: to_proto::range(&line_index, reference.file_range.range),
kind: reference.access.map(to_proto::document_highlight_kind),
})
.collect();
Ok(Some(res))
}
pub(crate) fn handle_ssr(
snap: GlobalStateSnapshot,
params: lsp_ext::SsrParams,
) -> Result<lsp_types::WorkspaceEdit> {
let _p = profile::span("handle_ssr");
let selections = params
.selections
.iter()
.map(|range| from_proto::file_range(&snap, params.position.text_document.clone(), *range))
.collect::<Result<Vec<_>, _>>()?;
let position = from_proto::file_position(&snap, params.position)?;
let source_change = snap.analysis.structural_search_replace(
&params.query,
params.parse_only,
position,
selections,
)??;
to_proto::workspace_edit(&snap, source_change)
}
pub(crate) fn publish_diagnostics(
snap: &GlobalStateSnapshot,
file_id: FileId,
) -> Result<Vec<Diagnostic>> {
let _p = profile::span("publish_diagnostics");
let line_index = snap.analysis.file_line_index(file_id)?;
let diagnostics: Vec<Diagnostic> = snap
.analysis
.diagnostics(&snap.config.diagnostics, file_id)?
.into_iter()
.map(|d| Diagnostic {
range: to_proto::range(&line_index, d.range),
severity: Some(to_proto::diagnostic_severity(d.severity)),
code: d.code.map(|d| d.as_str().to_owned()).map(NumberOrString::String),
code_description: d.code.and_then(|code| {
lsp_types::Url::parse(&format!(
"https://rust-analyzer.github.io/manual.html#{}",
code.as_str()
))
.ok()
.map(|href| lsp_types::CodeDescription { href })
}),
source: Some("rust-analyzer".to_string()),
message: d.message,
related_information: None,
tags: if d.unused { Some(vec![DiagnosticTag::Unnecessary]) } else { None },
data: None,
})
.collect();
Ok(diagnostics)
}
pub(crate) fn handle_inlay_hints(
snap: GlobalStateSnapshot,
params: InlayHintsParams,
) -> Result<Vec<InlayHint>> {
let _p = profile::span("handle_inlay_hints");
let file_id = from_proto::file_id(&snap, &params.text_document.uri)?;
let line_index = snap.analysis.file_line_index(file_id)?;
Ok(snap
.analysis
.inlay_hints(file_id, &snap.config.inlay_hints)?
.into_iter()
.map(|it| to_proto::inlay_hint(&line_index, it))
.collect())
}
pub(crate) fn handle_call_hierarchy_prepare(
snap: GlobalStateSnapshot,
params: CallHierarchyPrepareParams,
) -> Result<Option<Vec<CallHierarchyItem>>> {
let _p = profile::span("handle_call_hierarchy_prepare");
let position = from_proto::file_position(&snap, params.text_document_position_params)?;
let nav_info = match snap.analysis.call_hierarchy(position)? {
None => return Ok(None),
Some(it) => it,
};
let RangeInfo { range: _, info: navs } = nav_info;
let res = navs
.into_iter()
.filter(|it| it.kind == Some(SymbolKind::Function))
.map(|it| to_proto::call_hierarchy_item(&snap, it))
.collect::<Result<Vec<_>>>()?;
Ok(Some(res))
}
pub(crate) fn handle_call_hierarchy_incoming(
snap: GlobalStateSnapshot,
params: CallHierarchyIncomingCallsParams,
) -> Result<Option<Vec<CallHierarchyIncomingCall>>> {
let _p = profile::span("handle_call_hierarchy_incoming");
let item = params.item;
let doc = TextDocumentIdentifier::new(item.uri);
let frange = from_proto::file_range(&snap, doc, item.selection_range)?;
let fpos = FilePosition { file_id: frange.file_id, offset: frange.range.start() };
let call_items = match snap.analysis.incoming_calls(fpos)? {
None => return Ok(None),
Some(it) => it,
};
let mut res = vec![];
for call_item in call_items.into_iter() {
let file_id = call_item.target.file_id;
let line_index = snap.analysis.file_line_index(file_id)?;
let item = to_proto::call_hierarchy_item(&snap, call_item.target)?;
res.push(CallHierarchyIncomingCall {
from: item,
from_ranges: call_item
.ranges
.into_iter()
.map(|it| to_proto::range(&line_index, it))
.collect(),
});
}
Ok(Some(res))
}
pub(crate) fn handle_call_hierarchy_outgoing(
snap: GlobalStateSnapshot,
params: CallHierarchyOutgoingCallsParams,
) -> Result<Option<Vec<CallHierarchyOutgoingCall>>> {
let _p = profile::span("handle_call_hierarchy_outgoing");
let item = params.item;
let doc = TextDocumentIdentifier::new(item.uri);
let frange = from_proto::file_range(&snap, doc, item.selection_range)?;
let fpos = FilePosition { file_id: frange.file_id, offset: frange.range.start() };
let call_items = match snap.analysis.outgoing_calls(fpos)? {
None => return Ok(None),
Some(it) => it,
};
let mut res = vec![];
for call_item in call_items.into_iter() {
let file_id = call_item.target.file_id;
let line_index = snap.analysis.file_line_index(file_id)?;
let item = to_proto::call_hierarchy_item(&snap, call_item.target)?;
res.push(CallHierarchyOutgoingCall {
to: item,
from_ranges: call_item
.ranges
.into_iter()
.map(|it| to_proto::range(&line_index, it))
.collect(),
});
}
Ok(Some(res))
}
pub(crate) fn handle_semantic_tokens_full(
snap: GlobalStateSnapshot,
params: SemanticTokensParams,
) -> Result<Option<SemanticTokensResult>> {
let _p = profile::span("handle_semantic_tokens_full");
let file_id = from_proto::file_id(&snap, &params.text_document.uri)?;
let text = snap.analysis.file_text(file_id)?;
let line_index = snap.analysis.file_line_index(file_id)?;
let highlights = snap.analysis.highlight(file_id)?;
let semantic_tokens = to_proto::semantic_tokens(&text, &line_index, highlights);
// Unconditionally cache the tokens
snap.semantic_tokens_cache.lock().insert(params.text_document.uri, semantic_tokens.clone());
Ok(Some(semantic_tokens.into()))
}
pub(crate) fn handle_semantic_tokens_full_delta(
snap: GlobalStateSnapshot,
params: SemanticTokensDeltaParams,
) -> Result<Option<SemanticTokensFullDeltaResult>> {
let _p = profile::span("handle_semantic_tokens_full_delta");
let file_id = from_proto::file_id(&snap, &params.text_document.uri)?;
let text = snap.analysis.file_text(file_id)?;
let line_index = snap.analysis.file_line_index(file_id)?;
let highlights = snap.analysis.highlight(file_id)?;
let semantic_tokens = to_proto::semantic_tokens(&text, &line_index, highlights);
let mut cache = snap.semantic_tokens_cache.lock();
let cached_tokens = cache.entry(params.text_document.uri).or_default();
if let Some(prev_id) = &cached_tokens.result_id {
if *prev_id == params.previous_result_id {
let delta = to_proto::semantic_token_delta(&cached_tokens, &semantic_tokens);
*cached_tokens = semantic_tokens;
return Ok(Some(delta.into()));
}
}
*cached_tokens = semantic_tokens.clone();
Ok(Some(semantic_tokens.into()))
}
pub(crate) fn handle_semantic_tokens_range(
snap: GlobalStateSnapshot,
params: SemanticTokensRangeParams,
) -> Result<Option<SemanticTokensRangeResult>> {
let _p = profile::span("handle_semantic_tokens_range");
let frange = from_proto::file_range(&snap, params.text_document, params.range)?;
let text = snap.analysis.file_text(frange.file_id)?;
let line_index = snap.analysis.file_line_index(frange.file_id)?;
let highlights = snap.analysis.highlight_range(frange)?;
let semantic_tokens = to_proto::semantic_tokens(&text, &line_index, highlights);
Ok(Some(semantic_tokens.into()))
}
pub(crate) fn handle_open_docs(
snap: GlobalStateSnapshot,
params: lsp_types::TextDocumentPositionParams,
) -> Result<Option<lsp_types::Url>> {
let _p = profile::span("handle_open_docs");
let position = from_proto::file_position(&snap, params)?;
let remote = snap.analysis.external_docs(position)?;
Ok(remote.and_then(|remote| Url::parse(&remote).ok()))
}
pub(crate) fn handle_open_cargo_toml(
snap: GlobalStateSnapshot,
params: lsp_ext::OpenCargoTomlParams,
) -> Result<Option<lsp_types::GotoDefinitionResponse>> {
let _p = profile::span("handle_open_cargo_toml");
let file_id = from_proto::file_id(&snap, &params.text_document.uri)?;
let cargo_spec = match CargoTargetSpec::for_file(&snap, file_id)? {
Some(it) => it,
None => return Ok(None),
};
let cargo_toml_url = to_proto::url_from_abs_path(&cargo_spec.cargo_toml);
let res: lsp_types::GotoDefinitionResponse =
Location::new(cargo_toml_url, Range::default()).into();
Ok(Some(res))
}
fn implementation_title(count: usize) -> String {
if count == 1 {
"1 implementation".into()
} else {
format!("{} implementations", count)
}
}
fn reference_title(count: usize) -> String {
if count == 1 {
"1 reference".into()
} else {
format!("{} references", 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 run_single_command(runnable: &lsp_ext::Runnable, title: &str) -> Command {
Command {
title: title.to_string(),
command: "rust-analyzer.runSingle".into(),
arguments: Some(vec![to_value(runnable).unwrap()]),
}
}
fn debug_single_command(runnable: &lsp_ext::Runnable) -> Command {
Command {
title: "Debug".into(),
command: "rust-analyzer.debugSingle".into(),
arguments: Some(vec![to_value(runnable).unwrap()]),
}
}
fn goto_location_command(snap: &GlobalStateSnapshot, nav: &NavigationTarget) -> Option<Command> {
let value = if snap.config.client_caps.location_link {
let link = to_proto::location_link(snap, None, nav.clone()).ok()?;
to_value(link).ok()?
} else {
let range = FileRange { file_id: nav.file_id, range: nav.focus_or_full_range() };
let location = to_proto::location(snap, range).ok()?;
to_value(location).ok()?
};
Some(Command {
title: nav.name.to_string(),
command: "rust-analyzer.gotoLocation".into(),
arguments: Some(vec![value]),
})
}
fn to_command_link(command: Command, tooltip: String) -> lsp_ext::CommandLink {
lsp_ext::CommandLink { tooltip: Some(tooltip), command }
}
fn show_impl_command_link(
snap: &GlobalStateSnapshot,
position: &FilePosition,
) -> Option<lsp_ext::CommandLinkGroup> {
if snap.config.hover.implementations {
if let Some(nav_data) = snap.analysis.goto_implementation(*position).unwrap_or(None) {
let uri = to_proto::url(snap, position.file_id);
let line_index = snap.analysis.file_line_index(position.file_id).ok()?;
let position = to_proto::position(&line_index, position.offset);
let locations: Vec<_> = nav_data
.info
.into_iter()
.filter_map(|nav| to_proto::location_from_nav(snap, nav).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 runnable_action_links(
snap: &GlobalStateSnapshot,
file_id: FileId,
runnable: Runnable,
) -> Option<lsp_ext::CommandLinkGroup> {
let cargo_spec = CargoTargetSpec::for_file(&snap, file_id).ok()?;
if !snap.config.hover.runnable() || should_skip_target(&runnable, cargo_spec.as_ref()) {
return None;
}
let action: &'static _ = runnable.action();
to_proto::runnable(snap, file_id, runnable).ok().map(|r| {
let mut group = lsp_ext::CommandLinkGroup::default();
if snap.config.hover.run {
let run_command = run_single_command(&r, action.run_title);
group.commands.push(to_command_link(run_command, r.label.clone()));
}
if snap.config.hover.debug {
let dbg_command = debug_single_command(&r);
group.commands.push(to_command_link(dbg_command, r.label));
}
group
})
}
fn goto_type_action_links(
snap: &GlobalStateSnapshot,
nav_targets: &[HoverGotoTypeData],
) -> Option<lsp_ext::CommandLinkGroup> {
if !snap.config.hover.goto_type_def || nav_targets.is_empty() {
return None;
}
Some(lsp_ext::CommandLinkGroup {
title: Some("Go to ".into()),
commands: nav_targets
.iter()
.filter_map(|it| {
goto_location_command(snap, &it.nav)
.map(|cmd| to_command_link(cmd, it.mod_path.clone()))
})
.collect(),
})
}
fn prepare_hover_actions(
snap: &GlobalStateSnapshot,
file_id: FileId,
actions: &[HoverAction],
) -> Vec<lsp_ext::CommandLinkGroup> {
if snap.config.hover.none() || !snap.config.client_caps.hover_actions {
return Vec::new();
}
actions
.iter()
.filter_map(|it| match it {
HoverAction::Implementaion(position) => show_impl_command_link(snap, position),
HoverAction::Runnable(r) => runnable_action_links(snap, file_id, r.clone()),
HoverAction::GoToType(targets) => goto_type_action_links(snap, targets),
})
.collect()
}
fn should_skip_target(runnable: &Runnable, cargo_spec: Option<&CargoTargetSpec>) -> bool {
match runnable.kind {
RunnableKind::Bin => {
// Do not suggest binary run on other target than binary
match &cargo_spec {
Some(spec) => !matches!(
spec.target_kind,
TargetKind::Bin | TargetKind::Example | TargetKind::Test
),
None => true,
}
}
_ => false,
}
}
#[derive(Debug, Serialize, Deserialize)]
struct CompletionResolveData {
position: lsp_types::TextDocumentPositionParams,
full_import_path: String,
imported_name: String,
}
fn fill_resolve_data(
resolve_data: &mut Option<serde_json::Value>,
item: &ide::CompletionItem,
position: &TextDocumentPositionParams,
) -> Option<()> {
let import_edit = item.import_to_add()?;
let full_import_path = import_edit.import_path.to_string();
let imported_name = import_edit.import_path.segments.clone().pop()?.to_string();
*resolve_data = Some(
to_value(CompletionResolveData {
position: position.to_owned(),
full_import_path,
imported_name,
})
.unwrap(),
);
Some(())
}