//! Conversion of rust-analyzer specific types to lsp_types equivalents. use std::{ iter::once, path, sync::atomic::{AtomicU32, Ordering}, }; use ide::{ Annotation, AnnotationKind, Assist, AssistKind, Cancellable, CompletionItem, CompletionItemKind, CompletionRelevance, Documentation, FileId, FileRange, FileSystemEdit, Fold, FoldKind, Highlight, HlMod, HlOperator, HlPunct, HlRange, HlTag, Indel, InlayHint, InlayHintLabel, InlayHintLabelPart, InlayKind, Markup, NavigationTarget, ReferenceCategory, RenameError, Runnable, Severity, SignatureHelp, SourceChange, StructureNodeKind, SymbolKind, TextEdit, TextRange, TextSize, }; use itertools::Itertools; use serde_json::to_value; use vfs::AbsPath; use crate::{ cargo_target_spec::CargoTargetSpec, config::{CallInfoConfig, Config}, global_state::GlobalStateSnapshot, line_index::{LineEndings, LineIndex, PositionEncoding}, lsp_ext, lsp_utils::invalid_params_error, semantic_tokens, }; pub(crate) fn position(line_index: &LineIndex, offset: TextSize) -> lsp_types::Position { let line_col = line_index.index.line_col(offset); match line_index.encoding { PositionEncoding::Utf8 => lsp_types::Position::new(line_col.line, line_col.col), PositionEncoding::Wide(enc) => { let line_col = line_index.index.to_wide(enc, line_col); lsp_types::Position::new(line_col.line, line_col.col) } } } pub(crate) fn range(line_index: &LineIndex, range: TextRange) -> lsp_types::Range { let start = position(line_index, range.start()); let end = position(line_index, range.end()); lsp_types::Range::new(start, end) } pub(crate) fn symbol_kind(symbol_kind: SymbolKind) -> lsp_types::SymbolKind { match symbol_kind { SymbolKind::Function => lsp_types::SymbolKind::FUNCTION, SymbolKind::Struct => lsp_types::SymbolKind::STRUCT, SymbolKind::Enum => lsp_types::SymbolKind::ENUM, SymbolKind::Variant => lsp_types::SymbolKind::ENUM_MEMBER, SymbolKind::Trait => lsp_types::SymbolKind::INTERFACE, SymbolKind::Macro | SymbolKind::BuiltinAttr | SymbolKind::Attribute | SymbolKind::Derive | SymbolKind::DeriveHelper => lsp_types::SymbolKind::FUNCTION, SymbolKind::Module | SymbolKind::ToolModule => lsp_types::SymbolKind::MODULE, SymbolKind::TypeAlias | SymbolKind::TypeParam | SymbolKind::SelfType => { lsp_types::SymbolKind::TYPE_PARAMETER } SymbolKind::Field => lsp_types::SymbolKind::FIELD, SymbolKind::Static => lsp_types::SymbolKind::CONSTANT, SymbolKind::Const => lsp_types::SymbolKind::CONSTANT, SymbolKind::ConstParam => lsp_types::SymbolKind::CONSTANT, SymbolKind::Impl => lsp_types::SymbolKind::OBJECT, SymbolKind::Local | SymbolKind::SelfParam | SymbolKind::LifetimeParam | SymbolKind::ValueParam | SymbolKind::Label => lsp_types::SymbolKind::VARIABLE, SymbolKind::Union => lsp_types::SymbolKind::STRUCT, } } pub(crate) fn structure_node_kind(kind: StructureNodeKind) -> lsp_types::SymbolKind { match kind { StructureNodeKind::SymbolKind(symbol) => symbol_kind(symbol), StructureNodeKind::Region => lsp_types::SymbolKind::NAMESPACE, } } pub(crate) fn document_highlight_kind( category: ReferenceCategory, ) -> Option { match category { ReferenceCategory::Read => Some(lsp_types::DocumentHighlightKind::READ), ReferenceCategory::Write => Some(lsp_types::DocumentHighlightKind::WRITE), ReferenceCategory::Import => None, } } pub(crate) fn diagnostic_severity(severity: Severity) -> lsp_types::DiagnosticSeverity { match severity { Severity::Error => lsp_types::DiagnosticSeverity::ERROR, Severity::WeakWarning => lsp_types::DiagnosticSeverity::HINT, } } pub(crate) fn documentation(documentation: Documentation) -> lsp_types::Documentation { let value = crate::markdown::format_docs(documentation.as_str()); let markup_content = lsp_types::MarkupContent { kind: lsp_types::MarkupKind::Markdown, value }; lsp_types::Documentation::MarkupContent(markup_content) } pub(crate) fn completion_item_kind( completion_item_kind: CompletionItemKind, ) -> lsp_types::CompletionItemKind { match completion_item_kind { CompletionItemKind::Binding => lsp_types::CompletionItemKind::VARIABLE, CompletionItemKind::BuiltinType => lsp_types::CompletionItemKind::STRUCT, CompletionItemKind::InferredType => lsp_types::CompletionItemKind::SNIPPET, CompletionItemKind::Keyword => lsp_types::CompletionItemKind::KEYWORD, CompletionItemKind::Method => lsp_types::CompletionItemKind::METHOD, CompletionItemKind::Snippet => lsp_types::CompletionItemKind::SNIPPET, CompletionItemKind::UnresolvedReference => lsp_types::CompletionItemKind::REFERENCE, CompletionItemKind::SymbolKind(symbol) => match symbol { SymbolKind::Attribute => lsp_types::CompletionItemKind::FUNCTION, SymbolKind::Const => lsp_types::CompletionItemKind::CONSTANT, SymbolKind::ConstParam => lsp_types::CompletionItemKind::TYPE_PARAMETER, SymbolKind::Derive => lsp_types::CompletionItemKind::FUNCTION, SymbolKind::DeriveHelper => lsp_types::CompletionItemKind::FUNCTION, SymbolKind::Enum => lsp_types::CompletionItemKind::ENUM, SymbolKind::Field => lsp_types::CompletionItemKind::FIELD, SymbolKind::Function => lsp_types::CompletionItemKind::FUNCTION, SymbolKind::Impl => lsp_types::CompletionItemKind::TEXT, SymbolKind::Label => lsp_types::CompletionItemKind::VARIABLE, SymbolKind::LifetimeParam => lsp_types::CompletionItemKind::TYPE_PARAMETER, SymbolKind::Local => lsp_types::CompletionItemKind::VARIABLE, SymbolKind::Macro => lsp_types::CompletionItemKind::FUNCTION, SymbolKind::Module => lsp_types::CompletionItemKind::MODULE, SymbolKind::SelfParam => lsp_types::CompletionItemKind::VALUE, SymbolKind::SelfType => lsp_types::CompletionItemKind::TYPE_PARAMETER, SymbolKind::Static => lsp_types::CompletionItemKind::VALUE, SymbolKind::Struct => lsp_types::CompletionItemKind::STRUCT, SymbolKind::Trait => lsp_types::CompletionItemKind::INTERFACE, SymbolKind::TypeAlias => lsp_types::CompletionItemKind::STRUCT, SymbolKind::TypeParam => lsp_types::CompletionItemKind::TYPE_PARAMETER, SymbolKind::Union => lsp_types::CompletionItemKind::STRUCT, SymbolKind::ValueParam => lsp_types::CompletionItemKind::VALUE, SymbolKind::Variant => lsp_types::CompletionItemKind::ENUM_MEMBER, SymbolKind::BuiltinAttr => lsp_types::CompletionItemKind::FUNCTION, SymbolKind::ToolModule => lsp_types::CompletionItemKind::MODULE, }, } } pub(crate) fn text_edit(line_index: &LineIndex, indel: Indel) -> lsp_types::TextEdit { let range = range(line_index, indel.delete); let new_text = match line_index.endings { LineEndings::Unix => indel.insert, LineEndings::Dos => indel.insert.replace('\n', "\r\n"), }; lsp_types::TextEdit { range, new_text } } pub(crate) fn completion_text_edit( line_index: &LineIndex, insert_replace_support: Option, indel: Indel, ) -> lsp_types::CompletionTextEdit { let text_edit = text_edit(line_index, indel); match insert_replace_support { Some(cursor_pos) => lsp_types::InsertReplaceEdit { new_text: text_edit.new_text, insert: lsp_types::Range { start: text_edit.range.start, end: cursor_pos }, replace: text_edit.range, } .into(), None => text_edit.into(), } } pub(crate) fn snippet_text_edit( line_index: &LineIndex, is_snippet: bool, indel: Indel, ) -> lsp_ext::SnippetTextEdit { let text_edit = text_edit(line_index, indel); let insert_text_format = if is_snippet { Some(lsp_types::InsertTextFormat::SNIPPET) } else { None }; lsp_ext::SnippetTextEdit { range: text_edit.range, new_text: text_edit.new_text, insert_text_format, annotation_id: None, } } pub(crate) fn text_edit_vec( line_index: &LineIndex, text_edit: TextEdit, ) -> Vec { text_edit.into_iter().map(|indel| self::text_edit(line_index, indel)).collect() } pub(crate) fn snippet_text_edit_vec( line_index: &LineIndex, is_snippet: bool, text_edit: TextEdit, ) -> Vec { text_edit .into_iter() .map(|indel| self::snippet_text_edit(line_index, is_snippet, indel)) .collect() } pub(crate) fn completion_items( config: &Config, line_index: &LineIndex, tdpp: lsp_types::TextDocumentPositionParams, items: Vec, ) -> Vec { let max_relevance = items.iter().map(|it| it.relevance.score()).max().unwrap_or_default(); let mut res = Vec::with_capacity(items.len()); for item in items { completion_item(&mut res, config, line_index, &tdpp, max_relevance, item); } if let Some(limit) = config.completion().limit { res.sort_by(|item1, item2| item1.sort_text.cmp(&item2.sort_text)); res.truncate(limit); } res } fn completion_item( acc: &mut Vec, config: &Config, line_index: &LineIndex, tdpp: &lsp_types::TextDocumentPositionParams, max_relevance: u32, item: CompletionItem, ) { let insert_replace_support = config.insert_replace_support().then_some(tdpp.position); let ref_match = item.ref_match(); let lookup = item.lookup().to_string(); let mut additional_text_edits = Vec::new(); // LSP does not allow arbitrary edits in completion, so we have to do a // non-trivial mapping here. let text_edit = { let mut text_edit = None; let source_range = item.source_range; for indel in item.text_edit { if indel.delete.contains_range(source_range) { // Extract this indel as the main edit text_edit = Some(if indel.delete == source_range { self::completion_text_edit(line_index, insert_replace_support, indel.clone()) } else { assert!(source_range.end() == indel.delete.end()); let range1 = TextRange::new(indel.delete.start(), source_range.start()); let range2 = source_range; let indel1 = Indel::delete(range1); let indel2 = Indel::replace(range2, indel.insert.clone()); additional_text_edits.push(self::text_edit(line_index, indel1)); self::completion_text_edit(line_index, insert_replace_support, indel2) }) } else { assert!(source_range.intersect(indel.delete).is_none()); let text_edit = self::text_edit(line_index, indel.clone()); additional_text_edits.push(text_edit); } } text_edit.unwrap() }; let insert_text_format = item.is_snippet.then_some(lsp_types::InsertTextFormat::SNIPPET); let tags = item.deprecated.then(|| vec![lsp_types::CompletionItemTag::DEPRECATED]); let command = if item.trigger_call_info && config.client_commands().trigger_parameter_hints { Some(command::trigger_parameter_hints()) } else { None }; let mut lsp_item = lsp_types::CompletionItem { label: item.label.to_string(), detail: item.detail.map(|it| it.to_string()), filter_text: Some(lookup), kind: Some(completion_item_kind(item.kind)), text_edit: Some(text_edit), additional_text_edits: Some(additional_text_edits), documentation: item.documentation.map(documentation), deprecated: Some(item.deprecated), tags, command, insert_text_format, ..Default::default() }; if config.completion_label_details_support() { lsp_item.label_details = Some(lsp_types::CompletionItemLabelDetails { detail: None, description: lsp_item.detail.clone(), }); } set_score(&mut lsp_item, max_relevance, item.relevance); if config.completion().enable_imports_on_the_fly { if !item.import_to_add.is_empty() { let imports: Vec<_> = item .import_to_add .into_iter() .filter_map(|import_edit| { let import_path = &import_edit.import_path; let import_name = import_path.segments().last()?; Some(lsp_ext::CompletionImport { full_import_path: import_path.to_string(), imported_name: import_name.to_string(), }) }) .collect(); if !imports.is_empty() { let data = lsp_ext::CompletionResolveData { position: tdpp.clone(), imports }; lsp_item.data = Some(to_value(data).unwrap()); } } } if let Some((label, indel, relevance)) = ref_match { let mut lsp_item_with_ref = lsp_types::CompletionItem { label, ..lsp_item.clone() }; lsp_item_with_ref .additional_text_edits .get_or_insert_with(Default::default) .push(self::text_edit(line_index, indel)); set_score(&mut lsp_item_with_ref, max_relevance, relevance); acc.push(lsp_item_with_ref); }; acc.push(lsp_item); fn set_score( res: &mut lsp_types::CompletionItem, max_relevance: u32, relevance: CompletionRelevance, ) { if relevance.is_relevant() && relevance.score() == max_relevance { res.preselect = Some(true); } // The relevance needs to be inverted to come up with a sort score // because the client will sort ascending. let sort_score = relevance.score() ^ 0xFF_FF_FF_FF; // Zero pad the string to ensure values can be properly sorted // by the client. Hex format is used because it is easier to // visually compare very large values, which the sort text // tends to be since it is the opposite of the score. res.sort_text = Some(format!("{sort_score:08x}")); } } pub(crate) fn signature_help( call_info: SignatureHelp, config: CallInfoConfig, label_offsets: bool, ) -> lsp_types::SignatureHelp { let (label, parameters) = match (config.params_only, label_offsets) { (concise, false) => { let params = call_info .parameter_labels() .map(|label| lsp_types::ParameterInformation { label: lsp_types::ParameterLabel::Simple(label.to_string()), documentation: None, }) .collect::>(); let label = if concise { call_info.parameter_labels().join(", ") } else { call_info.signature }; (label, params) } (false, true) => { let params = call_info .parameter_ranges() .iter() .map(|it| { let start = call_info.signature[..it.start().into()].chars().count() as u32; let end = call_info.signature[..it.end().into()].chars().count() as u32; [start, end] }) .map(|label_offsets| lsp_types::ParameterInformation { label: lsp_types::ParameterLabel::LabelOffsets(label_offsets), documentation: None, }) .collect::>(); (call_info.signature, params) } (true, true) => { let mut params = Vec::new(); let mut label = String::new(); let mut first = true; for param in call_info.parameter_labels() { if !first { label.push_str(", "); } first = false; let start = label.chars().count() as u32; label.push_str(param); let end = label.chars().count() as u32; params.push(lsp_types::ParameterInformation { label: lsp_types::ParameterLabel::LabelOffsets([start, end]), documentation: None, }); } (label, params) } }; let documentation = call_info.doc.filter(|_| config.docs).map(|doc| { lsp_types::Documentation::MarkupContent(lsp_types::MarkupContent { kind: lsp_types::MarkupKind::Markdown, value: doc, }) }); let active_parameter = call_info.active_parameter.map(|it| it as u32); let signature = lsp_types::SignatureInformation { label, documentation, parameters: Some(parameters), active_parameter, }; lsp_types::SignatureHelp { signatures: vec![signature], active_signature: Some(0), active_parameter, } } pub(crate) fn inlay_hint( snap: &GlobalStateSnapshot, line_index: &LineIndex, render_colons: bool, mut inlay_hint: InlayHint, ) -> Cancellable { match inlay_hint.kind { InlayKind::Parameter if render_colons => inlay_hint.label.append_str(":"), InlayKind::Type if render_colons => inlay_hint.label.prepend_str(": "), InlayKind::ClosureReturnType => inlay_hint.label.prepend_str(" -> "), InlayKind::Discriminant => inlay_hint.label.prepend_str(" = "), _ => {} } let (label, tooltip) = inlay_hint_label(snap, inlay_hint.label)?; Ok(lsp_types::InlayHint { position: match inlay_hint.kind { // before annotated thing InlayKind::OpeningParenthesis | InlayKind::Parameter | InlayKind::Adjustment | InlayKind::BindingMode => position(line_index, inlay_hint.range.start()), // after annotated thing InlayKind::ClosureReturnType | InlayKind::Type | InlayKind::Discriminant | InlayKind::Chaining | InlayKind::GenericParamList | InlayKind::ClosingParenthesis | InlayKind::AdjustmentPostfix | InlayKind::Lifetime | InlayKind::ClosingBrace => position(line_index, inlay_hint.range.end()), }, padding_left: Some(match inlay_hint.kind { InlayKind::Type => !render_colons, InlayKind::Chaining | InlayKind::ClosingBrace => true, InlayKind::ClosingParenthesis | InlayKind::Discriminant | InlayKind::OpeningParenthesis | InlayKind::BindingMode | InlayKind::ClosureReturnType | InlayKind::GenericParamList | InlayKind::Adjustment | InlayKind::AdjustmentPostfix | InlayKind::Lifetime | InlayKind::Parameter => false, }), padding_right: Some(match inlay_hint.kind { InlayKind::ClosingParenthesis | InlayKind::OpeningParenthesis | InlayKind::Chaining | InlayKind::ClosureReturnType | InlayKind::GenericParamList | InlayKind::Adjustment | InlayKind::AdjustmentPostfix | InlayKind::Type | InlayKind::Discriminant | InlayKind::ClosingBrace => false, InlayKind::BindingMode => { matches!(&label, lsp_types::InlayHintLabel::String(s) if s != "&") } InlayKind::Parameter | InlayKind::Lifetime => true, }), kind: match inlay_hint.kind { InlayKind::Parameter => Some(lsp_types::InlayHintKind::PARAMETER), InlayKind::ClosureReturnType | InlayKind::Type | InlayKind::Chaining => { Some(lsp_types::InlayHintKind::TYPE) } InlayKind::ClosingParenthesis | InlayKind::Discriminant | InlayKind::OpeningParenthesis | InlayKind::BindingMode | InlayKind::GenericParamList | InlayKind::Lifetime | InlayKind::Adjustment | InlayKind::AdjustmentPostfix | InlayKind::ClosingBrace => None, }, text_edits: None, data: None, tooltip, label, }) } fn inlay_hint_label( snap: &GlobalStateSnapshot, mut label: InlayHintLabel, ) -> Cancellable<(lsp_types::InlayHintLabel, Option)> { let res = match &*label.parts { [InlayHintLabelPart { linked_location: None, .. }] => { let InlayHintLabelPart { text, tooltip, .. } = label.parts.pop().unwrap(); ( lsp_types::InlayHintLabel::String(text), match tooltip { Some(ide::InlayTooltip::String(s)) => { Some(lsp_types::InlayHintTooltip::String(s)) } Some(ide::InlayTooltip::Markdown(s)) => { Some(lsp_types::InlayHintTooltip::MarkupContent(lsp_types::MarkupContent { kind: lsp_types::MarkupKind::Markdown, value: s, })) } None => None, }, ) } _ => { let parts = label .parts .into_iter() .map(|part| { part.linked_location.map(|range| location(snap, range)).transpose().map( |location| lsp_types::InlayHintLabelPart { value: part.text, tooltip: match part.tooltip { Some(ide::InlayTooltip::String(s)) => { Some(lsp_types::InlayHintLabelPartTooltip::String(s)) } Some(ide::InlayTooltip::Markdown(s)) => { Some(lsp_types::InlayHintLabelPartTooltip::MarkupContent( lsp_types::MarkupContent { kind: lsp_types::MarkupKind::Markdown, value: s, }, )) } None => None, }, location, command: None, }, ) }) .collect::>()?; (lsp_types::InlayHintLabel::LabelParts(parts), None) } }; Ok(res) } static TOKEN_RESULT_COUNTER: AtomicU32 = AtomicU32::new(1); pub(crate) fn semantic_tokens( text: &str, line_index: &LineIndex, highlights: Vec, ) -> lsp_types::SemanticTokens { let id = TOKEN_RESULT_COUNTER.fetch_add(1, Ordering::SeqCst).to_string(); let mut builder = semantic_tokens::SemanticTokensBuilder::new(id); for highlight_range in highlights { if highlight_range.highlight.is_empty() { continue; } let (ty, mods) = semantic_token_type_and_modifiers(highlight_range.highlight); let token_index = semantic_tokens::type_index(ty); let modifier_bitset = mods.0; for mut text_range in line_index.index.lines(highlight_range.range) { if text[text_range].ends_with('\n') { text_range = TextRange::new(text_range.start(), text_range.end() - TextSize::of('\n')); } let range = range(line_index, text_range); builder.push(range, token_index, modifier_bitset); } } builder.build() } pub(crate) fn semantic_token_delta( previous: &lsp_types::SemanticTokens, current: &lsp_types::SemanticTokens, ) -> lsp_types::SemanticTokensDelta { let result_id = current.result_id.clone(); let edits = semantic_tokens::diff_tokens(&previous.data, ¤t.data); lsp_types::SemanticTokensDelta { result_id, edits } } fn semantic_token_type_and_modifiers( highlight: Highlight, ) -> (lsp_types::SemanticTokenType, semantic_tokens::ModifierSet) { let mut mods = semantic_tokens::ModifierSet::default(); let type_ = match highlight.tag { HlTag::Symbol(symbol) => match symbol { SymbolKind::Attribute => semantic_tokens::DECORATOR, SymbolKind::Derive => semantic_tokens::DERIVE, SymbolKind::DeriveHelper => semantic_tokens::DERIVE_HELPER, SymbolKind::Module => semantic_tokens::NAMESPACE, SymbolKind::Impl => semantic_tokens::TYPE_ALIAS, SymbolKind::Field => semantic_tokens::PROPERTY, SymbolKind::TypeParam => semantic_tokens::TYPE_PARAMETER, SymbolKind::ConstParam => semantic_tokens::CONST_PARAMETER, SymbolKind::LifetimeParam => semantic_tokens::LIFETIME, SymbolKind::Label => semantic_tokens::LABEL, SymbolKind::ValueParam => semantic_tokens::PARAMETER, SymbolKind::SelfParam => semantic_tokens::SELF_KEYWORD, SymbolKind::SelfType => semantic_tokens::SELF_TYPE_KEYWORD, SymbolKind::Local => semantic_tokens::VARIABLE, SymbolKind::Function => { if highlight.mods.contains(HlMod::Associated) { semantic_tokens::METHOD } else { semantic_tokens::FUNCTION } } SymbolKind::Const => { mods |= semantic_tokens::CONSTANT; mods |= semantic_tokens::STATIC; semantic_tokens::VARIABLE } SymbolKind::Static => { mods |= semantic_tokens::STATIC; semantic_tokens::VARIABLE } SymbolKind::Struct => semantic_tokens::STRUCT, SymbolKind::Enum => semantic_tokens::ENUM, SymbolKind::Variant => semantic_tokens::ENUM_MEMBER, SymbolKind::Union => semantic_tokens::UNION, SymbolKind::TypeAlias => semantic_tokens::TYPE_ALIAS, SymbolKind::Trait => semantic_tokens::INTERFACE, SymbolKind::Macro => semantic_tokens::MACRO, SymbolKind::BuiltinAttr => semantic_tokens::BUILTIN_ATTRIBUTE, SymbolKind::ToolModule => semantic_tokens::TOOL_MODULE, }, HlTag::AttributeBracket => semantic_tokens::ATTRIBUTE_BRACKET, HlTag::BoolLiteral => semantic_tokens::BOOLEAN, HlTag::BuiltinType => semantic_tokens::BUILTIN_TYPE, HlTag::ByteLiteral | HlTag::NumericLiteral => semantic_tokens::NUMBER, HlTag::CharLiteral => semantic_tokens::CHAR, HlTag::Comment => semantic_tokens::COMMENT, HlTag::EscapeSequence => semantic_tokens::ESCAPE_SEQUENCE, HlTag::FormatSpecifier => semantic_tokens::FORMAT_SPECIFIER, HlTag::Keyword => semantic_tokens::KEYWORD, HlTag::None => semantic_tokens::GENERIC, HlTag::Operator(op) => match op { HlOperator::Bitwise => semantic_tokens::BITWISE, HlOperator::Arithmetic => semantic_tokens::ARITHMETIC, HlOperator::Logical => semantic_tokens::LOGICAL, HlOperator::Comparison => semantic_tokens::COMPARISON, HlOperator::Other => semantic_tokens::OPERATOR, }, HlTag::StringLiteral => semantic_tokens::STRING, HlTag::UnresolvedReference => semantic_tokens::UNRESOLVED_REFERENCE, HlTag::Punctuation(punct) => match punct { HlPunct::Bracket => semantic_tokens::BRACKET, HlPunct::Brace => semantic_tokens::BRACE, HlPunct::Parenthesis => semantic_tokens::PARENTHESIS, HlPunct::Angle => semantic_tokens::ANGLE, HlPunct::Comma => semantic_tokens::COMMA, HlPunct::Dot => semantic_tokens::DOT, HlPunct::Colon => semantic_tokens::COLON, HlPunct::Semi => semantic_tokens::SEMICOLON, HlPunct::Other => semantic_tokens::PUNCTUATION, HlPunct::MacroBang => semantic_tokens::MACRO_BANG, }, }; for modifier in highlight.mods.iter() { let modifier = match modifier { HlMod::Associated => continue, HlMod::Async => semantic_tokens::ASYNC, HlMod::Attribute => semantic_tokens::ATTRIBUTE_MODIFIER, HlMod::Callable => semantic_tokens::CALLABLE, HlMod::Consuming => semantic_tokens::CONSUMING, HlMod::ControlFlow => semantic_tokens::CONTROL_FLOW, HlMod::CrateRoot => semantic_tokens::CRATE_ROOT, HlMod::DefaultLibrary => semantic_tokens::DEFAULT_LIBRARY, HlMod::Definition => semantic_tokens::DECLARATION, HlMod::Documentation => semantic_tokens::DOCUMENTATION, HlMod::Injected => semantic_tokens::INJECTED, HlMod::IntraDocLink => semantic_tokens::INTRA_DOC_LINK, HlMod::Library => semantic_tokens::LIBRARY, HlMod::Mutable => semantic_tokens::MUTABLE, HlMod::Public => semantic_tokens::PUBLIC, HlMod::Reference => semantic_tokens::REFERENCE, HlMod::Static => semantic_tokens::STATIC, HlMod::Trait => semantic_tokens::TRAIT_MODIFIER, HlMod::Unsafe => semantic_tokens::UNSAFE, }; mods |= modifier; } (type_, mods) } pub(crate) fn folding_range( text: &str, line_index: &LineIndex, line_folding_only: bool, fold: Fold, ) -> lsp_types::FoldingRange { let kind = match fold.kind { FoldKind::Comment => Some(lsp_types::FoldingRangeKind::Comment), FoldKind::Imports => Some(lsp_types::FoldingRangeKind::Imports), FoldKind::Region => Some(lsp_types::FoldingRangeKind::Region), FoldKind::Mods | FoldKind::Block | FoldKind::ArgList | FoldKind::Consts | FoldKind::Statics | FoldKind::WhereClause | FoldKind::ReturnType | FoldKind::Array | FoldKind::MatchArm => None, }; let range = range(line_index, fold.range); if line_folding_only { // Clients with line_folding_only == true (such as VSCode) will fold the whole end line // even if it contains text not in the folding range. To prevent that we exclude // range.end.line from the folding region if there is more text after range.end // on the same line. let has_more_text_on_end_line = text[TextRange::new(fold.range.end(), TextSize::of(text))] .chars() .take_while(|it| *it != '\n') .any(|it| !it.is_whitespace()); let end_line = if has_more_text_on_end_line { range.end.line.saturating_sub(1) } else { range.end.line }; lsp_types::FoldingRange { start_line: range.start.line, start_character: None, end_line, end_character: None, kind, collapsed_text: None, } } else { lsp_types::FoldingRange { start_line: range.start.line, start_character: Some(range.start.character), end_line: range.end.line, end_character: Some(range.end.character), kind, collapsed_text: None, } } } pub(crate) fn url(snap: &GlobalStateSnapshot, file_id: FileId) -> lsp_types::Url { snap.file_id_to_url(file_id) } /// Returns a `Url` object from a given path, will lowercase drive letters if present. /// This will only happen when processing windows paths. /// /// When processing non-windows path, this is essentially the same as `Url::from_file_path`. pub(crate) fn url_from_abs_path(path: &AbsPath) -> lsp_types::Url { let url = lsp_types::Url::from_file_path(path).unwrap(); match path.as_ref().components().next() { Some(path::Component::Prefix(prefix)) if matches!(prefix.kind(), path::Prefix::Disk(_) | path::Prefix::VerbatimDisk(_)) => { // Need to lowercase driver letter } _ => return url, } let driver_letter_range = { let (scheme, drive_letter, _rest) = match url.as_str().splitn(3, ':').collect_tuple() { Some(it) => it, None => return url, }; let start = scheme.len() + ':'.len_utf8(); start..(start + drive_letter.len()) }; // Note: lowercasing the `path` itself doesn't help, the `Url::parse` // machinery *also* canonicalizes the drive letter. So, just massage the // string in place. let mut url: String = url.into(); url[driver_letter_range].make_ascii_lowercase(); lsp_types::Url::parse(&url).unwrap() } pub(crate) fn optional_versioned_text_document_identifier( snap: &GlobalStateSnapshot, file_id: FileId, ) -> lsp_types::OptionalVersionedTextDocumentIdentifier { let url = url(snap, file_id); let version = snap.url_file_version(&url); lsp_types::OptionalVersionedTextDocumentIdentifier { uri: url, version } } pub(crate) fn location( snap: &GlobalStateSnapshot, frange: FileRange, ) -> Cancellable { let url = url(snap, frange.file_id); let line_index = snap.file_line_index(frange.file_id)?; let range = range(&line_index, frange.range); let loc = lsp_types::Location::new(url, range); Ok(loc) } /// Prefer using `location_link`, if the client has the cap. pub(crate) fn location_from_nav( snap: &GlobalStateSnapshot, nav: NavigationTarget, ) -> Cancellable { let url = url(snap, nav.file_id); let line_index = snap.file_line_index(nav.file_id)?; let range = range(&line_index, nav.full_range); let loc = lsp_types::Location::new(url, range); Ok(loc) } pub(crate) fn location_link( snap: &GlobalStateSnapshot, src: Option, target: NavigationTarget, ) -> Cancellable { let origin_selection_range = match src { Some(src) => { let line_index = snap.file_line_index(src.file_id)?; let range = range(&line_index, src.range); Some(range) } None => None, }; let (target_uri, target_range, target_selection_range) = location_info(snap, target)?; let res = lsp_types::LocationLink { origin_selection_range, target_uri, target_range, target_selection_range, }; Ok(res) } fn location_info( snap: &GlobalStateSnapshot, target: NavigationTarget, ) -> Cancellable<(lsp_types::Url, lsp_types::Range, lsp_types::Range)> { let line_index = snap.file_line_index(target.file_id)?; let target_uri = url(snap, target.file_id); let target_range = range(&line_index, target.full_range); let target_selection_range = target.focus_range.map(|it| range(&line_index, it)).unwrap_or(target_range); Ok((target_uri, target_range, target_selection_range)) } pub(crate) fn goto_definition_response( snap: &GlobalStateSnapshot, src: Option, targets: Vec, ) -> Cancellable { if snap.config.location_link() { let links = targets .into_iter() .map(|nav| location_link(snap, src, nav)) .collect::>>()?; Ok(links.into()) } else { let locations = targets .into_iter() .map(|nav| { location(snap, FileRange { file_id: nav.file_id, range: nav.focus_or_full_range() }) }) .collect::>>()?; Ok(locations.into()) } } fn outside_workspace_annotation_id() -> String { String::from("OutsideWorkspace") } pub(crate) fn snippet_text_document_edit( snap: &GlobalStateSnapshot, is_snippet: bool, file_id: FileId, edit: TextEdit, ) -> Cancellable { let text_document = optional_versioned_text_document_identifier(snap, file_id); let line_index = snap.file_line_index(file_id)?; let mut edits: Vec<_> = edit.into_iter().map(|it| snippet_text_edit(&line_index, is_snippet, it)).collect(); if snap.analysis.is_library_file(file_id)? && snap.config.change_annotation_support() { for edit in &mut edits { edit.annotation_id = Some(outside_workspace_annotation_id()) } } Ok(lsp_ext::SnippetTextDocumentEdit { text_document, edits }) } pub(crate) fn snippet_text_document_ops( snap: &GlobalStateSnapshot, file_system_edit: FileSystemEdit, ) -> Cancellable> { let mut ops = Vec::new(); match file_system_edit { FileSystemEdit::CreateFile { dst, initial_contents } => { let uri = snap.anchored_path(&dst); let create_file = lsp_types::ResourceOp::Create(lsp_types::CreateFile { uri: uri.clone(), options: None, annotation_id: None, }); ops.push(lsp_ext::SnippetDocumentChangeOperation::Op(create_file)); if !initial_contents.is_empty() { let text_document = lsp_types::OptionalVersionedTextDocumentIdentifier { uri, version: None }; let text_edit = lsp_ext::SnippetTextEdit { range: lsp_types::Range::default(), new_text: initial_contents, insert_text_format: Some(lsp_types::InsertTextFormat::PLAIN_TEXT), annotation_id: None, }; let edit_file = lsp_ext::SnippetTextDocumentEdit { text_document, edits: vec![text_edit] }; ops.push(lsp_ext::SnippetDocumentChangeOperation::Edit(edit_file)); } } FileSystemEdit::MoveFile { src, dst } => { let old_uri = snap.file_id_to_url(src); let new_uri = snap.anchored_path(&dst); let mut rename_file = lsp_types::RenameFile { old_uri, new_uri, options: None, annotation_id: None }; if snap.analysis.is_library_file(src).ok() == Some(true) && snap.config.change_annotation_support() { rename_file.annotation_id = Some(outside_workspace_annotation_id()) } ops.push(lsp_ext::SnippetDocumentChangeOperation::Op(lsp_types::ResourceOp::Rename( rename_file, ))) } FileSystemEdit::MoveDir { src, src_id, dst } => { let old_uri = snap.anchored_path(&src); let new_uri = snap.anchored_path(&dst); let mut rename_file = lsp_types::RenameFile { old_uri, new_uri, options: None, annotation_id: None }; if snap.analysis.is_library_file(src_id).ok() == Some(true) && snap.config.change_annotation_support() { rename_file.annotation_id = Some(outside_workspace_annotation_id()) } ops.push(lsp_ext::SnippetDocumentChangeOperation::Op(lsp_types::ResourceOp::Rename( rename_file, ))) } } Ok(ops) } pub(crate) fn snippet_workspace_edit( snap: &GlobalStateSnapshot, source_change: SourceChange, ) -> Cancellable { let mut document_changes: Vec = Vec::new(); for op in source_change.file_system_edits { let ops = snippet_text_document_ops(snap, op)?; document_changes.extend_from_slice(&ops); } for (file_id, edit) in source_change.source_file_edits { let edit = snippet_text_document_edit(snap, source_change.is_snippet, file_id, edit)?; document_changes.push(lsp_ext::SnippetDocumentChangeOperation::Edit(edit)); } let mut workspace_edit = lsp_ext::SnippetWorkspaceEdit { changes: None, document_changes: Some(document_changes), change_annotations: None, }; if snap.config.change_annotation_support() { workspace_edit.change_annotations = Some( once(( outside_workspace_annotation_id(), lsp_types::ChangeAnnotation { label: String::from("Edit outside of the workspace"), needs_confirmation: Some(true), description: Some(String::from( "This edit lies outside of the workspace and may affect dependencies", )), }, )) .collect(), ) } Ok(workspace_edit) } pub(crate) fn workspace_edit( snap: &GlobalStateSnapshot, source_change: SourceChange, ) -> Cancellable { assert!(!source_change.is_snippet); snippet_workspace_edit(snap, source_change).map(|it| it.into()) } impl From for lsp_types::WorkspaceEdit { fn from(snippet_workspace_edit: lsp_ext::SnippetWorkspaceEdit) -> lsp_types::WorkspaceEdit { lsp_types::WorkspaceEdit { changes: None, document_changes: snippet_workspace_edit.document_changes.map(|changes| { lsp_types::DocumentChanges::Operations( changes .into_iter() .map(|change| match change { lsp_ext::SnippetDocumentChangeOperation::Op(op) => { lsp_types::DocumentChangeOperation::Op(op) } lsp_ext::SnippetDocumentChangeOperation::Edit(edit) => { lsp_types::DocumentChangeOperation::Edit( lsp_types::TextDocumentEdit { text_document: edit.text_document, edits: edit.edits.into_iter().map(From::from).collect(), }, ) } }) .collect(), ) }), change_annotations: snippet_workspace_edit.change_annotations, } } } impl From for lsp_types::OneOf { fn from( lsp_ext::SnippetTextEdit { annotation_id, insert_text_format:_, new_text, range }: lsp_ext::SnippetTextEdit, ) -> Self { match annotation_id { Some(annotation_id) => lsp_types::OneOf::Right(lsp_types::AnnotatedTextEdit { text_edit: lsp_types::TextEdit { range, new_text }, annotation_id, }), None => lsp_types::OneOf::Left(lsp_types::TextEdit { range, new_text }), } } } pub(crate) fn call_hierarchy_item( snap: &GlobalStateSnapshot, target: NavigationTarget, ) -> Cancellable { let name = target.name.to_string(); let detail = target.description.clone(); let kind = target.kind.map(symbol_kind).unwrap_or(lsp_types::SymbolKind::FUNCTION); let (uri, range, selection_range) = location_info(snap, target)?; Ok(lsp_types::CallHierarchyItem { name, kind, tags: None, detail, uri, range, selection_range, data: None, }) } pub(crate) fn code_action_kind(kind: AssistKind) -> lsp_types::CodeActionKind { match kind { AssistKind::None | AssistKind::Generate => lsp_types::CodeActionKind::EMPTY, AssistKind::QuickFix => lsp_types::CodeActionKind::QUICKFIX, AssistKind::Refactor => lsp_types::CodeActionKind::REFACTOR, AssistKind::RefactorExtract => lsp_types::CodeActionKind::REFACTOR_EXTRACT, AssistKind::RefactorInline => lsp_types::CodeActionKind::REFACTOR_INLINE, AssistKind::RefactorRewrite => lsp_types::CodeActionKind::REFACTOR_REWRITE, } } pub(crate) fn code_action( snap: &GlobalStateSnapshot, assist: Assist, resolve_data: Option<(usize, lsp_types::CodeActionParams)>, ) -> Cancellable { let mut res = lsp_ext::CodeAction { title: assist.label.to_string(), group: assist.group.filter(|_| snap.config.code_action_group()).map(|gr| gr.0), kind: Some(code_action_kind(assist.id.1)), edit: None, is_preferred: None, data: None, command: None, }; if assist.trigger_signature_help && snap.config.client_commands().trigger_parameter_hints { res.command = Some(command::trigger_parameter_hints()); } match (assist.source_change, resolve_data) { (Some(it), _) => res.edit = Some(snippet_workspace_edit(snap, it)?), (None, Some((index, code_action_params))) => { res.data = Some(lsp_ext::CodeActionData { id: format!("{}:{}:{index}", assist.id.0, assist.id.1.name()), code_action_params, }); } (None, None) => { stdx::never!("assist should always be resolved if client can't do lazy resolving") } }; Ok(res) } pub(crate) fn runnable( snap: &GlobalStateSnapshot, runnable: Runnable, ) -> Cancellable { let config = snap.config.runnables(); let spec = CargoTargetSpec::for_file(snap, runnable.nav.file_id)?; let workspace_root = spec.as_ref().map(|it| it.workspace_root.clone()); let target = spec.as_ref().map(|s| s.target.clone()); let (cargo_args, executable_args) = CargoTargetSpec::runnable_args(snap, spec, &runnable.kind, &runnable.cfg); let label = runnable.label(target); let location = location_link(snap, None, runnable.nav)?; Ok(lsp_ext::Runnable { label, location: Some(location), kind: lsp_ext::RunnableKind::Cargo, args: lsp_ext::CargoRunnable { workspace_root: workspace_root.map(|it| it.into()), override_cargo: config.override_cargo, cargo_args, cargo_extra_args: config.cargo_extra_args, executable_args, expect_test: None, }, }) } pub(crate) fn code_lens( acc: &mut Vec, snap: &GlobalStateSnapshot, annotation: Annotation, ) -> Cancellable<()> { let client_commands_config = snap.config.client_commands(); match annotation.kind { AnnotationKind::Runnable(run) => { let line_index = snap.file_line_index(run.nav.file_id)?; let annotation_range = range(&line_index, annotation.range); let title = run.title(); let can_debug = match run.kind { ide::RunnableKind::DocTest { .. } => false, ide::RunnableKind::TestMod { .. } | ide::RunnableKind::Test { .. } | ide::RunnableKind::Bench { .. } | ide::RunnableKind::Bin => true, }; let r = runnable(snap, run)?; let lens_config = snap.config.lens(); if lens_config.run && client_commands_config.run_single && r.args.workspace_root.is_some() { let command = command::run_single(&r, &title); acc.push(lsp_types::CodeLens { range: annotation_range, command: Some(command), data: None, }) } if lens_config.debug && can_debug && client_commands_config.debug_single { let command = command::debug_single(&r); acc.push(lsp_types::CodeLens { range: annotation_range, command: Some(command), data: None, }) } } AnnotationKind::HasImpls { pos: file_range, data } => { if !client_commands_config.show_reference { return Ok(()); } let line_index = snap.file_line_index(file_range.file_id)?; let annotation_range = range(&line_index, annotation.range); let url = url(snap, file_range.file_id); let id = lsp_types::TextDocumentIdentifier { uri: url.clone() }; let doc_pos = lsp_types::TextDocumentPositionParams::new(id, annotation_range.start); let goto_params = lsp_types::request::GotoImplementationParams { text_document_position_params: doc_pos, work_done_progress_params: Default::default(), partial_result_params: Default::default(), }; let command = data.map(|ranges| { let locations: Vec = ranges .into_iter() .filter_map(|target| { location( snap, FileRange { file_id: target.file_id, range: target.full_range }, ) .ok() }) .collect(); command::show_references( implementation_title(locations.len()), &url, annotation_range.start, locations, ) }); acc.push(lsp_types::CodeLens { range: annotation_range, command, data: Some(to_value(lsp_ext::CodeLensResolveData::Impls(goto_params)).unwrap()), }) } AnnotationKind::HasReferences { pos: file_range, data } => { if !client_commands_config.show_reference { return Ok(()); } let line_index = snap.file_line_index(file_range.file_id)?; let annotation_range = range(&line_index, annotation.range); let url = url(snap, file_range.file_id); let id = lsp_types::TextDocumentIdentifier { uri: url.clone() }; let doc_pos = lsp_types::TextDocumentPositionParams::new(id, annotation_range.start); let command = data.map(|ranges| { let locations: Vec = ranges.into_iter().filter_map(|range| location(snap, range).ok()).collect(); command::show_references( reference_title(locations.len()), &url, annotation_range.start, locations, ) }); acc.push(lsp_types::CodeLens { range: annotation_range, command, data: Some(to_value(lsp_ext::CodeLensResolveData::References(doc_pos)).unwrap()), }) } } Ok(()) } pub(crate) mod command { use ide::{FileRange, NavigationTarget}; use serde_json::to_value; use crate::{ global_state::GlobalStateSnapshot, lsp_ext, to_proto::{location, location_link}, }; pub(crate) fn show_references( title: String, uri: &lsp_types::Url, position: lsp_types::Position, locations: Vec, ) -> lsp_types::Command { // We cannot use the 'editor.action.showReferences' command directly // because that command requires vscode types which we convert in the handler // on the client side. lsp_types::Command { title, command: "rust-analyzer.showReferences".into(), arguments: Some(vec![ to_value(uri).unwrap(), to_value(position).unwrap(), to_value(locations).unwrap(), ]), } } pub(crate) fn run_single(runnable: &lsp_ext::Runnable, title: &str) -> lsp_types::Command { lsp_types::Command { title: title.to_string(), command: "rust-analyzer.runSingle".into(), arguments: Some(vec![to_value(runnable).unwrap()]), } } pub(crate) fn debug_single(runnable: &lsp_ext::Runnable) -> lsp_types::Command { lsp_types::Command { title: "Debug".into(), command: "rust-analyzer.debugSingle".into(), arguments: Some(vec![to_value(runnable).unwrap()]), } } pub(crate) fn goto_location( snap: &GlobalStateSnapshot, nav: &NavigationTarget, ) -> Option { let value = if snap.config.location_link() { let link = location_link(snap, None, nav.clone()).ok()?; to_value(link).ok()? } else { let range = FileRange { file_id: nav.file_id, range: nav.focus_or_full_range() }; let location = location(snap, range).ok()?; to_value(location).ok()? }; Some(lsp_types::Command { title: nav.name.to_string(), command: "rust-analyzer.gotoLocation".into(), arguments: Some(vec![value]), }) } pub(crate) fn trigger_parameter_hints() -> lsp_types::Command { lsp_types::Command { title: "triggerParameterHints".into(), command: "rust-analyzer.triggerParameterHints".into(), arguments: None, } } } pub(crate) fn implementation_title(count: usize) -> String { if count == 1 { "1 implementation".into() } else { format!("{count} implementations") } } pub(crate) fn reference_title(count: usize) -> String { if count == 1 { "1 reference".into() } else { format!("{count} references") } } pub(crate) fn markup_content( markup: Markup, kind: ide::HoverDocFormat, ) -> lsp_types::MarkupContent { let kind = match kind { ide::HoverDocFormat::Markdown => lsp_types::MarkupKind::Markdown, ide::HoverDocFormat::PlainText => lsp_types::MarkupKind::PlainText, }; let value = crate::markdown::format_docs(markup.as_str()); lsp_types::MarkupContent { kind, value } } pub(crate) fn rename_error(err: RenameError) -> crate::LspError { // This is wrong, but we don't have a better alternative I suppose? // https://github.com/microsoft/language-server-protocol/issues/1341 invalid_params_error(err.to_string()) } #[cfg(test)] mod tests { use std::sync::Arc; use ide::Analysis; use super::*; #[test] fn conv_fold_line_folding_only_fixup() { let text = r#"mod a; mod b; mod c; fn main() { if cond { a::do_a(); } else { b::do_b(); } }"#; let (analysis, file_id) = Analysis::from_single_file(text.to_string()); let folds = analysis.folding_ranges(file_id).unwrap(); assert_eq!(folds.len(), 4); let line_index = LineIndex { index: Arc::new(ide::LineIndex::new(text)), endings: LineEndings::Unix, encoding: PositionEncoding::Utf8, }; let converted: Vec = folds.into_iter().map(|it| folding_range(text, &line_index, true, it)).collect(); let expected_lines = [(0, 2), (4, 10), (5, 6), (7, 9)]; assert_eq!(converted.len(), expected_lines.len()); for (folding_range, (start_line, end_line)) in converted.iter().zip(expected_lines.iter()) { assert_eq!(folding_range.start_line, *start_line); assert_eq!(folding_range.start_character, None); assert_eq!(folding_range.end_line, *end_line); assert_eq!(folding_range.end_character, None); } } // `Url` is not able to parse windows paths on unix machines. #[test] #[cfg(target_os = "windows")] fn test_lowercase_drive_letter() { use std::path::Path; let url = url_from_abs_path(Path::new("C:\\Test").try_into().unwrap()); assert_eq!(url.to_string(), "file:///c:/Test"); let url = url_from_abs_path(Path::new(r#"\\localhost\C$\my_dir"#).try_into().unwrap()); assert_eq!(url.to_string(), "file://localhost/C$/my_dir"); } }