Unnamed repository; edit this file 'description' to name the repository.
Auto merge of #14710 - jhgg:chore/refactor-notification-handlers, r=Veykril
chore: rust-analyzer: refactor notification handlers Fixes the FIXME in `on_notification`. ```rust // FIXME: Move these implementations out into a module similar to on_request ``` No code has changed, this just moves stuff around.
bors 2023-05-02
parent 9c0c13e · parent b9007a2 · commit 98e76bd
-rw-r--r--crates/rust-analyzer/src/handlers.rs1890
-rw-r--r--crates/rust-analyzer/src/handlers/notification.rs339
-rw-r--r--crates/rust-analyzer/src/handlers/request.rs1883
-rw-r--r--crates/rust-analyzer/src/main_loop.rs303
4 files changed, 2248 insertions, 2167 deletions
diff --git a/crates/rust-analyzer/src/handlers.rs b/crates/rust-analyzer/src/handlers.rs
index a5b9004d80..a00d0fba7c 100644
--- a/crates/rust-analyzer/src/handlers.rs
+++ b/crates/rust-analyzer/src/handlers.rs
@@ -2,1336 +2,14 @@
//! 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::AssistResolveStrategy;
+use lsp_types::{Diagnostic, DiagnosticTag, NumberOrString};
+use vfs::FileId;
-use anyhow::Context;
-use ide::{
- AnnotationConfig, AssistKind, AssistResolveStrategy, Cancellable, FileId, FilePosition,
- FileRange, HoverAction, HoverGotoTypeData, Query, RangeInfo, ReferenceCategory, Runnable,
- RunnableKind, SingleResolve, SourceChange, TextEdit,
-};
-use ide_db::SymbolKind;
-use lsp_server::ErrorCode;
-use lsp_types::{
- CallHierarchyIncomingCall, CallHierarchyIncomingCallsParams, CallHierarchyItem,
- CallHierarchyOutgoingCall, CallHierarchyOutgoingCallsParams, CallHierarchyPrepareParams,
- CodeLens, CompletionItem, Diagnostic, DiagnosticTag, DocumentFormattingParams, FoldingRange,
- FoldingRangeParams, HoverContents, InlayHint, InlayHintParams, Location, LocationLink,
- NumberOrString, Position, PrepareRenameResponse, Range, RenameParams,
- SemanticTokensDeltaParams, SemanticTokensFullDeltaResult, SemanticTokensParams,
- SemanticTokensRangeParams, SemanticTokensRangeResult, SemanticTokensResult, SymbolInformation,
- SymbolTag, TextDocumentIdentifier, Url, WorkspaceEdit,
-};
-use project_model::{ManifestPath, ProjectWorkspace, TargetKind};
-use serde_json::json;
-use stdx::{format_to, never};
-use syntax::{algo, ast, AstNode, TextRange, TextSize};
-use vfs::{AbsPath, AbsPathBuf};
+use crate::{global_state::GlobalStateSnapshot, to_proto, Result};
-use crate::{
- cargo_target_spec::CargoTargetSpec,
- config::{RustfmtConfig, WorkspaceSymbolConfig},
- diff::diff,
- from_proto,
- global_state::{GlobalState, GlobalStateSnapshot},
- line_index::LineEndings,
- lsp_ext::{self, PositionOrRange, ViewCrateGraphParams, WorkspaceSymbolParams},
- lsp_utils::{all_edits_are_disjoint, invalid_params_error},
- to_proto, LspError, Result,
-};
-
-pub(crate) fn handle_workspace_reload(state: &mut GlobalState, _: ()) -> Result<()> {
- state.proc_macro_clients = Arc::new([]);
- state.proc_macro_changed = false;
-
- state.fetch_workspaces_queue.request_op("reload workspace request".to_string(), ());
- Ok(())
-}
-
-pub(crate) fn handle_proc_macros_rebuild(state: &mut GlobalState, _: ()) -> Result<()> {
- state.proc_macro_clients = Arc::new([]);
- state.proc_macro_changed = false;
-
- state.fetch_build_data_queue.request_op("rebuild proc macros request".to_string(), ());
- Ok(())
-}
-
-pub(crate) fn handle_cancel_flycheck(state: &mut GlobalState, _: ()) -> Result<()> {
- let _p = profile::span("handle_stop_flycheck");
- state.flycheck.iter().for_each(|flycheck| flycheck.cancel());
- Ok(())
-}
-
-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");
- format_to!(
- buf,
- "Loaded {:?} packages across {} workspace{}.\n",
- snap.workspaces.iter().map(|w| w.n_packages()).sum::<usize>(),
- snap.workspaces.len(),
- if snap.workspaces.len() == 1 { "" } else { "s" }
- );
-
- format_to!(
- buf,
- "Workspace root folders: {:?}",
- snap.workspaces
- .iter()
- .flat_map(|ws| ws.workspace_definition_path())
- .collect::<Vec<&AbsPath>>()
- );
- }
- format_to!(buf, "\nVfs memory usage: {}\n", snap.vfs_memory_usage());
- buf.push_str("\nAnalysis:\n");
- buf.push_str(
- &snap
- .analysis
- .status(file_id)
- .unwrap_or_else(|_| "Analysis retrieval was cancelled".to_owned()),
- );
- Ok(buf)
-}
-
-pub(crate) fn handle_memory_usage(state: &mut GlobalState, _: ()) -> Result<String> {
- let _p = profile::span("handle_memory_usage");
- let mut mem = state.analysis_host.per_query_memory_usage();
- mem.push(("Remaining".into(), profile::memory_usage().allocated));
-
- let mut out = String::new();
- for (name, bytes) in mem {
- format_to!(out, "{:>8} {}\n", bytes, name);
- }
- Ok(out)
-}
-
-pub(crate) fn handle_shuffle_crate_graph(state: &mut GlobalState, _: ()) -> Result<()> {
- state.analysis_host.shuffle_crate_graph();
- Ok(())
-}
-
-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.file_line_index(id)?;
- let text_range = params.range.and_then(|r| from_proto::text_range(&line_index, r).ok());
- 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_view_mir(
- snap: GlobalStateSnapshot,
- params: lsp_types::TextDocumentPositionParams,
-) -> Result<String> {
- let _p = profile::span("handle_view_mir");
- let position = from_proto::file_position(&snap, params)?;
- let res = snap.analysis.view_mir(position)?;
- Ok(res)
-}
-
-pub(crate) fn handle_interpret_function(
- snap: GlobalStateSnapshot,
- params: lsp_types::TextDocumentPositionParams,
-) -> Result<String> {
- let _p = profile::span("handle_interpret_function");
- let position = from_proto::file_position(&snap, params)?;
- let res = snap.analysis.interpret_function(position)?;
- Ok(res)
-}
-
-pub(crate) fn handle_view_file_text(
- snap: GlobalStateSnapshot,
- params: lsp_types::TextDocumentIdentifier,
-) -> Result<String> {
- let file_id = from_proto::file_id(&snap, &params.uri)?;
- Ok(snap.analysis.file_text(file_id)?.to_string())
-}
-
-pub(crate) fn handle_view_item_tree(
- snap: GlobalStateSnapshot,
- params: lsp_ext::ViewItemTreeParams,
-) -> Result<String> {
- let _p = profile::span("handle_view_item_tree");
- let file_id = from_proto::file_id(&snap, &params.text_document.uri)?;
- let res = snap.analysis.view_item_tree(file_id)?;
- Ok(res)
-}
-
-pub(crate) fn handle_view_crate_graph(
- snap: GlobalStateSnapshot,
- params: ViewCrateGraphParams,
-) -> Result<String> {
- let _p = profile::span("handle_view_crate_graph");
- let dot = snap.analysis.view_crate_graph(params.full)??;
- Ok(dot)
-}
-
-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.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.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.file_line_index(file_id)?;
- params
- .positions
- .into_iter()
- .map(|position| {
- let offset = from_proto::offset(&line_index, position);
- offset.map(|offset| {
- 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()
-}
-
-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 config = snap.config.join_lines();
- let file_id = from_proto::file_id(&snap, &params.text_document.uri)?;
- let line_index = snap.file_line_index(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(&config, FileRange { file_id, range })?;
- match res.union(edit) {
- Ok(()) => (),
- Err(_edit) => {
- // just ignore overlapping edits
- }
- }
- }
-
- Ok(to_proto::text_edit_vec(&line_index, 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.file_line_index(position.file_id)?;
- let edit = to_proto::snippet_text_edit_vec(&line_index, true, edit);
- Ok(Some(edit))
-}
-
-pub(crate) fn handle_on_type_formatting(
- snap: GlobalStateSnapshot,
- params: lsp_types::DocumentOnTypeFormattingParams,
-) -> Result<Option<Vec<lsp_ext::SnippetTextEdit>>> {
- let _p = profile::span("handle_on_type_formatting");
- let mut position = from_proto::file_position(&snap, params.text_document_position)?;
- let line_index = snap.file_line_index(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');
-
- let text = snap.analysis.file_text(position.file_id)?;
- if stdx::never!(!text[usize::from(position.offset)..].starts_with(char_typed)) {
- return Ok(None);
- }
-
- // 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, snap.config.typing_autoclose_angle())?;
- let edit = match edit {
- Some(it) => it,
- None => return Ok(None),
- };
-
- // This should be a single-file edit
- let (_, text_edit) = edit.source_file_edits.into_iter().next().unwrap();
-
- let change = to_proto::snippet_text_edit_vec(&line_index, edit.is_snippet, text_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.file_line_index(file_id)?;
-
- let mut parents: Vec<(lsp_types::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 = lsp_types::DocumentSymbol {
- name: symbol.label,
- detail: symbol.detail,
- kind: to_proto::structure_node_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.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: &lsp_types::DocumentSymbol,
- container_name: Option<String>,
- url: &Url,
- res: &mut Vec<SymbolInformation>,
- ) {
- let mut tags = Vec::new();
-
- #[allow(deprecated)]
- if let Some(true) = symbol.deprecated {
- 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: WorkspaceSymbolParams,
-) -> Result<Option<Vec<SymbolInformation>>> {
- let _p = profile::span("handle_workspace_symbol");
-
- let config = snap.config.workspace_symbol();
- let (all_symbols, libs) = decide_search_scope_and_kind(&params, &config);
- let limit = config.search_limit;
-
- 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(limit);
- q
- };
- let mut res = exec_query(&snap, query)?;
- if res.is_empty() && !all_symbols {
- let mut query = Query::new(params.query);
- query.limit(limit);
- res = exec_query(&snap, query)?;
- }
-
- return Ok(Some(res));
-
- fn decide_search_scope_and_kind(
- params: &WorkspaceSymbolParams,
- config: &WorkspaceSymbolConfig,
- ) -> (bool, bool) {
- // Support old-style parsing of markers in the query.
- let mut all_symbols = params.query.contains('#');
- let mut libs = params.query.contains('*');
-
- // If no explicit marker was set, check request params. If that's also empty
- // use global config.
- if !all_symbols {
- let search_kind = match params.search_kind {
- Some(ref search_kind) => search_kind,
- None => &config.search_kind,
- };
- all_symbols = match search_kind {
- lsp_ext::WorkspaceSymbolSearchKind::OnlyTypes => false,
- lsp_ext::WorkspaceSymbolSearchKind::AllSymbols => true,
- }
- }
-
- if !libs {
- let search_scope = match params.search_scope {
- Some(ref search_scope) => search_scope,
- None => &config.search_scope,
- };
- libs = match search_scope {
- lsp_ext::WorkspaceSymbolSearchScope::Workspace => false,
- lsp_ext::WorkspaceSymbolSearchScope::WorkspaceAndDependencies => true,
- }
- }
-
- (all_symbols, libs)
- }
-
- 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 => {
- if from_path.is_dir() {
- // add '/' to end of url -- from `file://path/to/folder` to `file://path/to/folder/`
- let mut old_folder_name = from_path.file_stem()?.to_str()?.to_string();
- old_folder_name.push('/');
- let from_with_trailing_slash = from.join(&old_folder_name).ok()?;
-
- let imitate_from_url = from_with_trailing_slash.join("mod.rs").ok()?;
- let new_file_name = to_path.file_name()?.to_str()?;
- Some((
- snap.url_to_file_id(&imitate_from_url).ok()?,
- new_file_name.to_string(),
- ))
- } else {
- let old_name = from_path.file_stem()?.to_str()?;
- let new_name = to_path.file_stem()?.to_str()?;
- match (old_name, new_name) {
- ("mod", _) => None,
- (_, "mod") => None,
- _ => 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 mut source_changes = source_changes.into_iter();
- let mut source_change = source_changes.next().unwrap_or_default();
- source_change.file_system_edits.clear();
- // no collect here because we want to merge text edits on same file ids
- source_change.extend(source_changes.flat_map(|it| it.source_file_edits));
- if source_change.source_file_edits.is_empty() {
- Ok(None)
- } else {
- Ok(Some(to_proto::workspace_edit(&snap, source_change)?))
- }
-}
-
-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_declaration(
- snap: GlobalStateSnapshot,
- params: lsp_types::request::GotoDeclarationParams,
-) -> Result<Option<lsp_types::request::GotoDeclarationResponse>> {
- let _p = profile::span("handle_goto_declaration");
- let position = from_proto::file_position(&snap, params.text_document_position_params.clone())?;
- let nav_info = match snap.analysis.goto_declaration(position)? {
- None => return handle_goto_definition(snap, params),
- 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");
- if let Ok(file_path) = &params.text_document.uri.to_file_path() {
- if file_path.file_name().unwrap_or_default() == "Cargo.toml" {
- // search workspaces for parent packages or fallback to workspace root
- let abs_path_buf = match AbsPathBuf::try_from(file_path.to_path_buf()).ok() {
- Some(abs_path_buf) => abs_path_buf,
- None => return Ok(None),
- };
-
- let manifest_path = match ManifestPath::try_from(abs_path_buf).ok() {
- Some(manifest_path) => manifest_path,
- None => return Ok(None),
- };
-
- let links: Vec<LocationLink> = snap
- .workspaces
- .iter()
- .filter_map(|ws| match ws {
- ProjectWorkspace::Cargo { cargo, .. } => cargo.parent_manifests(&manifest_path),
- _ => None,
- })
- .flatten()
- .map(|parent_manifest_path| LocationLink {
- origin_selection_range: None,
- target_uri: to_proto::url_from_abs_path(&parent_manifest_path),
- target_range: Range::default(),
- target_selection_range: Range::default(),
- })
- .collect::<_>();
- return Ok(Some(links.into()));
- }
-
- // check if invoked at the crate root
- let file_id = from_proto::file_id(&snap, &params.text_document.uri)?;
- let crate_id = match snap.analysis.crates_for(file_id)?.first() {
- Some(&crate_id) => crate_id,
- None => return Ok(None),
- };
- let cargo_spec = match CargoTargetSpec::for_file(&snap, file_id)? {
- Some(it) => it,
- None => return Ok(None),
- };
-
- if snap.analysis.crate_root(crate_id)? == file_id {
- let cargo_toml_url = to_proto::url_from_abs_path(&cargo_spec.cargo_toml);
- let res = vec![LocationLink {
- origin_selection_range: None,
- target_uri: cargo_toml_url,
- target_range: Range::default(),
- target_selection_range: Range::default(),
- }]
- .into();
- return Ok(Some(res));
- }
- }
-
- // locate parent module by semantics
- 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.file_line_index(file_id)?;
- let offset = params.position.and_then(|it| from_proto::offset(&line_index, it).ok());
- 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 should_skip_for_offset(&runnable, offset) {
- continue;
- }
- if should_skip_target(&runnable, cargo_spec.as_ref()) {
- continue;
- }
- let mut runnable = to_proto::runnable(&snap, 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"] {
- res.push(lsp_ext::Runnable {
- label: format!("cargo {cmd} -p {} --all-targets", 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 => {
- if !snap.config.linked_projects().is_empty() {
- 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,
- cargo_args: vec!["check".to_string(), "--workspace".to_string()],
- cargo_extra_args: config.cargo_extra_args,
- executable_args: Vec::new(),
- expect_test: None,
- },
- });
- }
- }
- }
- Ok(res)
-}
-
-fn should_skip_for_offset(runnable: &Runnable, offset: Option<TextSize>) -> bool {
- match offset {
- None => false,
- _ if matches!(&runnable.kind, RunnableKind::TestMod { .. }) => false,
- Some(offset) => !runnable.nav.full_range.contains_inclusive(offset),
- }
-}
-
-pub(crate) fn handle_related_tests(
- snap: GlobalStateSnapshot,
- params: lsp_types::TextDocumentPositionParams,
-) -> Result<Vec<lsp_ext::TestInfo>> {
- let _p = profile::span("handle_related_tests");
- let position = from_proto::file_position(&snap, params)?;
-
- let tests = snap.analysis.related_tests(position, None)?;
- let mut res = Vec::new();
- for it in tests {
- if let Ok(runnable) = to_proto::runnable(&snap, it) {
- res.push(lsp_ext::TestInfo { runnable })
- }
- }
-
- 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_trigger_character =
- params.context.and_then(|ctx| ctx.trigger_character).and_then(|s| s.chars().next());
-
- let completion_config = &snap.config.completion();
- let items = match snap.analysis.completions(
- completion_config,
- position,
- completion_trigger_character,
- )? {
- None => return Ok(None),
- Some(items) => items,
- };
- let line_index = snap.file_line_index(position.file_id)?;
-
- let items =
- to_proto::completion_items(&snap.config, &line_index, text_document_position, items);
-
- 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(invalid_params_error(
- "Received a completion with overlapping edits, this is not LSP-compliant".to_string(),
- )
- .into());
- }
-
- let data = match original_completion.data.take() {
- Some(it) => it,
- None => return Ok(original_completion),
- };
-
- let resolve_data: lsp_ext::CompletionResolveData = serde_json::from_value(data)?;
-
- let file_id = from_proto::file_id(&snap, &resolve_data.position.text_document.uri)?;
- let line_index = snap.file_line_index(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
- .imports
- .into_iter()
- .map(|import| (import.full_import_path, import.imported_name)),
- )?
- .into_iter()
- .flat_map(|edit| edit.into_iter().map(|indel| to_proto::text_edit(&line_index, 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.file_line_index(file_id)?;
- let line_folding_only = snap.config.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 help = match snap.analysis.signature_help(position)? {
- Some(it) => it,
- None => return Ok(None),
- };
- let config = snap.config.call_info();
- let res = to_proto::signature_help(help, config, snap.config.signature_help_label_offsets());
- Ok(Some(res))
-}
-
-pub(crate) fn handle_hover(
- snap: GlobalStateSnapshot,
- params: lsp_ext::HoverParams,
-) -> Result<Option<lsp_ext::Hover>> {
- let _p = profile::span("handle_hover");
- let range = match params.position {
- PositionOrRange::Position(position) => Range::new(position, position),
- PositionOrRange::Range(range) => range,
- };
-
- let file_range = from_proto::file_range(&snap, params.text_document, range)?;
- let info = match snap.analysis.hover(&snap.config.hover(), file_range)? {
- None => return Ok(None),
- Some(info) => info,
- };
-
- let line_index = snap.file_line_index(file_range.file_id)?;
- let range = to_proto::range(&line_index, info.range);
- let markup_kind = snap.config.hover().format;
- let hover = lsp_ext::Hover {
- hover: lsp_types::Hover {
- contents: HoverContents::Markup(to_proto::markup_content(
- info.info.markup,
- markup_kind,
- )),
- range: Some(range),
- },
- actions: if snap.config.hover_actions().none() {
- Vec::new()
- } else {
- prepare_hover_actions(&snap, &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)?.map_err(to_proto::rename_error)?;
-
- let line_index = snap.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)?;
-
- let mut change =
- snap.analysis.rename(position, &params.new_name)?.map_err(to_proto::rename_error)?;
-
- // this is kind of a hack to prevent double edits from happening when moving files
- // When a module gets renamed by renaming the mod declaration this causes the file to move
- // which in turn will trigger a WillRenameFiles request to the server for which we reply with a
- // a second identical set of renames, the client will then apply both edits causing incorrect edits
- // with this we only emit source_file_edits in the WillRenameFiles response which will do the rename instead
- // See https://github.com/microsoft/vscode-languageserver-node/issues/752 for more info
- if !change.file_system_edits.is_empty() && snap.config.will_rename() {
- change.source_file_edits.clear();
- }
- let workspace_edit = to_proto::workspace_edit(&snap, change)?;
- 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 exclude_imports = snap.config.find_all_refs_exclude_imports();
-
- let refs = match snap.analysis.find_all_refs(position, None)? {
- None => return Ok(None),
- Some(refs) => refs,
- };
-
- let include_declaration = params.context.include_declaration;
- let locations = refs
- .into_iter()
- .flat_map(|refs| {
- let decl = if include_declaration {
- refs.declaration.map(|decl| FileRange {
- file_id: decl.nav.file_id,
- range: decl.nav.focus_or_full_range(),
- })
- } else {
- None
- };
- refs.references
- .into_iter()
- .flat_map(|(file_id, refs)| {
- refs.into_iter()
- .filter(|&(_, category)| {
- !exclude_imports || category != Some(ReferenceCategory::Import)
- })
- .map(move |(range, _)| FileRange { file_id, range })
- })
- .chain(decl)
- })
- .filter_map(|frange| to_proto::location(&snap, frange).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");
-
- run_rustfmt(&snap, params.text_document, None)
-}
-
-pub(crate) fn handle_range_formatting(
- snap: GlobalStateSnapshot,
- params: lsp_types::DocumentRangeFormattingParams,
-) -> Result<Option<Vec<lsp_types::TextEdit>>> {
- let _p = profile::span("handle_range_formatting");
-
- run_rustfmt(&snap, params.text_document, Some(params.range))
-}
-
-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");
-
- if !snap.config.code_action_literals() {
- // We intentionally don't support command-based actions, as those either
- // require either custom client-code or server-initiated edits. Server
- // initiated edits break causality, so we avoid those.
- return Ok(None);
- }
-
- let line_index =
- snap.file_line_index(from_proto::file_id(&snap, &params.text_document.uri)?)?;
- let frange = from_proto::file_range(&snap, params.text_document.clone(), params.range)?;
-
- let mut assists_config = snap.config.assist();
- assists_config.allowed = params
- .context
- .only
- .clone()
- .map(|it| it.into_iter().filter_map(from_proto::assist_kind).collect());
-
- let mut res: Vec<lsp_ext::CodeAction> = Vec::new();
-
- let code_action_resolve_cap = snap.config.code_action_resolve();
- let resolve = if code_action_resolve_cap {
- AssistResolveStrategy::None
- } else {
- AssistResolveStrategy::All
- };
- let assists = snap.analysis.assists_with_fixes(
- &assists_config,
- &snap.config.diagnostics(),
- resolve,
- frange,
- )?;
- for (index, assist) in assists.into_iter().enumerate() {
- let resolve_data =
- if code_action_resolve_cap { Some((index, params.clone())) } else { None };
- let code_action = to_proto::code_action(&snap, assist, resolve_data)?;
- res.push(code_action)
- }
-
- // Fixes from `cargo check`.
- for fix in snap.check_fixes.values().filter_map(|it| it.get(&frange.file_id)).flatten() {
- // FIXME: this mapping is awkward and shouldn't exist. Refactor
- // `snap.check_fixes` to not convert to LSP prematurely.
- let intersect_fix_range = fix
- .ranges
- .iter()
- .copied()
- .filter_map(|range| from_proto::text_range(&line_index, range).ok())
- .any(|fix_range| fix_range.intersect(frange.range).is_some());
- if intersect_fix_range {
- res.push(fix.action.clone());
- }
- }
-
- Ok(Some(res))
-}
-
-pub(crate) fn handle_code_action_resolve(
- 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 => return Err(invalid_params_error("code action without data".to_string()).into()),
- };
-
- let file_id = from_proto::file_id(&snap, &params.code_action_params.text_document.uri)?;
- let line_index = snap.file_line_index(file_id)?;
- let range = from_proto::text_range(&line_index, params.code_action_params.range)?;
- let frange = FileRange { file_id, range };
-
- let mut assists_config = snap.config.assist();
- assists_config.allowed = params
- .code_action_params
- .context
- .only
- .map(|it| it.into_iter().filter_map(from_proto::assist_kind).collect());
-
- let (assist_index, assist_resolve) = match parse_action_id(&params.id) {
- Ok(parsed_data) => parsed_data,
- Err(e) => {
- return Err(invalid_params_error(format!(
- "Failed to parse action id string '{}': {e}",
- params.id
- ))
- .into())
- }
- };
-
- let expected_assist_id = assist_resolve.assist_id.clone();
- let expected_kind = assist_resolve.assist_kind;
-
- let assists = snap.analysis.assists_with_fixes(
- &assists_config,
- &snap.config.diagnostics(),
- AssistResolveStrategy::Single(assist_resolve),
- frange,
- )?;
-
- let assist = match assists.get(assist_index) {
- Some(assist) => assist,
- None => return Err(invalid_params_error(format!(
- "Failed to find the assist for index {} provided by the resolve request. Resolve request assist id: {}",
- assist_index, params.id,
- ))
- .into())
- };
- if assist.id.0 != expected_assist_id || assist.id.1 != expected_kind {
- return Err(invalid_params_error(format!(
- "Mismatching assist at index {} for the resolve parameters given. Resolve request assist id: {}, actual id: {:?}.",
- assist_index, params.id, assist.id
- ))
- .into());
- }
- let ca = to_proto::code_action(&snap, assist.clone(), None)?;
- code_action.edit = ca.edit;
- code_action.command = ca.command;
- Ok(code_action)
-}
-
-fn parse_action_id(action_id: &str) -> Result<(usize, SingleResolve), String> {
- let id_parts = action_id.split(':').collect::<Vec<_>>();
- match id_parts.as_slice() {
- [assist_id_string, assist_kind_string, index_string] => {
- let assist_kind: AssistKind = assist_kind_string.parse()?;
- let index: usize = match index_string.parse() {
- Ok(index) => index,
- Err(e) => return Err(format!("Incorrect index string: {e}")),
- };
- Ok((index, SingleResolve { assist_id: assist_id_string.to_string(), assist_kind }))
- }
- _ => Err("Action id contains incorrect number of segments".to_string()),
- }
-}
-
-pub(crate) fn handle_code_lens(
- snap: GlobalStateSnapshot,
- params: lsp_types::CodeLensParams,
-) -> Result<Option<Vec<CodeLens>>> {
- let _p = profile::span("handle_code_lens");
-
- let lens_config = snap.config.lens();
- if lens_config.none() {
- // early return before any db query!
- return Ok(Some(Vec::default()));
- }
-
- let file_id = from_proto::file_id(&snap, &params.text_document.uri)?;
- let cargo_target_spec = CargoTargetSpec::for_file(&snap, file_id)?;
-
- let annotations = snap.analysis.annotations(
- &AnnotationConfig {
- binary_target: cargo_target_spec
- .map(|spec| {
- matches!(
- spec.target_kind,
- TargetKind::Bin | TargetKind::Example | TargetKind::Test
- )
- })
- .unwrap_or(false),
- annotate_runnables: lens_config.runnable(),
- annotate_impls: lens_config.implementations,
- annotate_references: lens_config.refs_adt,
- annotate_method_references: lens_config.method_refs,
- annotate_enum_variant_references: lens_config.enum_variant_refs,
- location: lens_config.location.into(),
- },
- file_id,
- )?;
-
- let mut res = Vec::new();
- for a in annotations {
- to_proto::code_lens(&mut res, &snap, a)?;
- }
-
- Ok(Some(res))
-}
-
-pub(crate) fn handle_code_lens_resolve(
- snap: GlobalStateSnapshot,
- code_lens: CodeLens,
-) -> Result<CodeLens> {
- let Some(annotation) = from_proto::annotation(&snap, code_lens.clone())? else { return Ok(code_lens) };
- let annotation = snap.analysis.resolve_annotation(annotation)?;
-
- let mut acc = Vec::new();
- to_proto::code_lens(&mut acc, &snap, annotation)?;
-
- let res = match acc.pop() {
- Some(it) if acc.is_empty() => it,
- _ => {
- never!();
- code_lens
- }
- };
-
- Ok(res)
-}
-
-pub(crate) fn handle_document_highlight(
- snap: GlobalStateSnapshot,
- params: lsp_types::DocumentHighlightParams,
-) -> Result<Option<Vec<lsp_types::DocumentHighlight>>> {
- let _p = profile::span("handle_document_highlight");
- let position = from_proto::file_position(&snap, params.text_document_position_params)?;
- let line_index = snap.file_line_index(position.file_id)?;
-
- let refs = match snap.analysis.highlight_related(snap.config.highlight_related(), position)? {
- None => return Ok(None),
- Some(refs) => refs,
- };
- let res = refs
- .into_iter()
- .map(|ide::HighlightedRange { range, category }| lsp_types::DocumentHighlight {
- range: to_proto::range(&line_index, range),
- kind: category.and_then(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).map_err(Into::into)
-}
+pub(crate) mod request;
+pub(crate) mod notification;
pub(crate) fn publish_diagnostics(
snap: &GlobalStateSnapshot,
@@ -1364,559 +42,3 @@ pub(crate) fn publish_diagnostics(
.collect();
Ok(diagnostics)
}
-
-pub(crate) fn handle_inlay_hints(
- snap: GlobalStateSnapshot,
- params: InlayHintParams,
-) -> Result<Option<Vec<InlayHint>>> {
- let _p = profile::span("handle_inlay_hints");
- let document_uri = &params.text_document.uri;
- let FileRange { file_id, range } = from_proto::file_range(
- &snap,
- TextDocumentIdentifier::new(document_uri.to_owned()),
- params.range,
- )?;
- let line_index = snap.file_line_index(file_id)?;
- let inlay_hints_config = snap.config.inlay_hints();
- Ok(Some(
- snap.analysis
- .inlay_hints(&inlay_hints_config, file_id, Some(range))?
- .into_iter()
- .map(|it| {
- to_proto::inlay_hint(&snap, &line_index, inlay_hints_config.render_colons, it)
- })
- .collect::<Cancellable<Vec<_>>>()?,
- ))
-}
-
-pub(crate) fn handle_inlay_hints_resolve(
- _snap: GlobalStateSnapshot,
- hint: InlayHint,
-) -> Result<InlayHint> {
- let _p = profile::span("handle_inlay_hints_resolve");
- Ok(hint)
-}
-
-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::<Cancellable<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.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.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.file_line_index(file_id)?;
-
- let mut highlight_config = snap.config.highlighting_config();
- // Avoid flashing a bunch of unresolved references when the proc-macro servers haven't been spawned yet.
- highlight_config.syntactic_name_ref_highlighting =
- snap.workspaces.is_empty() || !snap.proc_macros_loaded;
-
- let highlights = snap.analysis.highlight(highlight_config, 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.file_line_index(file_id)?;
-
- let mut highlight_config = snap.config.highlighting_config();
- // Avoid flashing a bunch of unresolved references when the proc-macro servers haven't been spawned yet.
- highlight_config.syntactic_name_ref_highlighting =
- snap.workspaces.is_empty() || !snap.proc_macros_loaded;
-
- let highlights = snap.analysis.highlight(highlight_config, 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.file_line_index(frange.file_id)?;
-
- let mut highlight_config = snap.config.highlighting_config();
- // Avoid flashing a bunch of unresolved references when the proc-macro servers haven't been spawned yet.
- highlight_config.syntactic_name_ref_highlighting =
- snap.workspaces.is_empty() || !snap.proc_macros_loaded;
-
- let highlights = snap.analysis.highlight_range(highlight_config, 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))
-}
-
-pub(crate) fn handle_move_item(
- snap: GlobalStateSnapshot,
- params: lsp_ext::MoveItemParams,
-) -> Result<Vec<lsp_ext::SnippetTextEdit>> {
- let _p = profile::span("handle_move_item");
- let file_id = from_proto::file_id(&snap, &params.text_document.uri)?;
- let range = from_proto::file_range(&snap, params.text_document, params.range)?;
-
- let direction = match params.direction {
- lsp_ext::MoveItemDirection::Up => ide::Direction::Up,
- lsp_ext::MoveItemDirection::Down => ide::Direction::Down,
- };
-
- match snap.analysis.move_item(range, direction)? {
- Some(text_edit) => {
- let line_index = snap.file_line_index(file_id)?;
- Ok(to_proto::snippet_text_edit_vec(&line_index, true, text_edit))
- }
- None => Ok(vec![]),
- }
-}
-
-fn to_command_link(command: lsp_types::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_actions().implementations && snap.config.client_commands().show_reference {
- 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.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 = to_proto::implementation_title(locations.len());
- let command = to_proto::command::show_references(title, &uri, position, locations);
-
- return Some(lsp_ext::CommandLinkGroup {
- commands: vec![to_command_link(command, "Go to implementations".into())],
- ..Default::default()
- });
- }
- }
- None
-}
-
-fn show_ref_command_link(
- snap: &GlobalStateSnapshot,
- position: &FilePosition,
-) -> Option<lsp_ext::CommandLinkGroup> {
- if snap.config.hover_actions().references && snap.config.client_commands().show_reference {
- if let Some(ref_search_res) = snap.analysis.find_all_refs(*position, None).unwrap_or(None) {
- let uri = to_proto::url(snap, position.file_id);
- let line_index = snap.file_line_index(position.file_id).ok()?;
- let position = to_proto::position(&line_index, position.offset);
- let locations: Vec<_> = ref_search_res
- .into_iter()
- .flat_map(|res| res.references)
- .flat_map(|(file_id, ranges)| {
- ranges.into_iter().filter_map(move |(range, _)| {
- to_proto::location(snap, FileRange { file_id, range }).ok()
- })
- })
- .collect();
- let title = to_proto::reference_title(locations.len());
- let command = to_proto::command::show_references(title, &uri, position, locations);
-
- return Some(lsp_ext::CommandLinkGroup {
- commands: vec![to_command_link(command, "Go to references".into())],
- ..Default::default()
- });
- }
- }
- None
-}
-
-fn runnable_action_links(
- snap: &GlobalStateSnapshot,
- runnable: Runnable,
-) -> Option<lsp_ext::CommandLinkGroup> {
- let hover_actions_config = snap.config.hover_actions();
- if !hover_actions_config.runnable() {
- return None;
- }
-
- let cargo_spec = CargoTargetSpec::for_file(snap, runnable.nav.file_id).ok()?;
- if should_skip_target(&runnable, cargo_spec.as_ref()) {
- return None;
- }
-
- let client_commands_config = snap.config.client_commands();
- if !(client_commands_config.run_single || client_commands_config.debug_single) {
- return None;
- }
-
- let title = runnable.title();
- let r = to_proto::runnable(snap, runnable).ok()?;
-
- let mut group = lsp_ext::CommandLinkGroup::default();
-
- if hover_actions_config.run && client_commands_config.run_single {
- let run_command = to_proto::command::run_single(&r, &title);
- group.commands.push(to_command_link(run_command, r.label.clone()));
- }
-
- if hover_actions_config.debug && client_commands_config.debug_single {
- let dbg_command = to_proto::command::debug_single(&r);
- group.commands.push(to_command_link(dbg_command, r.label));
- }
-
- Some(group)
-}
-
-fn goto_type_action_links(
- snap: &GlobalStateSnapshot,
- nav_targets: &[HoverGotoTypeData],
-) -> Option<lsp_ext::CommandLinkGroup> {
- if !snap.config.hover_actions().goto_type_def
- || nav_targets.is_empty()
- || !snap.config.client_commands().goto_location
- {
- return None;
- }
-
- Some(lsp_ext::CommandLinkGroup {
- title: Some("Go to ".into()),
- commands: nav_targets
- .iter()
- .filter_map(|it| {
- to_proto::command::goto_location(snap, &it.nav)
- .map(|cmd| to_command_link(cmd, it.mod_path.clone()))
- })
- .collect(),
- })
-}
-
-fn prepare_hover_actions(
- snap: &GlobalStateSnapshot,
- actions: &[HoverAction],
-) -> Vec<lsp_ext::CommandLinkGroup> {
- actions
- .iter()
- .filter_map(|it| match it {
- HoverAction::Implementation(position) => show_impl_command_link(snap, position),
- HoverAction::Reference(position) => show_ref_command_link(snap, position),
- HoverAction::Runnable(r) => runnable_action_links(snap, 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,
- }
-}
-
-fn run_rustfmt(
- snap: &GlobalStateSnapshot,
- text_document: TextDocumentIdentifier,
- range: Option<lsp_types::Range>,
-) -> Result<Option<Vec<lsp_types::TextEdit>>> {
- let file_id = from_proto::file_id(snap, &text_document.uri)?;
- let file = snap.analysis.file_text(file_id)?;
-
- // Determine the edition of the crate the file belongs to (if there's multiple, we pick the
- // highest edition).
- let editions = snap
- .analysis
- .relevant_crates_for(file_id)?
- .into_iter()
- .map(|crate_id| snap.analysis.crate_edition(crate_id))
- .collect::<Result<Vec<_>, _>>()?;
- let edition = editions.iter().copied().max();
-
- let line_index = snap.file_line_index(file_id)?;
-
- let mut command = match snap.config.rustfmt() {
- RustfmtConfig::Rustfmt { extra_args, enable_range_formatting } => {
- let mut cmd = process::Command::new(toolchain::rustfmt());
- cmd.envs(snap.config.extra_env());
- cmd.args(extra_args);
- // try to chdir to the file so we can respect `rustfmt.toml`
- // FIXME: use `rustfmt --config-path` once
- // https://github.com/rust-lang/rustfmt/issues/4660 gets fixed
- match text_document.uri.to_file_path() {
- Ok(mut path) => {
- // pop off file name
- if path.pop() && path.is_dir() {
- cmd.current_dir(path);
- }
- }
- Err(_) => {
- tracing::error!(
- "Unable to get file path for {}, rustfmt.toml might be ignored",
- text_document.uri
- );
- }
- }
- if let Some(edition) = edition {
- cmd.arg("--edition");
- cmd.arg(edition.to_string());
- }
-
- if let Some(range) = range {
- if !enable_range_formatting {
- return Err(LspError::new(
- ErrorCode::InvalidRequest as i32,
- String::from(
- "rustfmt range formatting is unstable. \
- Opt-in by using a nightly build of rustfmt and setting \
- `rustfmt.rangeFormatting.enable` to true in your LSP configuration",
- ),
- )
- .into());
- }
-
- let frange = from_proto::file_range(snap, text_document, range)?;
- let start_line = line_index.index.line_col(frange.range.start()).line;
- let end_line = line_index.index.line_col(frange.range.end()).line;
-
- cmd.arg("--unstable-features");
- cmd.arg("--file-lines");
- cmd.arg(
- json!([{
- "file": "stdin",
- "range": [start_line, end_line]
- }])
- .to_string(),
- );
- }
-
- cmd
- }
- RustfmtConfig::CustomCommand { command, args } => {
- let mut cmd = process::Command::new(command);
- cmd.envs(snap.config.extra_env());
- cmd.args(args);
- cmd
- }
- };
-
- let mut rustfmt = command
- .stdin(Stdio::piped())
- .stdout(Stdio::piped())
- .stderr(Stdio::piped())
- .spawn()
- .context(format!("Failed to spawn {command:?}"))?;
-
- 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() {
- let rustfmt_not_installed =
- captured_stderr.contains("not installed") || captured_stderr.contains("not available");
-
- return match output.status.code() {
- Some(1) if !rustfmt_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.
- tracing::warn!(
- ?command,
- %captured_stderr,
- "rustfmt exited with status 1"
- );
- Ok(None)
- }
- _ => {
- // Something else happened - e.g. `rustfmt` is missing or caught a signal
- Err(LspError::new(
- -32900,
- format!(
- r#"rustfmt exited with:
- Status: {}
- stdout: {captured_stdout}
- stderr: {captured_stderr}"#,
- output.status,
- ),
- )
- .into())
- }
- };
- }
-
- let (new_text, new_line_endings) = LineEndings::normalize(captured_stdout);
-
- if line_index.endings != new_line_endings {
- // If line endings are different, send the entire file.
- // Diffing would not work here, as the line endings might be the only
- // difference.
- Ok(Some(to_proto::text_edit_vec(
- &line_index,
- TextEdit::replace(TextRange::up_to(TextSize::of(&*file)), new_text),
- )))
- } else if *file == new_text {
- // The document is already formatted correctly -- no edits needed.
- Ok(None)
- } else {
- Ok(Some(to_proto::text_edit_vec(&line_index, diff(&file, &new_text))))
- }
-}
diff --git a/crates/rust-analyzer/src/handlers/notification.rs b/crates/rust-analyzer/src/handlers/notification.rs
new file mode 100644
index 0000000000..a734f48301
--- /dev/null
+++ b/crates/rust-analyzer/src/handlers/notification.rs
@@ -0,0 +1,339 @@
+//! This module is responsible for implementing handlers for Language Server
+//! Protocol. This module specifically handles notifications.
+
+use std::{ops::Deref, sync::Arc};
+
+use itertools::Itertools;
+use lsp_types::{
+ CancelParams, DidChangeConfigurationParams, DidChangeTextDocumentParams,
+ DidChangeWatchedFilesParams, DidChangeWorkspaceFoldersParams, DidCloseTextDocumentParams,
+ DidOpenTextDocumentParams, DidSaveTextDocumentParams, WorkDoneProgressCancelParams,
+};
+use vfs::{AbsPathBuf, ChangeKind, VfsPath};
+
+use crate::{
+ config::Config, from_proto, global_state::GlobalState, lsp_ext::RunFlycheckParams,
+ lsp_utils::apply_document_changes, mem_docs::DocumentData, reload, Result,
+};
+
+pub(crate) fn handle_cancel(state: &mut GlobalState, params: CancelParams) -> Result<()> {
+ let id: lsp_server::RequestId = match params.id {
+ lsp_types::NumberOrString::Number(id) => id.into(),
+ lsp_types::NumberOrString::String(id) => id.into(),
+ };
+ state.cancel(id);
+ Ok(())
+}
+
+pub(crate) fn handle_work_done_progress_cancel(
+ state: &mut GlobalState,
+ params: WorkDoneProgressCancelParams,
+) -> Result<()> {
+ if let lsp_types::NumberOrString::String(s) = &params.token {
+ if let Some(id) = s.strip_prefix("rust-analyzer/flycheck/") {
+ if let Ok(id) = u32::from_str_radix(id, 10) {
+ if let Some(flycheck) = state.flycheck.get(id as usize) {
+ flycheck.cancel();
+ }
+ }
+ }
+ }
+
+ // Just ignore this. It is OK to continue sending progress
+ // notifications for this token, as the client can't know when
+ // we accepted notification.
+ Ok(())
+}
+
+pub(crate) fn handle_did_open_text_document(
+ state: &mut GlobalState,
+ params: DidOpenTextDocumentParams,
+) -> Result<()> {
+ let _p = profile::span("handle_did_open_text_document");
+
+ if let Ok(path) = from_proto::vfs_path(&params.text_document.uri) {
+ let already_exists = state
+ .mem_docs
+ .insert(path.clone(), DocumentData::new(params.text_document.version))
+ .is_err();
+ if already_exists {
+ tracing::error!("duplicate DidOpenTextDocument: {}", path);
+ }
+ state.vfs.write().0.set_file_contents(path, Some(params.text_document.text.into_bytes()));
+ }
+ Ok(())
+}
+
+pub(crate) fn handle_did_change_text_document(
+ state: &mut GlobalState,
+ params: DidChangeTextDocumentParams,
+) -> Result<()> {
+ let _p = profile::span("handle_did_change_text_document");
+
+ if let Ok(path) = from_proto::vfs_path(&params.text_document.uri) {
+ match state.mem_docs.get_mut(&path) {
+ Some(doc) => {
+ // The version passed in DidChangeTextDocument is the version after all edits are applied
+ // so we should apply it before the vfs is notified.
+ doc.version = params.text_document.version;
+ }
+ None => {
+ tracing::error!("unexpected DidChangeTextDocument: {}", path);
+ return Ok(());
+ }
+ };
+
+ let vfs = &mut state.vfs.write().0;
+ let file_id = vfs.file_id(&path).unwrap();
+ let text = apply_document_changes(
+ state.config.position_encoding(),
+ || std::str::from_utf8(vfs.file_contents(file_id)).unwrap().into(),
+ params.content_changes,
+ );
+
+ vfs.set_file_contents(path, Some(text.into_bytes()));
+ }
+ Ok(())
+}
+
+pub(crate) fn handle_did_close_text_document(
+ state: &mut GlobalState,
+ params: DidCloseTextDocumentParams,
+) -> Result<()> {
+ let _p = profile::span("handle_did_close_text_document");
+
+ if let Ok(path) = from_proto::vfs_path(&params.text_document.uri) {
+ if state.mem_docs.remove(&path).is_err() {
+ tracing::error!("orphan DidCloseTextDocument: {}", path);
+ }
+
+ state.semantic_tokens_cache.lock().remove(&params.text_document.uri);
+
+ if let Some(path) = path.as_path() {
+ state.loader.handle.invalidate(path.to_path_buf());
+ }
+ }
+ Ok(())
+}
+
+pub(crate) fn handle_did_save_text_document(
+ state: &mut GlobalState,
+ params: DidSaveTextDocumentParams,
+) -> Result<()> {
+ if let Ok(vfs_path) = from_proto::vfs_path(&params.text_document.uri) {
+ // Re-fetch workspaces if a workspace related file has changed
+ if let Some(abs_path) = vfs_path.as_path() {
+ if reload::should_refresh_for_change(abs_path, ChangeKind::Modify) {
+ state
+ .fetch_workspaces_queue
+ .request_op(format!("DidSaveTextDocument {}", abs_path.display()), ());
+ }
+ }
+
+ if !state.config.check_on_save() || run_flycheck(state, vfs_path) {
+ return Ok(());
+ }
+ } else if state.config.check_on_save() {
+ // No specific flycheck was triggered, so let's trigger all of them.
+ for flycheck in state.flycheck.iter() {
+ flycheck.restart();
+ }
+ }
+ Ok(())
+}
+
+pub(crate) fn handle_did_change_configuration(
+ state: &mut GlobalState,
+ _params: DidChangeConfigurationParams,
+) -> Result<()> {
+ // As stated in https://github.com/microsoft/language-server-protocol/issues/676,
+ // this notification's parameters should be ignored and the actual config queried separately.
+ state.send_request::<lsp_types::request::WorkspaceConfiguration>(
+ lsp_types::ConfigurationParams {
+ items: vec![lsp_types::ConfigurationItem {
+ scope_uri: None,
+ section: Some("rust-analyzer".to_string()),
+ }],
+ },
+ |this, resp| {
+ tracing::debug!("config update response: '{:?}", resp);
+ let lsp_server::Response { error, result, .. } = resp;
+
+ match (error, result) {
+ (Some(err), _) => {
+ tracing::error!("failed to fetch the server settings: {:?}", err)
+ }
+ (None, Some(mut configs)) => {
+ if let Some(json) = configs.get_mut(0) {
+ // Note that json can be null according to the spec if the client can't
+ // provide a configuration. This is handled in Config::update below.
+ let mut config = Config::clone(&*this.config);
+ if let Err(error) = config.update(json.take()) {
+ this.show_message(
+ lsp_types::MessageType::WARNING,
+ error.to_string(),
+ false,
+ );
+ }
+ this.update_configuration(config);
+ }
+ }
+ (None, None) => {
+ tracing::error!("received empty server settings response from the client")
+ }
+ }
+ },
+ );
+
+ Ok(())
+}
+
+pub(crate) fn handle_did_change_workspace_folders(
+ state: &mut GlobalState,
+ params: DidChangeWorkspaceFoldersParams,
+) -> Result<()> {
+ let config = Arc::make_mut(&mut state.config);
+
+ for workspace in params.event.removed {
+ let Ok(path) = workspace.uri.to_file_path() else { continue };
+ let Ok(path) = AbsPathBuf::try_from(path) else { continue };
+ config.remove_workspace(&path);
+ }
+
+ let added = params
+ .event
+ .added
+ .into_iter()
+ .filter_map(|it| it.uri.to_file_path().ok())
+ .filter_map(|it| AbsPathBuf::try_from(it).ok());
+ config.add_workspaces(added);
+
+ if !config.has_linked_projects() && config.detached_files().is_empty() {
+ config.rediscover_workspaces();
+ state.fetch_workspaces_queue.request_op("client workspaces changed".to_string(), ())
+ }
+
+ Ok(())
+}
+
+pub(crate) fn handle_did_change_watched_files(
+ state: &mut GlobalState,
+ params: DidChangeWatchedFilesParams,
+) -> Result<()> {
+ for change in params.changes {
+ if let Ok(path) = from_proto::abs_path(&change.uri) {
+ state.loader.handle.invalidate(path);
+ }
+ }
+ Ok(())
+}
+
+fn run_flycheck(state: &mut GlobalState, vfs_path: VfsPath) -> bool {
+ let _p = profile::span("run_flycheck");
+
+ let file_id = state.vfs.read().0.file_id(&vfs_path);
+ if let Some(file_id) = file_id {
+ let world = state.snapshot();
+ let mut updated = false;
+ let task = move || -> std::result::Result<(), ide::Cancelled> {
+ // Trigger flychecks for all workspaces that depend on the saved file
+ // Crates containing or depending on the saved file
+ let crate_ids: Vec<_> = world
+ .analysis
+ .crates_for(file_id)?
+ .into_iter()
+ .flat_map(|id| world.analysis.transitive_rev_deps(id))
+ .flatten()
+ .sorted()
+ .unique()
+ .collect();
+
+ let crate_root_paths: Vec<_> = crate_ids
+ .iter()
+ .filter_map(|&crate_id| {
+ world
+ .analysis
+ .crate_root(crate_id)
+ .map(|file_id| {
+ world.file_id_to_file_path(file_id).as_path().map(ToOwned::to_owned)
+ })
+ .transpose()
+ })
+ .collect::<ide::Cancellable<_>>()?;
+ let crate_root_paths: Vec<_> = crate_root_paths.iter().map(Deref::deref).collect();
+
+ // Find all workspaces that have at least one target containing the saved file
+ let workspace_ids = world.workspaces.iter().enumerate().filter(|(_, ws)| match ws {
+ project_model::ProjectWorkspace::Cargo { cargo, .. } => {
+ cargo.packages().any(|pkg| {
+ cargo[pkg]
+ .targets
+ .iter()
+ .any(|&it| crate_root_paths.contains(&cargo[it].root.as_path()))
+ })
+ }
+ project_model::ProjectWorkspace::Json { project, .. } => {
+ project.crates().any(|(c, _)| crate_ids.iter().any(|&crate_id| crate_id == c))
+ }
+ project_model::ProjectWorkspace::DetachedFiles { .. } => false,
+ });
+
+ // Find and trigger corresponding flychecks
+ for flycheck in world.flycheck.iter() {
+ for (id, _) in workspace_ids.clone() {
+ if id == flycheck.id() {
+ updated = true;
+ flycheck.restart();
+ continue;
+ }
+ }
+ }
+ // No specific flycheck was triggered, so let's trigger all of them.
+ if !updated {
+ for flycheck in world.flycheck.iter() {
+ flycheck.restart();
+ }
+ }
+ Ok(())
+ };
+ state.task_pool.handle.spawn_with_sender(move |_| {
+ if let Err(e) = std::panic::catch_unwind(task) {
+ tracing::error!("flycheck task panicked: {e:?}")
+ }
+ });
+ true
+ } else {
+ false
+ }
+}
+
+pub(crate) fn handle_cancel_flycheck(state: &mut GlobalState, _: ()) -> Result<()> {
+ let _p = profile::span("handle_stop_flycheck");
+ state.flycheck.iter().for_each(|flycheck| flycheck.cancel());
+ Ok(())
+}
+
+pub(crate) fn handle_clear_flycheck(state: &mut GlobalState, _: ()) -> Result<()> {
+ let _p = profile::span("handle_clear_flycheck");
+ state.diagnostics.clear_check_all();
+ Ok(())
+}
+
+pub(crate) fn handle_run_flycheck(
+ state: &mut GlobalState,
+ params: RunFlycheckParams,
+) -> Result<()> {
+ let _p = profile::span("handle_run_flycheck");
+ if let Some(text_document) = params.text_document {
+ if let Ok(vfs_path) = from_proto::vfs_path(&text_document.uri) {
+ if run_flycheck(state, vfs_path) {
+ return Ok(());
+ }
+ }
+ }
+ // No specific flycheck was triggered, so let's trigger all of them.
+ for flycheck in state.flycheck.iter() {
+ flycheck.restart();
+ }
+ Ok(())
+}
diff --git a/crates/rust-analyzer/src/handlers/request.rs b/crates/rust-analyzer/src/handlers/request.rs
new file mode 100644
index 0000000000..03e08d9cdf
--- /dev/null
+++ b/crates/rust-analyzer/src/handlers/request.rs
@@ -0,0 +1,1883 @@
+//! This module is responsible for implementing handlers for Language Server
+//! Protocol. This module specifically handles requests.
+
+use std::{
+ io::Write as _,
+ process::{self, Stdio},
+ sync::Arc,
+};
+
+use anyhow::Context;
+use ide::{
+ AnnotationConfig, AssistKind, AssistResolveStrategy, Cancellable, FilePosition, FileRange,
+ HoverAction, HoverGotoTypeData, Query, RangeInfo, ReferenceCategory, Runnable, RunnableKind,
+ SingleResolve, SourceChange, TextEdit,
+};
+use ide_db::SymbolKind;
+use lsp_server::ErrorCode;
+use lsp_types::{
+ CallHierarchyIncomingCall, CallHierarchyIncomingCallsParams, CallHierarchyItem,
+ CallHierarchyOutgoingCall, CallHierarchyOutgoingCallsParams, CallHierarchyPrepareParams,
+ CodeLens, CompletionItem, DocumentFormattingParams, FoldingRange, FoldingRangeParams,
+ HoverContents, InlayHint, InlayHintParams, Location, LocationLink, Position,
+ PrepareRenameResponse, Range, RenameParams, SemanticTokensDeltaParams,
+ SemanticTokensFullDeltaResult, SemanticTokensParams, SemanticTokensRangeParams,
+ SemanticTokensRangeResult, SemanticTokensResult, SymbolInformation, SymbolTag,
+ TextDocumentIdentifier, Url, WorkspaceEdit,
+};
+use project_model::{ManifestPath, ProjectWorkspace, TargetKind};
+use serde_json::json;
+use stdx::{format_to, never};
+use syntax::{algo, ast, AstNode, TextRange, TextSize};
+use vfs::{AbsPath, AbsPathBuf};
+
+use crate::{
+ cargo_target_spec::CargoTargetSpec,
+ config::{RustfmtConfig, WorkspaceSymbolConfig},
+ diff::diff,
+ from_proto,
+ global_state::{GlobalState, GlobalStateSnapshot},
+ line_index::LineEndings,
+ lsp_ext::{self, PositionOrRange, ViewCrateGraphParams, WorkspaceSymbolParams},
+ lsp_utils::{all_edits_are_disjoint, invalid_params_error},
+ to_proto, LspError, Result,
+};
+
+pub(crate) fn handle_workspace_reload(state: &mut GlobalState, _: ()) -> Result<()> {
+ state.proc_macro_clients = Arc::new([]);
+ state.proc_macro_changed = false;
+
+ state.fetch_workspaces_queue.request_op("reload workspace request".to_string(), ());
+ Ok(())
+}
+
+pub(crate) fn handle_proc_macros_rebuild(state: &mut GlobalState, _: ()) -> Result<()> {
+ state.proc_macro_clients = Arc::new([]);
+ state.proc_macro_changed = false;
+
+ state.fetch_build_data_queue.request_op("rebuild proc macros request".to_string(), ());
+ Ok(())
+}
+
+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");
+ format_to!(
+ buf,
+ "Loaded {:?} packages across {} workspace{}.\n",
+ snap.workspaces.iter().map(|w| w.n_packages()).sum::<usize>(),
+ snap.workspaces.len(),
+ if snap.workspaces.len() == 1 { "" } else { "s" }
+ );
+
+ format_to!(
+ buf,
+ "Workspace root folders: {:?}",
+ snap.workspaces
+ .iter()
+ .flat_map(|ws| ws.workspace_definition_path())
+ .collect::<Vec<&AbsPath>>()
+ );
+ }
+ format_to!(buf, "\nVfs memory usage: {}\n", snap.vfs_memory_usage());
+ buf.push_str("\nAnalysis:\n");
+ buf.push_str(
+ &snap
+ .analysis
+ .status(file_id)
+ .unwrap_or_else(|_| "Analysis retrieval was cancelled".to_owned()),
+ );
+ Ok(buf)
+}
+
+pub(crate) fn handle_memory_usage(state: &mut GlobalState, _: ()) -> Result<String> {
+ let _p = profile::span("handle_memory_usage");
+ let mut mem = state.analysis_host.per_query_memory_usage();
+ mem.push(("Remaining".into(), profile::memory_usage().allocated));
+
+ let mut out = String::new();
+ for (name, bytes) in mem {
+ format_to!(out, "{:>8} {}\n", bytes, name);
+ }
+ Ok(out)
+}
+
+pub(crate) fn handle_shuffle_crate_graph(state: &mut GlobalState, _: ()) -> Result<()> {
+ state.analysis_host.shuffle_crate_graph();
+ Ok(())
+}
+
+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.file_line_index(id)?;
+ let text_range = params.range.and_then(|r| from_proto::text_range(&line_index, r).ok());
+ 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_view_mir(
+ snap: GlobalStateSnapshot,
+ params: lsp_types::TextDocumentPositionParams,
+) -> Result<String> {
+ let _p = profile::span("handle_view_mir");
+ let position = from_proto::file_position(&snap, params)?;
+ let res = snap.analysis.view_mir(position)?;
+ Ok(res)
+}
+
+pub(crate) fn handle_interpret_function(
+ snap: GlobalStateSnapshot,
+ params: lsp_types::TextDocumentPositionParams,
+) -> Result<String> {
+ let _p = profile::span("handle_interpret_function");
+ let position = from_proto::file_position(&snap, params)?;
+ let res = snap.analysis.interpret_function(position)?;
+ Ok(res)
+}
+
+pub(crate) fn handle_view_file_text(
+ snap: GlobalStateSnapshot,
+ params: lsp_types::TextDocumentIdentifier,
+) -> Result<String> {
+ let file_id = from_proto::file_id(&snap, &params.uri)?;
+ Ok(snap.analysis.file_text(file_id)?.to_string())
+}
+
+pub(crate) fn handle_view_item_tree(
+ snap: GlobalStateSnapshot,
+ params: lsp_ext::ViewItemTreeParams,
+) -> Result<String> {
+ let _p = profile::span("handle_view_item_tree");
+ let file_id = from_proto::file_id(&snap, &params.text_document.uri)?;
+ let res = snap.analysis.view_item_tree(file_id)?;
+ Ok(res)
+}
+
+pub(crate) fn handle_view_crate_graph(
+ snap: GlobalStateSnapshot,
+ params: ViewCrateGraphParams,
+) -> Result<String> {
+ let _p = profile::span("handle_view_crate_graph");
+ let dot = snap.analysis.view_crate_graph(params.full)??;
+ Ok(dot)
+}
+
+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.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.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.file_line_index(file_id)?;
+ params
+ .positions
+ .into_iter()
+ .map(|position| {
+ let offset = from_proto::offset(&line_index, position);
+ offset.map(|offset| {
+ 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()
+}
+
+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 config = snap.config.join_lines();
+ let file_id = from_proto::file_id(&snap, &params.text_document.uri)?;
+ let line_index = snap.file_line_index(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(&config, FileRange { file_id, range })?;
+ match res.union(edit) {
+ Ok(()) => (),
+ Err(_edit) => {
+ // just ignore overlapping edits
+ }
+ }
+ }
+
+ Ok(to_proto::text_edit_vec(&line_index, 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.file_line_index(position.file_id)?;
+ let edit = to_proto::snippet_text_edit_vec(&line_index, true, edit);
+ Ok(Some(edit))
+}
+
+pub(crate) fn handle_on_type_formatting(
+ snap: GlobalStateSnapshot,
+ params: lsp_types::DocumentOnTypeFormattingParams,
+) -> Result<Option<Vec<lsp_ext::SnippetTextEdit>>> {
+ let _p = profile::span("handle_on_type_formatting");
+ let mut position = from_proto::file_position(&snap, params.text_document_position)?;
+ let line_index = snap.file_line_index(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');
+
+ let text = snap.analysis.file_text(position.file_id)?;
+ if stdx::never!(!text[usize::from(position.offset)..].starts_with(char_typed)) {
+ return Ok(None);
+ }
+
+ // 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, snap.config.typing_autoclose_angle())?;
+ let edit = match edit {
+ Some(it) => it,
+ None => return Ok(None),
+ };
+
+ // This should be a single-file edit
+ let (_, text_edit) = edit.source_file_edits.into_iter().next().unwrap();
+
+ let change = to_proto::snippet_text_edit_vec(&line_index, edit.is_snippet, text_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.file_line_index(file_id)?;
+
+ let mut parents: Vec<(lsp_types::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 = lsp_types::DocumentSymbol {
+ name: symbol.label,
+ detail: symbol.detail,
+ kind: to_proto::structure_node_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.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: &lsp_types::DocumentSymbol,
+ container_name: Option<String>,
+ url: &Url,
+ res: &mut Vec<SymbolInformation>,
+ ) {
+ let mut tags = Vec::new();
+
+ #[allow(deprecated)]
+ if let Some(true) = symbol.deprecated {
+ 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: WorkspaceSymbolParams,
+) -> Result<Option<Vec<SymbolInformation>>> {
+ let _p = profile::span("handle_workspace_symbol");
+
+ let config = snap.config.workspace_symbol();
+ let (all_symbols, libs) = decide_search_scope_and_kind(&params, &config);
+ let limit = config.search_limit;
+
+ 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(limit);
+ q
+ };
+ let mut res = exec_query(&snap, query)?;
+ if res.is_empty() && !all_symbols {
+ let mut query = Query::new(params.query);
+ query.limit(limit);
+ res = exec_query(&snap, query)?;
+ }
+
+ return Ok(Some(res));
+
+ fn decide_search_scope_and_kind(
+ params: &WorkspaceSymbolParams,
+ config: &WorkspaceSymbolConfig,
+ ) -> (bool, bool) {
+ // Support old-style parsing of markers in the query.
+ let mut all_symbols = params.query.contains('#');
+ let mut libs = params.query.contains('*');
+
+ // If no explicit marker was set, check request params. If that's also empty
+ // use global config.
+ if !all_symbols {
+ let search_kind = match params.search_kind {
+ Some(ref search_kind) => search_kind,
+ None => &config.search_kind,
+ };
+ all_symbols = match search_kind {
+ lsp_ext::WorkspaceSymbolSearchKind::OnlyTypes => false,
+ lsp_ext::WorkspaceSymbolSearchKind::AllSymbols => true,
+ }
+ }
+
+ if !libs {
+ let search_scope = match params.search_scope {
+ Some(ref search_scope) => search_scope,
+ None => &config.search_scope,
+ };
+ libs = match search_scope {
+ lsp_ext::WorkspaceSymbolSearchScope::Workspace => false,
+ lsp_ext::WorkspaceSymbolSearchScope::WorkspaceAndDependencies => true,
+ }
+ }
+
+ (all_symbols, libs)
+ }
+
+ 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 => {
+ if from_path.is_dir() {
+ // add '/' to end of url -- from `file://path/to/folder` to `file://path/to/folder/`
+ let mut old_folder_name = from_path.file_stem()?.to_str()?.to_string();
+ old_folder_name.push('/');
+ let from_with_trailing_slash = from.join(&old_folder_name).ok()?;
+
+ let imitate_from_url = from_with_trailing_slash.join("mod.rs").ok()?;
+ let new_file_name = to_path.file_name()?.to_str()?;
+ Some((
+ snap.url_to_file_id(&imitate_from_url).ok()?,
+ new_file_name.to_string(),
+ ))
+ } else {
+ let old_name = from_path.file_stem()?.to_str()?;
+ let new_name = to_path.file_stem()?.to_str()?;
+ match (old_name, new_name) {
+ ("mod", _) => None,
+ (_, "mod") => None,
+ _ => 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 mut source_changes = source_changes.into_iter();
+ let mut source_change = source_changes.next().unwrap_or_default();
+ source_change.file_system_edits.clear();
+ // no collect here because we want to merge text edits on same file ids
+ source_change.extend(source_changes.flat_map(|it| it.source_file_edits));
+ if source_change.source_file_edits.is_empty() {
+ Ok(None)
+ } else {
+ Ok(Some(to_proto::workspace_edit(&snap, source_change)?))
+ }
+}
+
+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_declaration(
+ snap: GlobalStateSnapshot,
+ params: lsp_types::request::GotoDeclarationParams,
+) -> Result<Option<lsp_types::request::GotoDeclarationResponse>> {
+ let _p = profile::span("handle_goto_declaration");
+ let position = from_proto::file_position(&snap, params.text_document_position_params.clone())?;
+ let nav_info = match snap.analysis.goto_declaration(position)? {
+ None => return handle_goto_definition(snap, params),
+ 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");
+ if let Ok(file_path) = &params.text_document.uri.to_file_path() {
+ if file_path.file_name().unwrap_or_default() == "Cargo.toml" {
+ // search workspaces for parent packages or fallback to workspace root
+ let abs_path_buf = match AbsPathBuf::try_from(file_path.to_path_buf()).ok() {
+ Some(abs_path_buf) => abs_path_buf,
+ None => return Ok(None),
+ };
+
+ let manifest_path = match ManifestPath::try_from(abs_path_buf).ok() {
+ Some(manifest_path) => manifest_path,
+ None => return Ok(None),
+ };
+
+ let links: Vec<LocationLink> = snap
+ .workspaces
+ .iter()
+ .filter_map(|ws| match ws {
+ ProjectWorkspace::Cargo { cargo, .. } => cargo.parent_manifests(&manifest_path),
+ _ => None,
+ })
+ .flatten()
+ .map(|parent_manifest_path| LocationLink {
+ origin_selection_range: None,
+ target_uri: to_proto::url_from_abs_path(&parent_manifest_path),
+ target_range: Range::default(),
+ target_selection_range: Range::default(),
+ })
+ .collect::<_>();
+ return Ok(Some(links.into()));
+ }
+
+ // check if invoked at the crate root
+ let file_id = from_proto::file_id(&snap, &params.text_document.uri)?;
+ let crate_id = match snap.analysis.crates_for(file_id)?.first() {
+ Some(&crate_id) => crate_id,
+ None => return Ok(None),
+ };
+ let cargo_spec = match CargoTargetSpec::for_file(&snap, file_id)? {
+ Some(it) => it,
+ None => return Ok(None),
+ };
+
+ if snap.analysis.crate_root(crate_id)? == file_id {
+ let cargo_toml_url = to_proto::url_from_abs_path(&cargo_spec.cargo_toml);
+ let res = vec![LocationLink {
+ origin_selection_range: None,
+ target_uri: cargo_toml_url,
+ target_range: Range::default(),
+ target_selection_range: Range::default(),
+ }]
+ .into();
+ return Ok(Some(res));
+ }
+ }
+
+ // locate parent module by semantics
+ 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.file_line_index(file_id)?;
+ let offset = params.position.and_then(|it| from_proto::offset(&line_index, it).ok());
+ 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 should_skip_for_offset(&runnable, offset) {
+ continue;
+ }
+ if should_skip_target(&runnable, cargo_spec.as_ref()) {
+ continue;
+ }
+ let mut runnable = to_proto::runnable(&snap, 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"] {
+ res.push(lsp_ext::Runnable {
+ label: format!("cargo {cmd} -p {} --all-targets", 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 => {
+ if !snap.config.linked_projects().is_empty() {
+ 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,
+ cargo_args: vec!["check".to_string(), "--workspace".to_string()],
+ cargo_extra_args: config.cargo_extra_args,
+ executable_args: Vec::new(),
+ expect_test: None,
+ },
+ });
+ }
+ }
+ }
+ Ok(res)
+}
+
+fn should_skip_for_offset(runnable: &Runnable, offset: Option<TextSize>) -> bool {
+ match offset {
+ None => false,
+ _ if matches!(&runnable.kind, RunnableKind::TestMod { .. }) => false,
+ Some(offset) => !runnable.nav.full_range.contains_inclusive(offset),
+ }
+}
+
+pub(crate) fn handle_related_tests(
+ snap: GlobalStateSnapshot,
+ params: lsp_types::TextDocumentPositionParams,
+) -> Result<Vec<lsp_ext::TestInfo>> {
+ let _p = profile::span("handle_related_tests");
+ let position = from_proto::file_position(&snap, params)?;
+
+ let tests = snap.analysis.related_tests(position, None)?;
+ let mut res = Vec::new();
+ for it in tests {
+ if let Ok(runnable) = to_proto::runnable(&snap, it) {
+ res.push(lsp_ext::TestInfo { runnable })
+ }
+ }
+
+ 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_trigger_character =
+ params.context.and_then(|ctx| ctx.trigger_character).and_then(|s| s.chars().next());
+
+ let completion_config = &snap.config.completion();
+ let items = match snap.analysis.completions(
+ completion_config,
+ position,
+ completion_trigger_character,
+ )? {
+ None => return Ok(None),
+ Some(items) => items,
+ };
+ let line_index = snap.file_line_index(position.file_id)?;
+
+ let items =
+ to_proto::completion_items(&snap.config, &line_index, text_document_position, items);
+
+ 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(invalid_params_error(
+ "Received a completion with overlapping edits, this is not LSP-compliant".to_string(),
+ )
+ .into());
+ }
+
+ let data = match original_completion.data.take() {
+ Some(it) => it,
+ None => return Ok(original_completion),
+ };
+
+ let resolve_data: lsp_ext::CompletionResolveData = serde_json::from_value(data)?;
+
+ let file_id = from_proto::file_id(&snap, &resolve_data.position.text_document.uri)?;
+ let line_index = snap.file_line_index(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
+ .imports
+ .into_iter()
+ .map(|import| (import.full_import_path, import.imported_name)),
+ )?
+ .into_iter()
+ .flat_map(|edit| edit.into_iter().map(|indel| to_proto::text_edit(&line_index, 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.file_line_index(file_id)?;
+ let line_folding_only = snap.config.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 help = match snap.analysis.signature_help(position)? {
+ Some(it) => it,
+ None => return Ok(None),
+ };
+ let config = snap.config.call_info();
+ let res = to_proto::signature_help(help, config, snap.config.signature_help_label_offsets());
+ Ok(Some(res))
+}
+
+pub(crate) fn handle_hover(
+ snap: GlobalStateSnapshot,
+ params: lsp_ext::HoverParams,
+) -> Result<Option<lsp_ext::Hover>> {
+ let _p = profile::span("handle_hover");
+ let range = match params.position {
+ PositionOrRange::Position(position) => Range::new(position, position),
+ PositionOrRange::Range(range) => range,
+ };
+
+ let file_range = from_proto::file_range(&snap, params.text_document, range)?;
+ let info = match snap.analysis.hover(&snap.config.hover(), file_range)? {
+ None => return Ok(None),
+ Some(info) => info,
+ };
+
+ let line_index = snap.file_line_index(file_range.file_id)?;
+ let range = to_proto::range(&line_index, info.range);
+ let markup_kind = snap.config.hover().format;
+ let hover = lsp_ext::Hover {
+ hover: lsp_types::Hover {
+ contents: HoverContents::Markup(to_proto::markup_content(
+ info.info.markup,
+ markup_kind,
+ )),
+ range: Some(range),
+ },
+ actions: if snap.config.hover_actions().none() {
+ Vec::new()
+ } else {
+ prepare_hover_actions(&snap, &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)?.map_err(to_proto::rename_error)?;
+
+ let line_index = snap.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)?;
+
+ let mut change =
+ snap.analysis.rename(position, &params.new_name)?.map_err(to_proto::rename_error)?;
+
+ // this is kind of a hack to prevent double edits from happening when moving files
+ // When a module gets renamed by renaming the mod declaration this causes the file to move
+ // which in turn will trigger a WillRenameFiles request to the server for which we reply with a
+ // a second identical set of renames, the client will then apply both edits causing incorrect edits
+ // with this we only emit source_file_edits in the WillRenameFiles response which will do the rename instead
+ // See https://github.com/microsoft/vscode-languageserver-node/issues/752 for more info
+ if !change.file_system_edits.is_empty() && snap.config.will_rename() {
+ change.source_file_edits.clear();
+ }
+ let workspace_edit = to_proto::workspace_edit(&snap, change)?;
+ 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 exclude_imports = snap.config.find_all_refs_exclude_imports();
+
+ let refs = match snap.analysis.find_all_refs(position, None)? {
+ None => return Ok(None),
+ Some(refs) => refs,
+ };
+
+ let include_declaration = params.context.include_declaration;
+ let locations = refs
+ .into_iter()
+ .flat_map(|refs| {
+ let decl = if include_declaration {
+ refs.declaration.map(|decl| FileRange {
+ file_id: decl.nav.file_id,
+ range: decl.nav.focus_or_full_range(),
+ })
+ } else {
+ None
+ };
+ refs.references
+ .into_iter()
+ .flat_map(|(file_id, refs)| {
+ refs.into_iter()
+ .filter(|&(_, category)| {
+ !exclude_imports || category != Some(ReferenceCategory::Import)
+ })
+ .map(move |(range, _)| FileRange { file_id, range })
+ })
+ .chain(decl)
+ })
+ .filter_map(|frange| to_proto::location(&snap, frange).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");
+
+ run_rustfmt(&snap, params.text_document, None)
+}
+
+pub(crate) fn handle_range_formatting(
+ snap: GlobalStateSnapshot,
+ params: lsp_types::DocumentRangeFormattingParams,
+) -> Result<Option<Vec<lsp_types::TextEdit>>> {
+ let _p = profile::span("handle_range_formatting");
+
+ run_rustfmt(&snap, params.text_document, Some(params.range))
+}
+
+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");
+
+ if !snap.config.code_action_literals() {
+ // We intentionally don't support command-based actions, as those either
+ // require either custom client-code or server-initiated edits. Server
+ // initiated edits break causality, so we avoid those.
+ return Ok(None);
+ }
+
+ let line_index =
+ snap.file_line_index(from_proto::file_id(&snap, &params.text_document.uri)?)?;
+ let frange = from_proto::file_range(&snap, params.text_document.clone(), params.range)?;
+
+ let mut assists_config = snap.config.assist();
+ assists_config.allowed = params
+ .context
+ .only
+ .clone()
+ .map(|it| it.into_iter().filter_map(from_proto::assist_kind).collect());
+
+ let mut res: Vec<lsp_ext::CodeAction> = Vec::new();
+
+ let code_action_resolve_cap = snap.config.code_action_resolve();
+ let resolve = if code_action_resolve_cap {
+ AssistResolveStrategy::None
+ } else {
+ AssistResolveStrategy::All
+ };
+ let assists = snap.analysis.assists_with_fixes(
+ &assists_config,
+ &snap.config.diagnostics(),
+ resolve,
+ frange,
+ )?;
+ for (index, assist) in assists.into_iter().enumerate() {
+ let resolve_data =
+ if code_action_resolve_cap { Some((index, params.clone())) } else { None };
+ let code_action = to_proto::code_action(&snap, assist, resolve_data)?;
+ res.push(code_action)
+ }
+
+ // Fixes from `cargo check`.
+ for fix in snap.check_fixes.values().filter_map(|it| it.get(&frange.file_id)).flatten() {
+ // FIXME: this mapping is awkward and shouldn't exist. Refactor
+ // `snap.check_fixes` to not convert to LSP prematurely.
+ let intersect_fix_range = fix
+ .ranges
+ .iter()
+ .copied()
+ .filter_map(|range| from_proto::text_range(&line_index, range).ok())
+ .any(|fix_range| fix_range.intersect(frange.range).is_some());
+ if intersect_fix_range {
+ res.push(fix.action.clone());
+ }
+ }
+
+ Ok(Some(res))
+}
+
+pub(crate) fn handle_code_action_resolve(
+ 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 => return Err(invalid_params_error("code action without data".to_string()).into()),
+ };
+
+ let file_id = from_proto::file_id(&snap, &params.code_action_params.text_document.uri)?;
+ let line_index = snap.file_line_index(file_id)?;
+ let range = from_proto::text_range(&line_index, params.code_action_params.range)?;
+ let frange = FileRange { file_id, range };
+
+ let mut assists_config = snap.config.assist();
+ assists_config.allowed = params
+ .code_action_params
+ .context
+ .only
+ .map(|it| it.into_iter().filter_map(from_proto::assist_kind).collect());
+
+ let (assist_index, assist_resolve) = match parse_action_id(&params.id) {
+ Ok(parsed_data) => parsed_data,
+ Err(e) => {
+ return Err(invalid_params_error(format!(
+ "Failed to parse action id string '{}': {e}",
+ params.id
+ ))
+ .into())
+ }
+ };
+
+ let expected_assist_id = assist_resolve.assist_id.clone();
+ let expected_kind = assist_resolve.assist_kind;
+
+ let assists = snap.analysis.assists_with_fixes(
+ &assists_config,
+ &snap.config.diagnostics(),
+ AssistResolveStrategy::Single(assist_resolve),
+ frange,
+ )?;
+
+ let assist = match assists.get(assist_index) {
+ Some(assist) => assist,
+ None => return Err(invalid_params_error(format!(
+ "Failed to find the assist for index {} provided by the resolve request. Resolve request assist id: {}",
+ assist_index, params.id,
+ ))
+ .into())
+ };
+ if assist.id.0 != expected_assist_id || assist.id.1 != expected_kind {
+ return Err(invalid_params_error(format!(
+ "Mismatching assist at index {} for the resolve parameters given. Resolve request assist id: {}, actual id: {:?}.",
+ assist_index, params.id, assist.id
+ ))
+ .into());
+ }
+ let ca = to_proto::code_action(&snap, assist.clone(), None)?;
+ code_action.edit = ca.edit;
+ code_action.command = ca.command;
+ Ok(code_action)
+}
+
+fn parse_action_id(action_id: &str) -> Result<(usize, SingleResolve), String> {
+ let id_parts = action_id.split(':').collect::<Vec<_>>();
+ match id_parts.as_slice() {
+ [assist_id_string, assist_kind_string, index_string] => {
+ let assist_kind: AssistKind = assist_kind_string.parse()?;
+ let index: usize = match index_string.parse() {
+ Ok(index) => index,
+ Err(e) => return Err(format!("Incorrect index string: {e}")),
+ };
+ Ok((index, SingleResolve { assist_id: assist_id_string.to_string(), assist_kind }))
+ }
+ _ => Err("Action id contains incorrect number of segments".to_string()),
+ }
+}
+
+pub(crate) fn handle_code_lens(
+ snap: GlobalStateSnapshot,
+ params: lsp_types::CodeLensParams,
+) -> Result<Option<Vec<CodeLens>>> {
+ let _p = profile::span("handle_code_lens");
+
+ let lens_config = snap.config.lens();
+ if lens_config.none() {
+ // early return before any db query!
+ return Ok(Some(Vec::default()));
+ }
+
+ let file_id = from_proto::file_id(&snap, &params.text_document.uri)?;
+ let cargo_target_spec = CargoTargetSpec::for_file(&snap, file_id)?;
+
+ let annotations = snap.analysis.annotations(
+ &AnnotationConfig {
+ binary_target: cargo_target_spec
+ .map(|spec| {
+ matches!(
+ spec.target_kind,
+ TargetKind::Bin | TargetKind::Example | TargetKind::Test
+ )
+ })
+ .unwrap_or(false),
+ annotate_runnables: lens_config.runnable(),
+ annotate_impls: lens_config.implementations,
+ annotate_references: lens_config.refs_adt,
+ annotate_method_references: lens_config.method_refs,
+ annotate_enum_variant_references: lens_config.enum_variant_refs,
+ location: lens_config.location.into(),
+ },
+ file_id,
+ )?;
+
+ let mut res = Vec::new();
+ for a in annotations {
+ to_proto::code_lens(&mut res, &snap, a)?;
+ }
+
+ Ok(Some(res))
+}
+
+pub(crate) fn handle_code_lens_resolve(
+ snap: GlobalStateSnapshot,
+ code_lens: CodeLens,
+) -> Result<CodeLens> {
+ let Some(annotation) = from_proto::annotation(&snap, code_lens.clone())? else { return Ok(code_lens) };
+ let annotation = snap.analysis.resolve_annotation(annotation)?;
+
+ let mut acc = Vec::new();
+ to_proto::code_lens(&mut acc, &snap, annotation)?;
+
+ let res = match acc.pop() {
+ Some(it) if acc.is_empty() => it,
+ _ => {
+ never!();
+ code_lens
+ }
+ };
+
+ Ok(res)
+}
+
+pub(crate) fn handle_document_highlight(
+ snap: GlobalStateSnapshot,
+ params: lsp_types::DocumentHighlightParams,
+) -> Result<Option<Vec<lsp_types::DocumentHighlight>>> {
+ let _p = profile::span("handle_document_highlight");
+ let position = from_proto::file_position(&snap, params.text_document_position_params)?;
+ let line_index = snap.file_line_index(position.file_id)?;
+
+ let refs = match snap.analysis.highlight_related(snap.config.highlight_related(), position)? {
+ None => return Ok(None),
+ Some(refs) => refs,
+ };
+ let res = refs
+ .into_iter()
+ .map(|ide::HighlightedRange { range, category }| lsp_types::DocumentHighlight {
+ range: to_proto::range(&line_index, range),
+ kind: category.and_then(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).map_err(Into::into)
+}
+
+pub(crate) fn handle_inlay_hints(
+ snap: GlobalStateSnapshot,
+ params: InlayHintParams,
+) -> Result<Option<Vec<InlayHint>>> {
+ let _p = profile::span("handle_inlay_hints");
+ let document_uri = &params.text_document.uri;
+ let FileRange { file_id, range } = from_proto::file_range(
+ &snap,
+ TextDocumentIdentifier::new(document_uri.to_owned()),
+ params.range,
+ )?;
+ let line_index = snap.file_line_index(file_id)?;
+ let inlay_hints_config = snap.config.inlay_hints();
+ Ok(Some(
+ snap.analysis
+ .inlay_hints(&inlay_hints_config, file_id, Some(range))?
+ .into_iter()
+ .map(|it| {
+ to_proto::inlay_hint(&snap, &line_index, inlay_hints_config.render_colons, it)
+ })
+ .collect::<Cancellable<Vec<_>>>()?,
+ ))
+}
+
+pub(crate) fn handle_inlay_hints_resolve(
+ _snap: GlobalStateSnapshot,
+ hint: InlayHint,
+) -> Result<InlayHint> {
+ let _p = profile::span("handle_inlay_hints_resolve");
+ Ok(hint)
+}
+
+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::<Cancellable<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.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.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.file_line_index(file_id)?;
+
+ let mut highlight_config = snap.config.highlighting_config();
+ // Avoid flashing a bunch of unresolved references when the proc-macro servers haven't been spawned yet.
+ highlight_config.syntactic_name_ref_highlighting =
+ snap.workspaces.is_empty() || !snap.proc_macros_loaded;
+
+ let highlights = snap.analysis.highlight(highlight_config, 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.file_line_index(file_id)?;
+
+ let mut highlight_config = snap.config.highlighting_config();
+ // Avoid flashing a bunch of unresolved references when the proc-macro servers haven't been spawned yet.
+ highlight_config.syntactic_name_ref_highlighting =
+ snap.workspaces.is_empty() || !snap.proc_macros_loaded;
+
+ let highlights = snap.analysis.highlight(highlight_config, 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.file_line_index(frange.file_id)?;
+
+ let mut highlight_config = snap.config.highlighting_config();
+ // Avoid flashing a bunch of unresolved references when the proc-macro servers haven't been spawned yet.
+ highlight_config.syntactic_name_ref_highlighting =
+ snap.workspaces.is_empty() || !snap.proc_macros_loaded;
+
+ let highlights = snap.analysis.highlight_range(highlight_config, 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))
+}
+
+pub(crate) fn handle_move_item(
+ snap: GlobalStateSnapshot,
+ params: lsp_ext::MoveItemParams,
+) -> Result<Vec<lsp_ext::SnippetTextEdit>> {
+ let _p = profile::span("handle_move_item");
+ let file_id = from_proto::file_id(&snap, &params.text_document.uri)?;
+ let range = from_proto::file_range(&snap, params.text_document, params.range)?;
+
+ let direction = match params.direction {
+ lsp_ext::MoveItemDirection::Up => ide::Direction::Up,
+ lsp_ext::MoveItemDirection::Down => ide::Direction::Down,
+ };
+
+ match snap.analysis.move_item(range, direction)? {
+ Some(text_edit) => {
+ let line_index = snap.file_line_index(file_id)?;
+ Ok(to_proto::snippet_text_edit_vec(&line_index, true, text_edit))
+ }
+ None => Ok(vec![]),
+ }
+}
+
+fn to_command_link(command: lsp_types::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_actions().implementations && snap.config.client_commands().show_reference {
+ 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.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 = to_proto::implementation_title(locations.len());
+ let command = to_proto::command::show_references(title, &uri, position, locations);
+
+ return Some(lsp_ext::CommandLinkGroup {
+ commands: vec![to_command_link(command, "Go to implementations".into())],
+ ..Default::default()
+ });
+ }
+ }
+ None
+}
+
+fn show_ref_command_link(
+ snap: &GlobalStateSnapshot,
+ position: &FilePosition,
+) -> Option<lsp_ext::CommandLinkGroup> {
+ if snap.config.hover_actions().references && snap.config.client_commands().show_reference {
+ if let Some(ref_search_res) = snap.analysis.find_all_refs(*position, None).unwrap_or(None) {
+ let uri = to_proto::url(snap, position.file_id);
+ let line_index = snap.file_line_index(position.file_id).ok()?;
+ let position = to_proto::position(&line_index, position.offset);
+ let locations: Vec<_> = ref_search_res
+ .into_iter()
+ .flat_map(|res| res.references)
+ .flat_map(|(file_id, ranges)| {
+ ranges.into_iter().filter_map(move |(range, _)| {
+ to_proto::location(snap, FileRange { file_id, range }).ok()
+ })
+ })
+ .collect();
+ let title = to_proto::reference_title(locations.len());
+ let command = to_proto::command::show_references(title, &uri, position, locations);
+
+ return Some(lsp_ext::CommandLinkGroup {
+ commands: vec![to_command_link(command, "Go to references".into())],
+ ..Default::default()
+ });
+ }
+ }
+ None
+}
+
+fn runnable_action_links(
+ snap: &GlobalStateSnapshot,
+ runnable: Runnable,
+) -> Option<lsp_ext::CommandLinkGroup> {
+ let hover_actions_config = snap.config.hover_actions();
+ if !hover_actions_config.runnable() {
+ return None;
+ }
+
+ let cargo_spec = CargoTargetSpec::for_file(snap, runnable.nav.file_id).ok()?;
+ if should_skip_target(&runnable, cargo_spec.as_ref()) {
+ return None;
+ }
+
+ let client_commands_config = snap.config.client_commands();
+ if !(client_commands_config.run_single || client_commands_config.debug_single) {
+ return None;
+ }
+
+ let title = runnable.title();
+ let r = to_proto::runnable(snap, runnable).ok()?;
+
+ let mut group = lsp_ext::CommandLinkGroup::default();
+
+ if hover_actions_config.run && client_commands_config.run_single {
+ let run_command = to_proto::command::run_single(&r, &title);
+ group.commands.push(to_command_link(run_command, r.label.clone()));
+ }
+
+ if hover_actions_config.debug && client_commands_config.debug_single {
+ let dbg_command = to_proto::command::debug_single(&r);
+ group.commands.push(to_command_link(dbg_command, r.label));
+ }
+
+ Some(group)
+}
+
+fn goto_type_action_links(
+ snap: &GlobalStateSnapshot,
+ nav_targets: &[HoverGotoTypeData],
+) -> Option<lsp_ext::CommandLinkGroup> {
+ if !snap.config.hover_actions().goto_type_def
+ || nav_targets.is_empty()
+ || !snap.config.client_commands().goto_location
+ {
+ return None;
+ }
+
+ Some(lsp_ext::CommandLinkGroup {
+ title: Some("Go to ".into()),
+ commands: nav_targets
+ .iter()
+ .filter_map(|it| {
+ to_proto::command::goto_location(snap, &it.nav)
+ .map(|cmd| to_command_link(cmd, it.mod_path.clone()))
+ })
+ .collect(),
+ })
+}
+
+fn prepare_hover_actions(
+ snap: &GlobalStateSnapshot,
+ actions: &[HoverAction],
+) -> Vec<lsp_ext::CommandLinkGroup> {
+ actions
+ .iter()
+ .filter_map(|it| match it {
+ HoverAction::Implementation(position) => show_impl_command_link(snap, position),
+ HoverAction::Reference(position) => show_ref_command_link(snap, position),
+ HoverAction::Runnable(r) => runnable_action_links(snap, 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,
+ }
+}
+
+fn run_rustfmt(
+ snap: &GlobalStateSnapshot,
+ text_document: TextDocumentIdentifier,
+ range: Option<lsp_types::Range>,
+) -> Result<Option<Vec<lsp_types::TextEdit>>> {
+ let file_id = from_proto::file_id(snap, &text_document.uri)?;
+ let file = snap.analysis.file_text(file_id)?;
+
+ // Determine the edition of the crate the file belongs to (if there's multiple, we pick the
+ // highest edition).
+ let editions = snap
+ .analysis
+ .relevant_crates_for(file_id)?
+ .into_iter()
+ .map(|crate_id| snap.analysis.crate_edition(crate_id))
+ .collect::<Result<Vec<_>, _>>()?;
+ let edition = editions.iter().copied().max();
+
+ let line_index = snap.file_line_index(file_id)?;
+
+ let mut command = match snap.config.rustfmt() {
+ RustfmtConfig::Rustfmt { extra_args, enable_range_formatting } => {
+ let mut cmd = process::Command::new(toolchain::rustfmt());
+ cmd.envs(snap.config.extra_env());
+ cmd.args(extra_args);
+ // try to chdir to the file so we can respect `rustfmt.toml`
+ // FIXME: use `rustfmt --config-path` once
+ // https://github.com/rust-lang/rustfmt/issues/4660 gets fixed
+ match text_document.uri.to_file_path() {
+ Ok(mut path) => {
+ // pop off file name
+ if path.pop() && path.is_dir() {
+ cmd.current_dir(path);
+ }
+ }
+ Err(_) => {
+ tracing::error!(
+ "Unable to get file path for {}, rustfmt.toml might be ignored",
+ text_document.uri
+ );
+ }
+ }
+ if let Some(edition) = edition {
+ cmd.arg("--edition");
+ cmd.arg(edition.to_string());
+ }
+
+ if let Some(range) = range {
+ if !enable_range_formatting {
+ return Err(LspError::new(
+ ErrorCode::InvalidRequest as i32,
+ String::from(
+ "rustfmt range formatting is unstable. \
+ Opt-in by using a nightly build of rustfmt and setting \
+ `rustfmt.rangeFormatting.enable` to true in your LSP configuration",
+ ),
+ )
+ .into());
+ }
+
+ let frange = from_proto::file_range(snap, text_document, range)?;
+ let start_line = line_index.index.line_col(frange.range.start()).line;
+ let end_line = line_index.index.line_col(frange.range.end()).line;
+
+ cmd.arg("--unstable-features");
+ cmd.arg("--file-lines");
+ cmd.arg(
+ json!([{
+ "file": "stdin",
+ "range": [start_line, end_line]
+ }])
+ .to_string(),
+ );
+ }
+
+ cmd
+ }
+ RustfmtConfig::CustomCommand { command, args } => {
+ let mut cmd = process::Command::new(command);
+ cmd.envs(snap.config.extra_env());
+ cmd.args(args);
+ cmd
+ }
+ };
+
+ let mut rustfmt = command
+ .stdin(Stdio::piped())
+ .stdout(Stdio::piped())
+ .stderr(Stdio::piped())
+ .spawn()
+ .context(format!("Failed to spawn {command:?}"))?;
+
+ 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() {
+ let rustfmt_not_installed =
+ captured_stderr.contains("not installed") || captured_stderr.contains("not available");
+
+ return match output.status.code() {
+ Some(1) if !rustfmt_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.
+ tracing::warn!(
+ ?command,
+ %captured_stderr,
+ "rustfmt exited with status 1"
+ );
+ Ok(None)
+ }
+ _ => {
+ // Something else happened - e.g. `rustfmt` is missing or caught a signal
+ Err(LspError::new(
+ -32900,
+ format!(
+ r#"rustfmt exited with:
+ Status: {}
+ stdout: {captured_stdout}
+ stderr: {captured_stderr}"#,
+ output.status,
+ ),
+ )
+ .into())
+ }
+ };
+ }
+
+ let (new_text, new_line_endings) = LineEndings::normalize(captured_stdout);
+
+ if line_index.endings != new_line_endings {
+ // If line endings are different, send the entire file.
+ // Diffing would not work here, as the line endings might be the only
+ // difference.
+ Ok(Some(to_proto::text_edit_vec(
+ &line_index,
+ TextEdit::replace(TextRange::up_to(TextSize::of(&*file)), new_text),
+ )))
+ } else if *file == new_text {
+ // The document is already formatted correctly -- no edits needed.
+ Ok(None)
+ } else {
+ Ok(Some(to_proto::text_edit_vec(&line_index, diff(&file, &new_text))))
+ }
+}
diff --git a/crates/rust-analyzer/src/main_loop.rs b/crates/rust-analyzer/src/main_loop.rs
index 6f31c64122..dc0ea0b17e 100644
--- a/crates/rust-analyzer/src/main_loop.rs
+++ b/crates/rust-analyzer/src/main_loop.rs
@@ -2,7 +2,6 @@
//! requests/replies and notifications back to the client.
use std::{
fmt,
- ops::Deref,
sync::Arc,
time::{Duration, Instant},
};
@@ -11,20 +10,18 @@ use always_assert::always;
use crossbeam_channel::{select, Receiver};
use flycheck::FlycheckHandle;
use ide_db::base_db::{SourceDatabaseExt, VfsPath};
-use itertools::Itertools;
use lsp_server::{Connection, Notification, Request};
use lsp_types::notification::Notification as _;
-use vfs::{AbsPathBuf, ChangeKind, FileId};
+use vfs::FileId;
use crate::{
config::Config,
dispatch::{NotificationDispatcher, RequestDispatcher},
from_proto,
global_state::{file_id_to_url, url_to_file_id, GlobalState},
- handlers, lsp_ext,
- lsp_utils::{apply_document_changes, notification_is, Progress},
- mem_docs::DocumentData,
- reload::{self, BuildDataProgress, ProcMacroProgress, ProjectWorkspaceProgress},
+ lsp_ext,
+ lsp_utils::{notification_is, Progress},
+ reload::{BuildDataProgress, ProcMacroProgress, ProjectWorkspaceProgress},
Result,
};
@@ -652,6 +649,8 @@ impl GlobalState {
_ => (),
}
+ use crate::handlers::request as handlers;
+
dispatcher
.on_sync_mut::<lsp_ext::ReloadWorkspace>(handlers::handle_workspace_reload)
.on_sync_mut::<lsp_ext::RebuildProcMacros>(handlers::handle_proc_macros_rebuild)
@@ -723,284 +722,22 @@ impl GlobalState {
/// Handles an incoming notification.
fn on_notification(&mut self, not: Notification) -> Result<()> {
- // FIXME: Move these implementations out into a module similar to on_request
- fn run_flycheck(this: &mut GlobalState, vfs_path: VfsPath) -> bool {
- let file_id = this.vfs.read().0.file_id(&vfs_path);
- if let Some(file_id) = file_id {
- let world = this.snapshot();
- let mut updated = false;
- let task = move || -> std::result::Result<(), ide::Cancelled> {
- // Trigger flychecks for all workspaces that depend on the saved file
- // Crates containing or depending on the saved file
- let crate_ids: Vec<_> = world
- .analysis
- .crates_for(file_id)?
- .into_iter()
- .flat_map(|id| world.analysis.transitive_rev_deps(id))
- .flatten()
- .sorted()
- .unique()
- .collect();
-
- let crate_root_paths: Vec<_> = crate_ids
- .iter()
- .filter_map(|&crate_id| {
- world
- .analysis
- .crate_root(crate_id)
- .map(|file_id| {
- world
- .file_id_to_file_path(file_id)
- .as_path()
- .map(ToOwned::to_owned)
- })
- .transpose()
- })
- .collect::<ide::Cancellable<_>>()?;
- let crate_root_paths: Vec<_> =
- crate_root_paths.iter().map(Deref::deref).collect();
-
- // Find all workspaces that have at least one target containing the saved file
- let workspace_ids =
- world.workspaces.iter().enumerate().filter(|(_, ws)| match ws {
- project_model::ProjectWorkspace::Cargo { cargo, .. } => {
- cargo.packages().any(|pkg| {
- cargo[pkg].targets.iter().any(|&it| {
- crate_root_paths.contains(&cargo[it].root.as_path())
- })
- })
- }
- project_model::ProjectWorkspace::Json { project, .. } => project
- .crates()
- .any(|(c, _)| crate_ids.iter().any(|&crate_id| crate_id == c)),
- project_model::ProjectWorkspace::DetachedFiles { .. } => false,
- });
-
- // Find and trigger corresponding flychecks
- for flycheck in world.flycheck.iter() {
- for (id, _) in workspace_ids.clone() {
- if id == flycheck.id() {
- updated = true;
- flycheck.restart();
- continue;
- }
- }
- }
- // No specific flycheck was triggered, so let's trigger all of them.
- if !updated {
- for flycheck in world.flycheck.iter() {
- flycheck.restart();
- }
- }
- Ok(())
- };
- this.task_pool.handle.spawn_with_sender(move |_| {
- if let Err(e) = std::panic::catch_unwind(task) {
- tracing::error!("flycheck task panicked: {e:?}")
- }
- });
- true
- } else {
- false
- }
- }
+ use crate::handlers::notification as handlers;
+ use lsp_types::notification as notifs;
NotificationDispatcher { not: Some(not), global_state: self }
- .on::<lsp_types::notification::Cancel>(|this, params| {
- let id: lsp_server::RequestId = match params.id {
- lsp_types::NumberOrString::Number(id) => id.into(),
- lsp_types::NumberOrString::String(id) => id.into(),
- };
- this.cancel(id);
- Ok(())
- })?
- .on::<lsp_types::notification::WorkDoneProgressCancel>(|this, params| {
- if let lsp_types::NumberOrString::String(s) = &params.token {
- if let Some(id) = s.strip_prefix("rust-analyzer/flycheck/") {
- if let Ok(id) = u32::from_str_radix(id, 10) {
- if let Some(flycheck) = this.flycheck.get(id as usize) {
- flycheck.cancel();
- }
- }
- }
- }
- // Just ignore this. It is OK to continue sending progress
- // notifications for this token, as the client can't know when
- // we accepted notification.
- Ok(())
- })?
- .on::<lsp_types::notification::DidOpenTextDocument>(|this, params| {
- if let Ok(path) = from_proto::vfs_path(&params.text_document.uri) {
- let already_exists = this
- .mem_docs
- .insert(path.clone(), DocumentData::new(params.text_document.version))
- .is_err();
- if already_exists {
- tracing::error!("duplicate DidOpenTextDocument: {}", path);
- }
- this.vfs
- .write()
- .0
- .set_file_contents(path, Some(params.text_document.text.into_bytes()));
- }
- Ok(())
- })?
+ .on::<notifs::Cancel>(handlers::handle_cancel)?
+ .on::<notifs::WorkDoneProgressCancel>(handlers::handle_work_done_progress_cancel)?
+ .on::<notifs::DidOpenTextDocument>(handlers::handle_did_open_text_document)?
+ .on::<notifs::DidChangeTextDocument>(handlers::handle_did_change_text_document)?
+ .on::<notifs::DidCloseTextDocument>(handlers::handle_did_close_text_document)?
+ .on::<notifs::DidSaveTextDocument>(handlers::handle_did_save_text_document)?
+ .on::<notifs::DidChangeConfiguration>(handlers::handle_did_change_configuration)?
+ .on::<notifs::DidChangeWorkspaceFolders>(handlers::handle_did_change_workspace_folders)?
+ .on::<notifs::DidChangeWatchedFiles>(handlers::handle_did_change_watched_files)?
.on::<lsp_ext::CancelFlycheck>(handlers::handle_cancel_flycheck)?
- .on::<lsp_types::notification::DidChangeTextDocument>(|this, params| {
- if let Ok(path) = from_proto::vfs_path(&params.text_document.uri) {
- match this.mem_docs.get_mut(&path) {
- Some(doc) => {
- // The version passed in DidChangeTextDocument is the version after all edits are applied
- // so we should apply it before the vfs is notified.
- doc.version = params.text_document.version;
- }
- None => {
- tracing::error!("unexpected DidChangeTextDocument: {}", path);
- return Ok(());
- }
- };
-
- let vfs = &mut this.vfs.write().0;
- let file_id = vfs.file_id(&path).unwrap();
- let text = apply_document_changes(
- this.config.position_encoding(),
- || std::str::from_utf8(vfs.file_contents(file_id)).unwrap().into(),
- params.content_changes,
- );
-
- vfs.set_file_contents(path, Some(text.into_bytes()));
- }
- Ok(())
- })?
- .on::<lsp_types::notification::DidCloseTextDocument>(|this, params| {
- if let Ok(path) = from_proto::vfs_path(&params.text_document.uri) {
- if this.mem_docs.remove(&path).is_err() {
- tracing::error!("orphan DidCloseTextDocument: {}", path);
- }
-
- this.semantic_tokens_cache.lock().remove(&params.text_document.uri);
-
- if let Some(path) = path.as_path() {
- this.loader.handle.invalidate(path.to_path_buf());
- }
- }
- Ok(())
- })?
- .on::<lsp_ext::ClearFlycheck>(|this, ()| {
- this.diagnostics.clear_check_all();
- Ok(())
- })?
- .on::<lsp_ext::RunFlycheck>(|this, params| {
- if let Some(text_document) = params.text_document {
- if let Ok(vfs_path) = from_proto::vfs_path(&text_document.uri) {
- if run_flycheck(this, vfs_path) {
- return Ok(());
- }
- }
- }
- // No specific flycheck was triggered, so let's trigger all of them.
- for flycheck in this.flycheck.iter() {
- flycheck.restart();
- }
- Ok(())
- })?
- .on::<lsp_types::notification::DidSaveTextDocument>(|this, params| {
- if let Ok(vfs_path) = from_proto::vfs_path(&params.text_document.uri) {
- // Re-fetch workspaces if a workspace related file has changed
- if let Some(abs_path) = vfs_path.as_path() {
- if reload::should_refresh_for_change(abs_path, ChangeKind::Modify) {
- this.fetch_workspaces_queue.request_op(
- format!("DidSaveTextDocument {}", abs_path.display()),
- (),
- );
- }
- }
-
- if !this.config.check_on_save() || run_flycheck(this, vfs_path) {
- return Ok(());
- }
- } else if this.config.check_on_save() {
- // No specific flycheck was triggered, so let's trigger all of them.
- for flycheck in this.flycheck.iter() {
- flycheck.restart();
- }
- }
- Ok(())
- })?
- .on::<lsp_types::notification::DidChangeConfiguration>(|this, _params| {
- // As stated in https://github.com/microsoft/language-server-protocol/issues/676,
- // this notification's parameters should be ignored and the actual config queried separately.
- this.send_request::<lsp_types::request::WorkspaceConfiguration>(
- lsp_types::ConfigurationParams {
- items: vec![lsp_types::ConfigurationItem {
- scope_uri: None,
- section: Some("rust-analyzer".to_string()),
- }],
- },
- |this, resp| {
- tracing::debug!("config update response: '{:?}", resp);
- let lsp_server::Response { error, result, .. } = resp;
-
- match (error, result) {
- (Some(err), _) => {
- tracing::error!("failed to fetch the server settings: {:?}", err)
- }
- (None, Some(mut configs)) => {
- if let Some(json) = configs.get_mut(0) {
- // Note that json can be null according to the spec if the client can't
- // provide a configuration. This is handled in Config::update below.
- let mut config = Config::clone(&*this.config);
- if let Err(error) = config.update(json.take()) {
- this.show_message(
- lsp_types::MessageType::WARNING,
- error.to_string(),
- false,
- );
- }
- this.update_configuration(config);
- }
- }
- (None, None) => tracing::error!(
- "received empty server settings response from the client"
- ),
- }
- },
- );
-
- Ok(())
- })?
- .on::<lsp_types::notification::DidChangeWorkspaceFolders>(|this, params| {
- let config = Arc::make_mut(&mut this.config);
-
- for workspace in params.event.removed {
- let Ok(path) = workspace.uri.to_file_path() else { continue };
- let Ok(path) = AbsPathBuf::try_from(path) else { continue };
- config.remove_workspace(&path);
- }
-
- let added = params
- .event
- .added
- .into_iter()
- .filter_map(|it| it.uri.to_file_path().ok())
- .filter_map(|it| AbsPathBuf::try_from(it).ok());
- config.add_workspaces(added);
- if !config.has_linked_projects() && config.detached_files().is_empty() {
- config.rediscover_workspaces();
- this.fetch_workspaces_queue
- .request_op("client workspaces changed".to_string(), ())
- }
-
- Ok(())
- })?
- .on::<lsp_types::notification::DidChangeWatchedFiles>(|this, params| {
- for change in params.changes {
- if let Ok(path) = from_proto::abs_path(&change.uri) {
- this.loader.handle.invalidate(path);
- }
- }
- Ok(())
- })?
+ .on::<lsp_ext::ClearFlycheck>(handlers::handle_clear_flycheck)?
+ .on::<lsp_ext::RunFlycheck>(handlers::handle_run_flycheck)?
.finish();
Ok(())
}
@@ -1029,7 +766,7 @@ impl GlobalState {
let diagnostics = subscriptions
.into_iter()
.filter_map(|file_id| {
- handlers::publish_diagnostics(&snapshot, file_id)
+ crate::handlers::publish_diagnostics(&snapshot, file_id)
.ok()
.map(|diags| (file_id, diags))
})