diff --git a/Cargo.lock b/Cargo.lock index c3d1d764822596c0060f77e684852553b5b74b0a..4b619bc4d1d90f817bba19cc42a4b43df46cce16 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9043,7 +9043,9 @@ dependencies = [ "serde_json", "serde_json_lenient", "settings", + "smallvec", "smol", + "snippet", "task", "terminal", "text", diff --git a/crates/editor/src/code_context_menus.rs b/crates/editor/src/code_context_menus.rs index efecb70b59746b51db713b4a13153033d0dd3b10..dcd96674207f02101b4066924b011d2b9ebd7a08 100644 --- a/crates/editor/src/code_context_menus.rs +++ b/crates/editor/src/code_context_menus.rs @@ -835,36 +835,38 @@ impl CompletionsMenu { FontWeight::BOLD.into(), ) }), - styled_runs_for_code_label(&completion.label, &style.syntax).map( - |(range, mut highlight)| { - // Ignore font weight for syntax highlighting, as we'll use it - // for fuzzy matches. - highlight.font_weight = None; - if completion - .source - .lsp_completion(false) - .and_then(|lsp_completion| { - match (lsp_completion.deprecated, &lsp_completion.tags) - { - (Some(true), _) => Some(true), - (_, Some(tags)) => Some( - tags.contains(&CompletionItemTag::DEPRECATED), - ), - _ => None, + styled_runs_for_code_label( + &completion.label, + &style.syntax, + &style.local_player, + ) + .map(|(range, mut highlight)| { + // Ignore font weight for syntax highlighting, as we'll use it + // for fuzzy matches. + highlight.font_weight = None; + if completion + .source + .lsp_completion(false) + .and_then(|lsp_completion| { + match (lsp_completion.deprecated, &lsp_completion.tags) { + (Some(true), _) => Some(true), + (_, Some(tags)) => { + Some(tags.contains(&CompletionItemTag::DEPRECATED)) } - }) - .unwrap_or(false) - { - highlight.strikethrough = Some(StrikethroughStyle { - thickness: 1.0.into(), - ..Default::default() - }); - highlight.color = Some(cx.theme().colors().text_muted); - } + _ => None, + } + }) + .unwrap_or(false) + { + highlight.strikethrough = Some(StrikethroughStyle { + thickness: 1.0.into(), + ..Default::default() + }); + highlight.color = Some(cx.theme().colors().text_muted); + } - (range, highlight) - }, - ), + (range, highlight) + }), ); let completion_label = StyledText::new(completion.label.text.clone()) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 1f23f08b101c56cde25ac6cd2eaffbc2c7f32469..3569fb10a11506131b277b6b36245289f3ad716c 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -24922,6 +24922,7 @@ pub fn diagnostic_style(severity: lsp::DiagnosticSeverity, colors: &StatusColors pub fn styled_runs_for_code_label<'a>( label: &'a CodeLabel, syntax_theme: &'a theme::SyntaxTheme, + local_player: &'a theme::PlayerColor, ) -> impl 'a + Iterator, HighlightStyle)> { let fade_out = HighlightStyle { fade_out: Some(0.35), @@ -24934,7 +24935,17 @@ pub fn styled_runs_for_code_label<'a>( .iter() .enumerate() .flat_map(move |(ix, (range, highlight_id))| { - let style = if let Some(style) = highlight_id.style(syntax_theme) { + let style = if *highlight_id == language::HighlightId::TABSTOP_INSERT_ID { + HighlightStyle { + color: Some(local_player.cursor), + ..Default::default() + } + } else if *highlight_id == language::HighlightId::TABSTOP_REPLACE_ID { + HighlightStyle { + background_color: Some(local_player.selection), + ..Default::default() + } + } else if let Some(style) = highlight_id.style(syntax_theme) { style } else { return Default::default(); diff --git a/crates/language/src/highlight_map.rs b/crates/language/src/highlight_map.rs index 8829eb94ac576be4b1ad7bc6512fd4720cf81dcb..ed9eb5d11d7bc4b156dc9bd660fb10a485129c3d 100644 --- a/crates/language/src/highlight_map.rs +++ b/crates/language/src/highlight_map.rs @@ -51,6 +51,9 @@ impl HighlightMap { } impl HighlightId { + pub const TABSTOP_INSERT_ID: HighlightId = HighlightId(u32::MAX - 1); + pub const TABSTOP_REPLACE_ID: HighlightId = HighlightId(u32::MAX - 2); + pub(crate) fn is_default(&self) -> bool { *self == DEFAULT_SYNTAX_HIGHLIGHT_ID } diff --git a/crates/languages/Cargo.toml b/crates/languages/Cargo.toml index 235f7a94c738f4204421ac685d40c2a71ea980a5..c0aa9c39aacd86e45071bfe7f7289e50cb64b9b1 100644 --- a/crates/languages/Cargo.toml +++ b/crates/languages/Cargo.toml @@ -45,8 +45,8 @@ futures.workspace = true globset.workspace = true gpui.workspace = true http_client.workspace = true -json_schema_store.workspace = true itertools.workspace = true +json_schema_store.workspace = true language.workspace = true log.workspace = true lsp.workspace = true @@ -67,8 +67,9 @@ serde.workspace = true serde_json.workspace = true serde_json_lenient.workspace = true settings.workspace = true +smallvec.workspace = true smol.workspace = true -url.workspace = true +snippet.workspace = true task.workspace = true terminal.workspace = true theme.workspace = true @@ -91,6 +92,7 @@ tree-sitter-regex = { workspace = true, optional = true } tree-sitter-rust = { workspace = true, optional = true } tree-sitter-typescript = { workspace = true, optional = true } tree-sitter-yaml = { workspace = true, optional = true } +url.workspace = true util.workspace = true [dev-dependencies] diff --git a/crates/languages/src/rust.rs b/crates/languages/src/rust.rs index 25b7bfab7d049d3ba3ccef648dbd273e071635f0..ea56c1d203d111cd27f7654a7d8ef54d12cae9c9 100644 --- a/crates/languages/src/rust.rs +++ b/crates/languages/src/rust.rs @@ -13,7 +13,9 @@ use project::project_settings::ProjectSettings; use regex::Regex; use serde_json::json; use settings::Settings as _; +use smallvec::SmallVec; use smol::fs::{self}; +use std::cmp::Reverse; use std::fmt::Display; use std::ops::Range; use std::{ @@ -235,7 +237,7 @@ impl LspAdapter for RustLspAdapter { .or(completion.detail.as_ref()) .map(|detail| detail.trim()); // this tends to contain alias and import information - let detail_left = completion + let mut detail_left = completion .label_details .as_ref() .and_then(|detail| detail.detail.as_deref()); @@ -341,31 +343,88 @@ impl LspAdapter for RustLspAdapter { } } (_, kind) => { - let highlight_name = kind.and_then(|kind| match kind { - lsp::CompletionItemKind::STRUCT - | lsp::CompletionItemKind::INTERFACE - | lsp::CompletionItemKind::ENUM => Some("type"), - lsp::CompletionItemKind::ENUM_MEMBER => Some("variant"), - lsp::CompletionItemKind::KEYWORD => Some("keyword"), - lsp::CompletionItemKind::VALUE | lsp::CompletionItemKind::CONSTANT => { - Some("constant") + let mut label; + let mut runs = vec![]; + + if completion.insert_text_format == Some(lsp::InsertTextFormat::SNIPPET) + && let Some( + lsp::CompletionTextEdit::InsertAndReplace(lsp::InsertReplaceEdit { + new_text, + .. + }) + | lsp::CompletionTextEdit::Edit(lsp::TextEdit { new_text, .. }), + ) = completion.text_edit.as_ref() + && let Ok(mut snippet) = snippet::Snippet::parse(new_text) + && !snippet.tabstops.is_empty() + { + label = String::new(); + + // we never display the final tabstop + snippet.tabstops.remove(snippet.tabstops.len() - 1); + + let mut text_pos = 0; + + let mut all_stop_ranges = snippet + .tabstops + .into_iter() + .flat_map(|stop| stop.ranges) + .collect::>(); + all_stop_ranges.sort_unstable_by_key(|a| (a.start, Reverse(a.end))); + + for range in &all_stop_ranges { + let start_pos = range.start as usize; + let end_pos = range.end as usize; + + label.push_str(&snippet.text[text_pos..end_pos]); + text_pos = end_pos; + + if start_pos == end_pos { + let caret_start = label.len(); + label.push('…'); + runs.push((caret_start..label.len(), HighlightId::TABSTOP_INSERT_ID)); + } else { + runs.push((start_pos..end_pos, HighlightId::TABSTOP_REPLACE_ID)); + } } - _ => None, - }); - let label = completion.label.clone(); - let mut runs = vec![]; - if let Some(highlight_name) = highlight_name { - let highlight_id = language.grammar()?.highlight_id_for_name(highlight_name)?; - runs.push(( - 0..label.rfind('(').unwrap_or(completion.label.len()), - highlight_id, - )); - } else if detail_left.is_none() { - return None; + label.push_str(&snippet.text[text_pos..]); + + if detail_left.is_some_and(|detail_left| detail_left == new_text) { + // We only include the left detail if it isn't the snippet again + detail_left.take(); + } + + runs.extend(language.highlight_text(&Rope::from(&label), 0..label.len())); + } else { + let highlight_name = kind.and_then(|kind| match kind { + lsp::CompletionItemKind::STRUCT + | lsp::CompletionItemKind::INTERFACE + | lsp::CompletionItemKind::ENUM => Some("type"), + lsp::CompletionItemKind::ENUM_MEMBER => Some("variant"), + lsp::CompletionItemKind::KEYWORD => Some("keyword"), + lsp::CompletionItemKind::VALUE | lsp::CompletionItemKind::CONSTANT => { + Some("constant") + } + _ => None, + }); + + label = completion.label.clone(); + + if let Some(highlight_name) = highlight_name { + let highlight_id = + language.grammar()?.highlight_id_for_name(highlight_name)?; + runs.push(( + 0..label.rfind('(').unwrap_or(completion.label.len()), + highlight_id, + )); + } else if detail_left.is_none() { + return None; + } } - mk_label(label, &|| 0..completion.label.len(), runs) + let label_len = label.len(); + + mk_label(label, &|| 0..label_len, runs) } }; @@ -379,6 +438,7 @@ impl LspAdapter for RustLspAdapter { label.text.push(')'); } } + Some(label) } @@ -1371,6 +1431,121 @@ mod tests { vec![(0..11, HighlightId(3)), (13..19, HighlightId(0))], )) ); + + // Snippet with insert tabstop (empty placeholder) + assert_eq!( + adapter + .label_for_completion( + &lsp::CompletionItem { + kind: Some(lsp::CompletionItemKind::SNIPPET), + label: "println!".to_string(), + insert_text_format: Some(lsp::InsertTextFormat::SNIPPET), + text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { + range: lsp::Range::default(), + new_text: "println!(\"$1\", $2)$0".to_string(), + })), + ..Default::default() + }, + &language, + ) + .await, + Some(CodeLabel::new( + "println!(\"…\", …)".to_string(), + 0..8, + vec![ + (10..13, HighlightId::TABSTOP_INSERT_ID), + (16..19, HighlightId::TABSTOP_INSERT_ID), + (0..7, HighlightId(2)), + (7..8, HighlightId(2)), + ], + )) + ); + + // Snippet with replace tabstop (placeholder with default text) + assert_eq!( + adapter + .label_for_completion( + &lsp::CompletionItem { + kind: Some(lsp::CompletionItemKind::SNIPPET), + label: "vec!".to_string(), + insert_text_format: Some(lsp::InsertTextFormat::SNIPPET), + text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { + range: lsp::Range::default(), + new_text: "vec![${1:elem}]$0".to_string(), + })), + ..Default::default() + }, + &language, + ) + .await, + Some(CodeLabel::new( + "vec![elem]".to_string(), + 0..4, + vec![ + (5..9, HighlightId::TABSTOP_REPLACE_ID), + (0..3, HighlightId(2)), + (3..4, HighlightId(2)), + ], + )) + ); + + // Snippet with tabstop appearing more than once + assert_eq!( + adapter + .label_for_completion( + &lsp::CompletionItem { + kind: Some(lsp::CompletionItemKind::SNIPPET), + label: "if let".to_string(), + insert_text_format: Some(lsp::InsertTextFormat::SNIPPET), + text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { + range: lsp::Range::default(), + new_text: "if let ${1:pat} = $1 {\n $0\n}".to_string(), + })), + ..Default::default() + }, + &language, + ) + .await, + Some(CodeLabel::new( + "if let pat = … {\n \n}".to_string(), + 0..6, + vec![ + (7..10, HighlightId::TABSTOP_REPLACE_ID), + (13..16, HighlightId::TABSTOP_INSERT_ID), + (0..2, HighlightId(1)), + (3..6, HighlightId(1)), + ], + )) + ); + + // Snippet with tabstops not in left-to-right order + assert_eq!( + adapter + .label_for_completion( + &lsp::CompletionItem { + kind: Some(lsp::CompletionItemKind::SNIPPET), + label: "for".to_string(), + insert_text_format: Some(lsp::InsertTextFormat::SNIPPET), + text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { + range: lsp::Range::default(), + new_text: "for ${2:item} in ${1:iter} {\n $0\n}".to_string(), + })), + ..Default::default() + }, + &language, + ) + .await, + Some(CodeLabel::new( + "for item in iter {\n \n}".to_string(), + 0..3, + vec![ + (4..8, HighlightId::TABSTOP_REPLACE_ID), + (12..16, HighlightId::TABSTOP_REPLACE_ID), + (0..3, HighlightId(1)), + (9..11, HighlightId(1)), + ], + )) + ); } #[gpui::test] diff --git a/crates/project_symbols/src/project_symbols.rs b/crates/project_symbols/src/project_symbols.rs index 95926116b7b450e36e8c53e192f95dfec76f1f00..61ed715ffd639c532257319d2165d530ae5c0513 100644 --- a/crates/project_symbols/src/project_symbols.rs +++ b/crates/project_symbols/src/project_symbols.rs @@ -224,7 +224,9 @@ impl PickerDelegate for ProjectSymbolsDelegate { let path_style = self.project.read(cx).path_style(cx); let string_match = &self.matches.get(ix)?; let symbol = &self.symbols.get(string_match.candidate_id)?; - let syntax_runs = styled_runs_for_code_label(&symbol.label, cx.theme().syntax()); + let theme = cx.theme(); + let local_player = theme.players().local(); + let syntax_runs = styled_runs_for_code_label(&symbol.label, theme.syntax(), &local_player); let path = match &symbol.path { SymbolLocation::InProject(project_path) => {