| //! Conversion of rust-analyzer specific types to lsp_types equivalents. |
| use std::{ |
| iter::once, |
| mem, 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, |
| InlayFieldsToResolve, InlayHint, InlayHintLabel, InlayHintLabelPart, InlayKind, Markup, |
| NavigationTarget, ReferenceCategory, RenameError, Runnable, Severity, SignatureHelp, |
| SnippetEdit, SourceChange, StructureNodeKind, SymbolKind, TextEdit, TextRange, TextSize, |
| }; |
| use ide_db::rust_doc::format_docs; |
| 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::{ |
| semantic_tokens::{self, standard_fallback_type}, |
| utils::invalid_params_error, |
| LspError, |
| }, |
| lsp_ext::{self, SnippetTextEdit}, |
| }; |
| |
| 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).unwrap(); |
| 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 | SymbolKind::TraitAlias => 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<lsp_types::DocumentHighlightKind> { |
| 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::Warning => lsp_types::DiagnosticSeverity::WARNING, |
| Severity::WeakWarning => lsp_types::DiagnosticSeverity::HINT, |
| // unreachable |
| Severity::Allow => lsp_types::DiagnosticSeverity::INFORMATION, |
| } |
| } |
| |
| pub(crate) fn documentation(documentation: Documentation) -> lsp_types::Documentation { |
| let value = format_docs(&documentation); |
| 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::TraitAlias => 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<lsp_types::Position>, |
| 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<lsp_types::TextEdit> { |
| 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<lsp_ext::SnippetTextEdit> { |
| 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<CompletionItem>, |
| ) -> Vec<lsp_types::CompletionItem> { |
| 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<lsp_types::CompletionItem>, |
| 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, |
| 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: item.label_detail.as_ref().map(ToString::to_string), |
| description: lsp_item.detail.clone(), |
| }); |
| } else if let Some(label_detail) = item.label_detail { |
| lsp_item.label.push_str(label_detail.as_str()); |
| } |
| |
| set_score(&mut lsp_item, max_relevance, item.relevance); |
| |
| if config.completion().enable_imports_on_the_fly && !item.import_to_add.is_empty() { |
| let imports: Vec<_> = item |
| .import_to_add |
| .into_iter() |
| .filter_map(|(import_path, import_name)| { |
| Some(lsp_ext::CompletionImport { |
| full_import_path: import_path, |
| imported_name: import_name, |
| }) |
| }) |
| .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::<Vec<_>>(); |
| 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::<Vec<_>>(); |
| (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: format_docs(&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, |
| fields_to_resolve: &InlayFieldsToResolve, |
| line_index: &LineIndex, |
| file_id: FileId, |
| inlay_hint: InlayHint, |
| ) -> Cancellable<lsp_types::InlayHint> { |
| let is_visual_studio_code = snap.config.is_visual_studio_code(); |
| let needs_resolve = inlay_hint.needs_resolve; |
| let (label, tooltip, mut something_to_resolve) = |
| inlay_hint_label(snap, fields_to_resolve, needs_resolve, inlay_hint.label)?; |
| let text_edits = |
| if !is_visual_studio_code && needs_resolve && fields_to_resolve.resolve_text_edits { |
| something_to_resolve |= inlay_hint.text_edit.is_some(); |
| None |
| } else { |
| inlay_hint.text_edit.map(|it| text_edit_vec(line_index, it)) |
| }; |
| let data = if needs_resolve && something_to_resolve { |
| Some(to_value(lsp_ext::InlayHintResolveData { file_id: file_id.index() }).unwrap()) |
| } else { |
| None |
| }; |
| |
| Ok(lsp_types::InlayHint { |
| position: match inlay_hint.position { |
| ide::InlayHintPosition::Before => position(line_index, inlay_hint.range.start()), |
| ide::InlayHintPosition::After => position(line_index, inlay_hint.range.end()), |
| }, |
| padding_left: Some(inlay_hint.pad_left), |
| padding_right: Some(inlay_hint.pad_right), |
| kind: match inlay_hint.kind { |
| InlayKind::Parameter => Some(lsp_types::InlayHintKind::PARAMETER), |
| InlayKind::Type | InlayKind::Chaining => Some(lsp_types::InlayHintKind::TYPE), |
| _ => None, |
| }, |
| text_edits, |
| data, |
| tooltip, |
| label, |
| }) |
| } |
| |
| fn inlay_hint_label( |
| snap: &GlobalStateSnapshot, |
| fields_to_resolve: &InlayFieldsToResolve, |
| needs_resolve: bool, |
| mut label: InlayHintLabel, |
| ) -> Cancellable<(lsp_types::InlayHintLabel, Option<lsp_types::InlayHintTooltip>, bool)> { |
| let mut something_to_resolve = false; |
| let (label, tooltip) = match &*label.parts { |
| [InlayHintLabelPart { linked_location: None, .. }] => { |
| let InlayHintLabelPart { text, tooltip, .. } = label.parts.pop().unwrap(); |
| let hint_tooltip = if needs_resolve && fields_to_resolve.resolve_hint_tooltip { |
| something_to_resolve |= tooltip.is_some(); |
| None |
| } else { |
| 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, |
| } |
| }; |
| (lsp_types::InlayHintLabel::String(text), hint_tooltip) |
| } |
| _ => { |
| let parts = label |
| .parts |
| .into_iter() |
| .map(|part| { |
| let tooltip = if needs_resolve && fields_to_resolve.resolve_label_tooltip { |
| something_to_resolve |= part.tooltip.is_some(); |
| None |
| } else { |
| 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, |
| } |
| }; |
| let location = if needs_resolve && fields_to_resolve.resolve_label_location { |
| something_to_resolve |= part.linked_location.is_some(); |
| None |
| } else { |
| part.linked_location.map(|range| location(snap, range)).transpose()? |
| }; |
| Ok(lsp_types::InlayHintLabelPart { |
| value: part.text, |
| tooltip, |
| location, |
| command: None, |
| }) |
| }) |
| .collect::<Cancellable<_>>()?; |
| (lsp_types::InlayHintLabel::LabelParts(parts), None) |
| } |
| }; |
| Ok((label, tooltip, something_to_resolve)) |
| } |
| |
| static TOKEN_RESULT_COUNTER: AtomicU32 = AtomicU32::new(1); |
| |
| pub(crate) fn semantic_tokens( |
| text: &str, |
| line_index: &LineIndex, |
| highlights: Vec<HlRange>, |
| semantics_tokens_augments_syntax_tokens: bool, |
| non_standard_tokens: bool, |
| ) -> 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; |
| } |
| |
| if semantics_tokens_augments_syntax_tokens { |
| match highlight_range.highlight.tag { |
| HlTag::BoolLiteral |
| | HlTag::ByteLiteral |
| | HlTag::CharLiteral |
| | HlTag::Comment |
| | HlTag::Keyword |
| | HlTag::NumericLiteral |
| | HlTag::Operator(_) |
| | HlTag::Punctuation(_) |
| | HlTag::StringLiteral |
| | HlTag::None |
| if highlight_range.highlight.mods.is_empty() => |
| { |
| continue |
| } |
| _ => (), |
| } |
| } |
| |
| let (mut ty, mut mods) = semantic_token_type_and_modifiers(highlight_range.highlight); |
| |
| if !non_standard_tokens { |
| ty = match standard_fallback_type(ty) { |
| Some(ty) => ty, |
| None => continue, |
| }; |
| mods.standard_fallback(); |
| } |
| 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::TraitAlias => 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::InvalidEscapeSequence => semantic_tokens::INVALID_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::Macro => semantic_tokens::MACRO_MODIFIER, |
| 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<lsp_types::Location> { |
| 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<lsp_types::Location> { |
| let url = url(snap, nav.file_id); |
| let line_index = snap.file_line_index(nav.file_id)?; |
| let range = range(&line_index, nav.focus_or_full_range()); |
| let loc = lsp_types::Location::new(url, range); |
| Ok(loc) |
| } |
| |
| pub(crate) fn location_link( |
| snap: &GlobalStateSnapshot, |
| src: Option<FileRange>, |
| target: NavigationTarget, |
| ) -> Cancellable<lsp_types::LocationLink> { |
| 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<FileRange>, |
| targets: Vec<NavigationTarget>, |
| ) -> Cancellable<lsp_types::GotoDefinitionResponse> { |
| if snap.config.location_link() { |
| let links = targets |
| .into_iter() |
| .map(|nav| location_link(snap, src, nav)) |
| .collect::<Cancellable<Vec<_>>>()?; |
| 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::<Cancellable<Vec<_>>>()?; |
| Ok(locations.into()) |
| } |
| } |
| |
| fn outside_workspace_annotation_id() -> String { |
| String::from("OutsideWorkspace") |
| } |
| |
| fn merge_text_and_snippet_edits( |
| line_index: &LineIndex, |
| edit: TextEdit, |
| snippet_edit: SnippetEdit, |
| ) -> Vec<SnippetTextEdit> { |
| let mut edits: Vec<SnippetTextEdit> = vec![]; |
| let mut snippets = snippet_edit.into_edit_ranges().into_iter().peekable(); |
| let text_edits = edit.into_iter(); |
| |
| for current_indel in text_edits { |
| let new_range = { |
| let insert_len = |
| TextSize::try_from(current_indel.insert.len()).unwrap_or(TextSize::from(u32::MAX)); |
| TextRange::at(current_indel.delete.start(), insert_len) |
| }; |
| |
| // insert any snippets before the text edit |
| for (snippet_index, snippet_range) in |
| snippets.take_while_ref(|(_, range)| range.end() < new_range.start()) |
| { |
| let snippet_range = if !stdx::always!( |
| snippet_range.is_empty(), |
| "placeholder range {:?} is before current text edit range {:?}", |
| snippet_range, |
| new_range |
| ) { |
| // only possible for tabstops, so make sure it's an empty/insert range |
| TextRange::empty(snippet_range.start()) |
| } else { |
| snippet_range |
| }; |
| |
| let range = range(line_index, snippet_range); |
| let new_text = format!("${snippet_index}"); |
| |
| edits.push(SnippetTextEdit { |
| range, |
| new_text, |
| insert_text_format: Some(lsp_types::InsertTextFormat::SNIPPET), |
| annotation_id: None, |
| }) |
| } |
| |
| if snippets.peek().is_some_and(|(_, range)| new_range.intersect(*range).is_some()) { |
| // at least one snippet edit intersects this text edit, |
| // so gather all of the edits that intersect this text edit |
| let mut all_snippets = snippets |
| .take_while_ref(|(_, range)| new_range.intersect(*range).is_some()) |
| .collect_vec(); |
| |
| // ensure all of the ranges are wholly contained inside of the new range |
| all_snippets.retain(|(_, range)| { |
| stdx::always!( |
| new_range.contains_range(*range), |
| "found placeholder range {:?} which wasn't fully inside of text edit's new range {:?}", range, new_range |
| ) |
| }); |
| |
| let mut text_edit = text_edit(line_index, current_indel); |
| |
| // escape out snippet text |
| stdx::replace(&mut text_edit.new_text, '\\', r"\\"); |
| stdx::replace(&mut text_edit.new_text, '$', r"\$"); |
| |
| // ...and apply! |
| for (index, range) in all_snippets.iter().rev() { |
| let start = (range.start() - new_range.start()).into(); |
| let end = (range.end() - new_range.start()).into(); |
| |
| if range.is_empty() { |
| text_edit.new_text.insert_str(start, &format!("${index}")); |
| } else { |
| text_edit.new_text.insert(end, '}'); |
| text_edit.new_text.insert_str(start, &format!("${{{index}:")); |
| } |
| } |
| |
| edits.push(SnippetTextEdit { |
| range: text_edit.range, |
| new_text: text_edit.new_text, |
| insert_text_format: Some(lsp_types::InsertTextFormat::SNIPPET), |
| annotation_id: None, |
| }) |
| } else { |
| // snippet edit was beyond the current one |
| // since it wasn't consumed, it's available for the next pass |
| edits.push(snippet_text_edit(line_index, false, current_indel)); |
| } |
| } |
| |
| // insert any remaining tabstops |
| edits.extend(snippets.map(|(snippet_index, snippet_range)| { |
| let snippet_range = if !stdx::always!( |
| snippet_range.is_empty(), |
| "found placeholder snippet {:?} without a text edit", |
| snippet_range |
| ) { |
| TextRange::empty(snippet_range.start()) |
| } else { |
| snippet_range |
| }; |
| |
| let range = range(line_index, snippet_range); |
| let new_text = format!("${snippet_index}"); |
| |
| SnippetTextEdit { |
| range, |
| new_text, |
| insert_text_format: Some(lsp_types::InsertTextFormat::SNIPPET), |
| annotation_id: None, |
| } |
| })); |
| |
| edits |
| } |
| |
| pub(crate) fn snippet_text_document_edit( |
| snap: &GlobalStateSnapshot, |
| is_snippet: bool, |
| file_id: FileId, |
| edit: TextEdit, |
| snippet_edit: Option<SnippetEdit>, |
| ) -> Cancellable<lsp_ext::SnippetTextDocumentEdit> { |
| let text_document = optional_versioned_text_document_identifier(snap, file_id); |
| let line_index = snap.file_line_index(file_id)?; |
| let mut edits = if let Some(snippet_edit) = snippet_edit { |
| merge_text_and_snippet_edits(&line_index, edit, snippet_edit) |
| } else { |
| 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<Vec<lsp_ext::SnippetDocumentChangeOperation>> { |
| 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, |
| mut source_change: SourceChange, |
| ) -> Cancellable<lsp_ext::SnippetWorkspaceEdit> { |
| let mut document_changes: Vec<lsp_ext::SnippetDocumentChangeOperation> = Vec::new(); |
| |
| for op in &mut source_change.file_system_edits { |
| if let FileSystemEdit::CreateFile { dst, initial_contents } = op { |
| // replace with a placeholder to avoid cloneing the edit |
| let op = FileSystemEdit::CreateFile { |
| dst: dst.clone(), |
| initial_contents: mem::take(initial_contents), |
| }; |
| let ops = snippet_text_document_ops(snap, op)?; |
| document_changes.extend_from_slice(&ops); |
| } |
| } |
| for (file_id, (edit, snippet_edit)) in source_change.source_file_edits { |
| let edit = snippet_text_document_edit( |
| snap, |
| source_change.is_snippet, |
| file_id, |
| edit, |
| snippet_edit, |
| )?; |
| document_changes.push(lsp_ext::SnippetDocumentChangeOperation::Edit(edit)); |
| } |
| for op in source_change.file_system_edits { |
| if !matches!(op, FileSystemEdit::CreateFile { .. }) { |
| let ops = snippet_text_document_ops(snap, op)?; |
| document_changes.extend_from_slice(&ops); |
| } |
| } |
| 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<lsp_types::WorkspaceEdit> { |
| assert!(!source_change.is_snippet); |
| snippet_workspace_edit(snap, source_change).map(|it| it.into()) |
| } |
| |
| impl From<lsp_ext::SnippetWorkspaceEdit> 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<lsp_ext::SnippetTextEdit> |
| for lsp_types::OneOf<lsp_types::TextEdit, lsp_types::AnnotatedTextEdit> |
| { |
| 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<lsp_types::CallHierarchyItem> { |
| 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<lsp_ext::CodeAction> { |
| 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<lsp_ext::Runnable> { |
| 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<lsp_types::CodeLens>, |
| 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, |
| }) |
| } |
| if lens_config.interpret { |
| let command = command::interpret_single(&r); |
| acc.push(lsp_types::CodeLens { |
| range: annotation_range, |
| command: Some(command), |
| data: None, |
| }) |
| } |
| } |
| AnnotationKind::HasImpls { pos, data } => { |
| if !client_commands_config.show_reference { |
| return Ok(()); |
| } |
| let line_index = snap.file_line_index(pos.file_id)?; |
| let annotation_range = range(&line_index, annotation.range); |
| let url = url(snap, pos.file_id); |
| let pos = position(&line_index, pos.offset); |
| |
| let id = lsp_types::TextDocumentIdentifier { uri: url.clone() }; |
| |
| let doc_pos = lsp_types::TextDocumentPositionParams::new(id, pos); |
| |
| 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<lsp_types::Location> = 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, |
| pos, |
| locations, |
| ) |
| }); |
| |
| acc.push(lsp_types::CodeLens { |
| range: annotation_range, |
| command, |
| data: (|| { |
| let version = snap.url_file_version(&url)?; |
| Some( |
| to_value(lsp_ext::CodeLensResolveData { |
| version, |
| kind: lsp_ext::CodeLensResolveDataKind::Impls(goto_params), |
| }) |
| .unwrap(), |
| ) |
| })(), |
| }) |
| } |
| AnnotationKind::HasReferences { pos, data } => { |
| if !client_commands_config.show_reference { |
| return Ok(()); |
| } |
| let line_index = snap.file_line_index(pos.file_id)?; |
| let annotation_range = range(&line_index, annotation.range); |
| let url = url(snap, pos.file_id); |
| let pos = position(&line_index, pos.offset); |
| |
| let id = lsp_types::TextDocumentIdentifier { uri: url.clone() }; |
| |
| let doc_pos = lsp_types::TextDocumentPositionParams::new(id, pos); |
| |
| let command = data.map(|ranges| { |
| let locations: Vec<lsp_types::Location> = |
| ranges.into_iter().filter_map(|range| location(snap, range).ok()).collect(); |
| |
| command::show_references(reference_title(locations.len()), &url, pos, locations) |
| }); |
| |
| acc.push(lsp_types::CodeLens { |
| range: annotation_range, |
| command, |
| data: (|| { |
| let version = snap.url_file_version(&url)?; |
| Some( |
| to_value(lsp_ext::CodeLensResolveData { |
| version, |
| kind: lsp_ext::CodeLensResolveDataKind::References(doc_pos), |
| }) |
| .unwrap(), |
| ) |
| })(), |
| }) |
| } |
| } |
| Ok(()) |
| } |
| |
| pub(crate) mod command { |
| use ide::{FileRange, NavigationTarget}; |
| use serde_json::to_value; |
| |
| use crate::{ |
| global_state::GlobalStateSnapshot, |
| lsp::to_proto::{location, location_link}, |
| lsp_ext, |
| }; |
| |
| pub(crate) fn show_references( |
| title: String, |
| uri: &lsp_types::Url, |
| position: lsp_types::Position, |
| locations: Vec<lsp_types::Location>, |
| ) -> 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 interpret_single(_runnable: &lsp_ext::Runnable) -> lsp_types::Command { |
| lsp_types::Command { |
| title: "Interpret".into(), |
| command: "rust-analyzer.interpretFunction".into(), |
| // FIXME: use the `_runnable` here. |
| arguments: Some(vec![]), |
| } |
| } |
| |
| pub(crate) fn goto_location( |
| snap: &GlobalStateSnapshot, |
| nav: &NavigationTarget, |
| ) -> Option<lsp_types::Command> { |
| 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 = format_docs(&Documentation::new(markup.into())); |
| lsp_types::MarkupContent { kind, value } |
| } |
| |
| pub(crate) fn rename_error(err: RenameError) -> 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 expect_test::{expect, Expect}; |
| use ide::{Analysis, FilePosition}; |
| use ide_db::source_change::Snippet; |
| use test_utils::extract_offset; |
| use triomphe::Arc; |
| |
| 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<lsp_types::FoldingRange> = |
| 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); |
| } |
| } |
| |
| #[test] |
| fn calling_function_with_ignored_code_in_signature() { |
| let text = r#" |
| fn foo() { |
| bar($0); |
| } |
| /// ``` |
| /// # use crate::bar; |
| /// bar(5); |
| /// ``` |
| fn bar(_: usize) {} |
| "#; |
| |
| let (offset, text) = extract_offset(text); |
| let (analysis, file_id) = Analysis::from_single_file(text); |
| let help = signature_help( |
| analysis.signature_help(FilePosition { file_id, offset }).unwrap().unwrap(), |
| CallInfoConfig { params_only: false, docs: true }, |
| false, |
| ); |
| let docs = match &help.signatures[help.active_signature.unwrap() as usize].documentation { |
| Some(lsp_types::Documentation::MarkupContent(content)) => &content.value, |
| _ => panic!("documentation contains markup"), |
| }; |
| assert!(docs.contains("bar(5)")); |
| assert!(!docs.contains("use crate::bar")); |
| } |
| |
| fn check_rendered_snippets(edit: TextEdit, snippets: SnippetEdit, expect: Expect) { |
| let text = r#"/* place to put all ranges in */"#; |
| let line_index = LineIndex { |
| index: Arc::new(ide::LineIndex::new(text)), |
| endings: LineEndings::Unix, |
| encoding: PositionEncoding::Utf8, |
| }; |
| |
| let res = merge_text_and_snippet_edits(&line_index, edit, snippets); |
| expect.assert_debug_eq(&res); |
| } |
| |
| #[test] |
| fn snippet_rendering_only_tabstops() { |
| let edit = TextEdit::builder().finish(); |
| let snippets = SnippetEdit::new(vec![ |
| Snippet::Tabstop(0.into()), |
| Snippet::Tabstop(0.into()), |
| Snippet::Tabstop(1.into()), |
| Snippet::Tabstop(1.into()), |
| ]); |
| |
| check_rendered_snippets( |
| edit, |
| snippets, |
| expect![[r#" |
| [ |
| SnippetTextEdit { |
| range: Range { |
| start: Position { |
| line: 0, |
| character: 0, |
| }, |
| end: Position { |
| line: 0, |
| character: 0, |
| }, |
| }, |
| new_text: "$1", |
| insert_text_format: Some( |
| Snippet, |
| ), |
| annotation_id: None, |
| }, |
| SnippetTextEdit { |
| range: Range { |
| start: Position { |
| line: 0, |
| character: 0, |
| }, |
| end: Position { |
| line: 0, |
| character: 0, |
| }, |
| }, |
| new_text: "$2", |
| insert_text_format: Some( |
| Snippet, |
| ), |
| annotation_id: None, |
| }, |
| SnippetTextEdit { |
| range: Range { |
| start: Position { |
| line: 0, |
| character: 1, |
| }, |
| end: Position { |
| line: 0, |
| character: 1, |
| }, |
| }, |
| new_text: "$3", |
| insert_text_format: Some( |
| Snippet, |
| ), |
| annotation_id: None, |
| }, |
| SnippetTextEdit { |
| range: Range { |
| start: Position { |
| line: 0, |
| character: 1, |
| }, |
| end: Position { |
| line: 0, |
| character: 1, |
| }, |
| }, |
| new_text: "$0", |
| insert_text_format: Some( |
| Snippet, |
| ), |
| annotation_id: None, |
| }, |
| ] |
| "#]], |
| ); |
| } |
| |
| #[test] |
| fn snippet_rendering_only_text_edits() { |
| let mut edit = TextEdit::builder(); |
| edit.insert(0.into(), "abc".to_owned()); |
| edit.insert(3.into(), "def".to_owned()); |
| let edit = edit.finish(); |
| let snippets = SnippetEdit::new(vec![]); |
| |
| check_rendered_snippets( |
| edit, |
| snippets, |
| expect![[r#" |
| [ |
| SnippetTextEdit { |
| range: Range { |
| start: Position { |
| line: 0, |
| character: 0, |
| }, |
| end: Position { |
| line: 0, |
| character: 0, |
| }, |
| }, |
| new_text: "abc", |
| insert_text_format: None, |
| annotation_id: None, |
| }, |
| SnippetTextEdit { |
| range: Range { |
| start: Position { |
| line: 0, |
| character: 3, |
| }, |
| end: Position { |
| line: 0, |
| character: 3, |
| }, |
| }, |
| new_text: "def", |
| insert_text_format: None, |
| annotation_id: None, |
| }, |
| ] |
| "#]], |
| ); |
| } |
| |
| #[test] |
| fn snippet_rendering_tabstop_after_text_edit() { |
| let mut edit = TextEdit::builder(); |
| edit.insert(0.into(), "abc".to_owned()); |
| let edit = edit.finish(); |
| let snippets = SnippetEdit::new(vec![Snippet::Tabstop(7.into())]); |
| |
| check_rendered_snippets( |
| edit, |
| snippets, |
| expect![[r#" |
| [ |
| SnippetTextEdit { |
| range: Range { |
| start: Position { |
| line: 0, |
| character: 0, |
| }, |
| end: Position { |
| line: 0, |
| character: 0, |
| }, |
| }, |
| new_text: "abc", |
| insert_text_format: None, |
| annotation_id: None, |
| }, |
| SnippetTextEdit { |
| range: Range { |
| start: Position { |
| line: 0, |
| character: 7, |
| }, |
| end: Position { |
| line: 0, |
| character: 7, |
| }, |
| }, |
| new_text: "$0", |
| insert_text_format: Some( |
| Snippet, |
| ), |
| annotation_id: None, |
| }, |
| ] |
| "#]], |
| ); |
| } |
| |
| #[test] |
| fn snippet_rendering_tabstops_before_text_edit() { |
| let mut edit = TextEdit::builder(); |
| edit.insert(2.into(), "abc".to_owned()); |
| let edit = edit.finish(); |
| let snippets = |
| SnippetEdit::new(vec![Snippet::Tabstop(0.into()), Snippet::Tabstop(0.into())]); |
| |
| check_rendered_snippets( |
| edit, |
| snippets, |
| expect![[r#" |
| [ |
| SnippetTextEdit { |
| range: Range { |
| start: Position { |
| line: 0, |
| character: 0, |
| }, |
| end: Position { |
| line: 0, |
| character: 0, |
| }, |
| }, |
| new_text: "$1", |
| insert_text_format: Some( |
| Snippet, |
| ), |
| annotation_id: None, |
| }, |
| SnippetTextEdit { |
| range: Range { |
| start: Position { |
| line: 0, |
| character: 0, |
| }, |
| end: Position { |
| line: 0, |
| character: 0, |
| }, |
| }, |
| new_text: "$0", |
| insert_text_format: Some( |
| Snippet, |
| ), |
| annotation_id: None, |
| }, |
| SnippetTextEdit { |
| range: Range { |
| start: Position { |
| line: 0, |
| character: 2, |
| }, |
| end: Position { |
| line: 0, |
| character: 2, |
| }, |
| }, |
| new_text: "abc", |
| insert_text_format: None, |
| annotation_id: None, |
| }, |
| ] |
| "#]], |
| ); |
| } |
| |
| #[test] |
| fn snippet_rendering_tabstops_between_text_edits() { |
| let mut edit = TextEdit::builder(); |
| edit.insert(0.into(), "abc".to_owned()); |
| edit.insert(7.into(), "abc".to_owned()); |
| let edit = edit.finish(); |
| let snippets = |
| SnippetEdit::new(vec![Snippet::Tabstop(4.into()), Snippet::Tabstop(4.into())]); |
| |
| check_rendered_snippets( |
| edit, |
| snippets, |
| expect![[r#" |
| [ |
| SnippetTextEdit { |
| range: Range { |
| start: Position { |
| line: 0, |
| character: 0, |
| }, |
| end: Position { |
| line: 0, |
| character: 0, |
| }, |
| }, |
| new_text: "abc", |
| insert_text_format: None, |
| annotation_id: None, |
| }, |
| SnippetTextEdit { |
| range: Range { |
| start: Position { |
| line: 0, |
| character: 4, |
| }, |
| end: Position { |
| line: 0, |
| character: 4, |
| }, |
| }, |
| new_text: "$1", |
| insert_text_format: Some( |
| Snippet, |
| ), |
| annotation_id: None, |
| }, |
| SnippetTextEdit { |
| range: Range { |
| start: Position { |
| line: 0, |
| character: 4, |
| }, |
| end: Position { |
| line: 0, |
| character: 4, |
| }, |
| }, |
| new_text: "$0", |
| insert_text_format: Some( |
| Snippet, |
| ), |
| annotation_id: None, |
| }, |
| SnippetTextEdit { |
| range: Range { |
| start: Position { |
| line: 0, |
| character: 7, |
| }, |
| end: Position { |
| line: 0, |
| character: 7, |
| }, |
| }, |
| new_text: "abc", |
| insert_text_format: None, |
| annotation_id: None, |
| }, |
| ] |
| "#]], |
| ); |
| } |
| |
| #[test] |
| fn snippet_rendering_multiple_tabstops_in_text_edit() { |
| let mut edit = TextEdit::builder(); |
| edit.insert(0.into(), "abcdefghijkl".to_owned()); |
| let edit = edit.finish(); |
| let snippets = SnippetEdit::new(vec![ |
| Snippet::Tabstop(0.into()), |
| Snippet::Tabstop(5.into()), |
| Snippet::Tabstop(12.into()), |
| ]); |
| |
| check_rendered_snippets( |
| edit, |
| snippets, |
| expect![[r#" |
| [ |
| SnippetTextEdit { |
| range: Range { |
| start: Position { |
| line: 0, |
| character: 0, |
| }, |
| end: Position { |
| line: 0, |
| character: 0, |
| }, |
| }, |
| new_text: "$1abcde$2fghijkl$0", |
| insert_text_format: Some( |
| Snippet, |
| ), |
| annotation_id: None, |
| }, |
| ] |
| "#]], |
| ); |
| } |
| |
| #[test] |
| fn snippet_rendering_multiple_placeholders_in_text_edit() { |
| let mut edit = TextEdit::builder(); |
| edit.insert(0.into(), "abcdefghijkl".to_owned()); |
| let edit = edit.finish(); |
| let snippets = SnippetEdit::new(vec![ |
| Snippet::Placeholder(TextRange::new(0.into(), 3.into())), |
| Snippet::Placeholder(TextRange::new(5.into(), 7.into())), |
| Snippet::Placeholder(TextRange::new(10.into(), 12.into())), |
| ]); |
| |
| check_rendered_snippets( |
| edit, |
| snippets, |
| expect![[r#" |
| [ |
| SnippetTextEdit { |
| range: Range { |
| start: Position { |
| line: 0, |
| character: 0, |
| }, |
| end: Position { |
| line: 0, |
| character: 0, |
| }, |
| }, |
| new_text: "${1:abc}de${2:fg}hij${0:kl}", |
| insert_text_format: Some( |
| Snippet, |
| ), |
| annotation_id: None, |
| }, |
| ] |
| "#]], |
| ); |
| } |
| |
| #[test] |
| fn snippet_rendering_escape_snippet_bits() { |
| // only needed for snippet formats |
| let mut edit = TextEdit::builder(); |
| edit.insert(0.into(), r"abc\def$".to_owned()); |
| edit.insert(8.into(), r"ghi\jkl$".to_owned()); |
| let edit = edit.finish(); |
| let snippets = |
| SnippetEdit::new(vec![Snippet::Placeholder(TextRange::new(0.into(), 3.into()))]); |
| |
| check_rendered_snippets( |
| edit, |
| snippets, |
| expect![[r#" |
| [ |
| SnippetTextEdit { |
| range: Range { |
| start: Position { |
| line: 0, |
| character: 0, |
| }, |
| end: Position { |
| line: 0, |
| character: 0, |
| }, |
| }, |
| new_text: "${0:abc}\\\\def\\$", |
| insert_text_format: Some( |
| Snippet, |
| ), |
| annotation_id: None, |
| }, |
| SnippetTextEdit { |
| range: Range { |
| start: Position { |
| line: 0, |
| character: 8, |
| }, |
| end: Position { |
| line: 0, |
| character: 8, |
| }, |
| }, |
| new_text: "ghi\\jkl$", |
| insert_text_format: None, |
| annotation_id: 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"); |
| } |
| } |