From 439d12cb8550a095c43c7e2eaa425d4fc4df7ec5 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 2 Feb 2022 18:14:30 -0800 Subject: [PATCH] Start work on syntax highlighting completions --- crates/editor/src/editor.rs | 178 ++++++++++++++++++++++++--- crates/language/src/buffer.rs | 24 +--- crates/language/src/highlight_map.rs | 2 +- crates/language/src/language.rs | 64 ++++++++-- crates/language/src/proto.rs | 8 +- crates/outline/src/outline.rs | 158 +----------------------- crates/zed/src/language.rs | 136 +++++++++++++++++--- 7 files changed, 349 insertions(+), 221 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index acb489efffbd16db24e2c8ceb00375f7d50a1174..8d99bd0e475c4e57c0fa0cdb413658c006bb10a5 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -20,7 +20,7 @@ use gpui::{ color::Color, elements::*, executor, - fonts::TextStyle, + fonts::{self, HighlightStyle, TextStyle}, geometry::vector::{vec2f, Vector2F}, keymap::Binding, text_layout, AppContext, ClipboardItem, Element, ElementBox, Entity, ModelHandle, @@ -489,7 +489,7 @@ impl CompletionState { }); for mat in &mut matches { - let filter_start = self.completions[mat.candidate_id].filter_range().start; + let filter_start = self.completions[mat.candidate_id].label.filter_range.start; for position in &mut mat.positions { *position += filter_start; } @@ -1628,7 +1628,7 @@ impl Editor { .map(|(id, completion)| { StringMatchCandidate::new( id, - completion.lsp_completion.label[completion.filter_range()].into(), + completion.label.text[completion.label.filter_range.clone()].into(), ) }) .collect(), @@ -1710,15 +1710,6 @@ impl Editor { move |range, items, cx| { let settings = build_settings(cx); let start_ix = range.start; - let label_style = LabelStyle { - text: settings.style.text.clone(), - highlight_text: settings - .style - .text - .clone() - .highlight(settings.style.autocomplete.match_highlight, cx.font_cache()) - .log_err(), - }; for (ix, mat) in matches[range].iter().enumerate() { let item_style = if start_ix + ix == selected_item { settings.style.autocomplete.selected_item @@ -1727,8 +1718,20 @@ impl Editor { }; let completion = &completions[mat.candidate_id]; items.push( - Label::new(completion.label().to_string(), label_style.clone()) - .with_highlights(mat.positions.clone()) + Text::new(completion.label.text.clone(), settings.style.text.clone()) + .with_soft_wrap(false) + .with_highlights(combine_syntax_and_fuzzy_match_highlights( + &completion.label.text, + settings.style.text.color.into(), + completion.label.runs.iter().filter_map( + |(range, highlight_id)| { + highlight_id + .style(&settings.style.syntax) + .map(|style| (range.clone(), style)) + }, + ), + &mat.positions, + )) .contained() .with_style(item_style) .boxed(), @@ -1742,7 +1745,11 @@ impl Editor { .iter() .enumerate() .max_by_key(|(_, mat)| { - state.completions[mat.candidate_id].label().chars().count() + state.completions[mat.candidate_id] + .label + .text + .chars() + .count() }) .map(|(ix, _)| ix), ) @@ -4699,6 +4706,77 @@ pub fn settings_builder( }) } +pub fn combine_syntax_and_fuzzy_match_highlights( + text: &str, + default_style: HighlightStyle, + syntax_ranges: impl Iterator, HighlightStyle)>, + match_indices: &[usize], +) -> Vec<(Range, HighlightStyle)> { + let mut result = Vec::new(); + let mut match_indices = match_indices.iter().copied().peekable(); + + for (range, mut syntax_highlight) in syntax_ranges.chain([(usize::MAX..0, Default::default())]) + { + syntax_highlight.font_properties.weight(Default::default()); + + // Add highlights for any fuzzy match characters before the next + // syntax highlight range. + while let Some(&match_index) = match_indices.peek() { + if match_index >= range.start { + break; + } + match_indices.next(); + let end_index = char_ix_after(match_index, text); + let mut match_style = default_style; + match_style.font_properties.weight(fonts::Weight::BOLD); + result.push((match_index..end_index, match_style)); + } + + if range.start == usize::MAX { + break; + } + + // Add highlights for any fuzzy match characters within the + // syntax highlight range. + let mut offset = range.start; + while let Some(&match_index) = match_indices.peek() { + if match_index >= range.end { + break; + } + + match_indices.next(); + if match_index > offset { + result.push((offset..match_index, syntax_highlight)); + } + + let mut end_index = char_ix_after(match_index, text); + while let Some(&next_match_index) = match_indices.peek() { + if next_match_index == end_index && next_match_index < range.end { + end_index = char_ix_after(next_match_index, text); + match_indices.next(); + } else { + break; + } + } + + let mut match_style = syntax_highlight; + match_style.font_properties.weight(fonts::Weight::BOLD); + result.push((match_index..end_index, match_style)); + offset = end_index; + } + + if offset < range.end { + result.push((offset..range.end, syntax_highlight)); + } + } + + fn char_ix_after(ix: usize, text: &str) -> usize { + ix + text[ix..].chars().next().unwrap().len_utf8() + } + + result +} + #[cfg(test)] mod tests { use super::*; @@ -7327,6 +7405,76 @@ mod tests { }); } + #[test] + fn test_combine_syntax_and_fuzzy_match_highlights() { + let string = "abcdefghijklmnop"; + let default = HighlightStyle::default(); + let syntax_ranges = [ + ( + 0..3, + HighlightStyle { + color: Color::red(), + ..default + }, + ), + ( + 4..8, + HighlightStyle { + color: Color::green(), + ..default + }, + ), + ]; + let match_indices = [4, 6, 7, 8]; + assert_eq!( + combine_syntax_and_fuzzy_match_highlights( + &string, + default, + syntax_ranges.into_iter(), + &match_indices, + ), + &[ + ( + 0..3, + HighlightStyle { + color: Color::red(), + ..default + }, + ), + ( + 4..5, + HighlightStyle { + color: Color::green(), + font_properties: *fonts::Properties::default().weight(fonts::Weight::BOLD), + ..default + }, + ), + ( + 5..6, + HighlightStyle { + color: Color::green(), + ..default + }, + ), + ( + 6..8, + HighlightStyle { + color: Color::green(), + font_properties: *fonts::Properties::default().weight(fonts::Weight::BOLD), + ..default + }, + ), + ( + 8..9, + HighlightStyle { + font_properties: *fonts::Properties::default().weight(fonts::Weight::BOLD), + ..default + }, + ), + ] + ); + } + fn empty_range(row: usize, column: usize) -> Range { let point = DisplayPoint::new(row as u32, column as u32); point..point diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 9342e956fb660a222444eb3b71ec5132688f38ba..8c026b08bfe8ffc57eb5b8f71554d155fcfb032d 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -7,7 +7,7 @@ pub use crate::{ use crate::{ diagnostic_set::{DiagnosticEntry, DiagnosticGroup}, outline::OutlineItem, - range_from_lsp, Outline, ToLspPosition, + range_from_lsp, CompletionLabel, Outline, ToLspPosition, }; use anyhow::{anyhow, Result}; use clock::ReplicaId; @@ -114,7 +114,7 @@ pub struct Diagnostic { pub struct Completion { pub old_range: Range, pub new_text: String, - pub label: Option, + pub label: CompletionLabel, pub lsp_completion: lsp::CompletionItem, } @@ -1829,7 +1829,7 @@ impl Buffer { Some(Completion { old_range: this.anchor_before(old_range.start)..this.anchor_after(old_range.end), new_text, - label: language.as_ref().and_then(|l| l.label_for_completion(&lsp_completion)), + label: language.as_ref().and_then(|l| l.label_for_completion(&lsp_completion)).unwrap_or_else(|| CompletionLabel::plain(&lsp_completion)), lsp_completion, }) } else { @@ -2664,28 +2664,12 @@ impl Default for Diagnostic { } impl Completion { - pub fn label(&self) -> &str { - self.label.as_deref().unwrap_or(&self.lsp_completion.label) - } - - pub fn filter_range(&self) -> Range { - if let Some(filter_text) = self.lsp_completion.filter_text.as_deref() { - if let Some(start) = self.label().find(filter_text) { - start..start + filter_text.len() - } else { - 0..self.label().len() - } - } else { - 0..self.label().len() - } - } - pub fn sort_key(&self) -> (usize, &str) { let kind_key = match self.lsp_completion.kind { Some(lsp::CompletionItemKind::VARIABLE) => 0, _ => 1, }; - (kind_key, &self.label()[self.filter_range()]) + (kind_key, &self.label.text[self.label.filter_range.clone()]) } pub fn is_snippet(&self) -> bool { diff --git a/crates/language/src/highlight_map.rs b/crates/language/src/highlight_map.rs index 5e1c7e430465ccc596c8cc09a19948ccb116408f..75e3c8526c5969ecabfe49b24929025a13848f3e 100644 --- a/crates/language/src/highlight_map.rs +++ b/crates/language/src/highlight_map.rs @@ -5,7 +5,7 @@ use theme::SyntaxTheme; #[derive(Clone, Debug)] pub struct HighlightMap(Arc<[HighlightId]>); -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct HighlightId(pub u32); const DEFAULT_HIGHLIGHT_ID: HighlightId = HighlightId(u32::MAX); diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 1dc35e0efa6d0c8ff90e094dd3837c4ef58b4b46..e54e0f49a40422b0195fd16bfc3dd868f1976b58 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -49,11 +49,23 @@ pub trait ToLspPosition { pub trait LspPostProcessor: 'static + Send + Sync { fn process_diagnostics(&self, diagnostics: &mut lsp::PublishDiagnosticsParams); - fn label_for_completion(&self, _completion: &lsp::CompletionItem) -> Option { + fn label_for_completion( + &self, + _: &lsp::CompletionItem, + _: &Language, + ) -> Option { None } } +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct CompletionLabel { + pub text: String, + pub runs: Vec<(Range, HighlightId)>, + pub filter_range: Range, + pub left_aligned_len: usize, +} + #[derive(Default, Deserialize)] pub struct LanguageConfig { pub name: String, @@ -253,24 +265,26 @@ impl Language { } } - pub fn label_for_completion(&self, completion: &lsp::CompletionItem) -> Option { + pub fn label_for_completion( + &self, + completion: &lsp::CompletionItem, + ) -> Option { self.lsp_post_processor - .as_ref() - .and_then(|p| p.label_for_completion(completion)) + .as_ref()? + .label_for_completion(completion, self) } - pub fn highlight_text<'a>(&'a self, text: &'a Rope) -> Vec<(Range, HighlightId)> { + pub fn highlight_text<'a>( + &'a self, + text: &'a Rope, + range: Range, + ) -> Vec<(Range, HighlightId)> { let mut result = Vec::new(); if let Some(grammar) = &self.grammar { let tree = grammar.parse_text(text, None); let mut offset = 0; - for chunk in BufferChunks::new( - text, - 0..text.len(), - Some(&tree), - self.grammar.as_ref(), - vec![], - ) { + for chunk in BufferChunks::new(text, range, Some(&tree), self.grammar.as_ref(), vec![]) + { let end_offset = offset + chunk.text.len(); if let Some(highlight_id) = chunk.highlight_id { result.push((offset..end_offset, highlight_id)); @@ -291,6 +305,10 @@ impl Language { HighlightMap::new(grammar.highlights_query.capture_names(), theme); } } + + pub fn grammar(&self) -> Option<&Arc> { + self.grammar.as_ref() + } } impl Grammar { @@ -316,6 +334,28 @@ impl Grammar { pub fn highlight_map(&self) -> HighlightMap { self.highlight_map.lock().clone() } + + pub fn highlight_id_for_name(&self, name: &str) -> Option { + let capture_id = self.highlights_query.capture_index_for_name(name)?; + Some(self.highlight_map.lock().get(capture_id)) + } +} + +impl CompletionLabel { + fn plain(completion: &lsp::CompletionItem) -> Self { + let mut result = Self { + text: completion.label.clone(), + runs: Vec::new(), + left_aligned_len: completion.label.len(), + filter_range: 0..completion.label.len(), + }; + if let Some(filter_text) = &completion.filter_text { + if let Some(ix) = completion.label.find(filter_text) { + result.filter_range = ix..ix + filter_text.len(); + } + } + result + } } #[cfg(any(test, feature = "test-support"))] diff --git a/crates/language/src/proto.rs b/crates/language/src/proto.rs index c934e0f3e4be7681d12b26a098fa9d78e08033e1..82787ec5712291041665d8de4280d12598d61866 100644 --- a/crates/language/src/proto.rs +++ b/crates/language/src/proto.rs @@ -1,4 +1,6 @@ -use crate::{diagnostic_set::DiagnosticEntry, Completion, Diagnostic, Language, Operation}; +use crate::{ + diagnostic_set::DiagnosticEntry, Completion, CompletionLabel, Diagnostic, Language, Operation, +}; use anyhow::{anyhow, Result}; use clock::ReplicaId; use collections::HashSet; @@ -403,7 +405,9 @@ pub fn deserialize_completion( Ok(Completion { old_range: old_start..old_end, new_text: completion.new_text, - label: language.and_then(|l| l.label_for_completion(&lsp_completion)), + label: language + .and_then(|l| l.label_for_completion(&lsp_completion)) + .unwrap_or(CompletionLabel::plain(&lsp_completion)), lsp_completion, }) } diff --git a/crates/outline/src/outline.rs b/crates/outline/src/outline.rs index b6921ce69f63c18d9cb92df28bbd61af97701de8..4518ae6afc65ca7587d7918c9be81146082a3581 100644 --- a/crates/outline/src/outline.rs +++ b/crates/outline/src/outline.rs @@ -1,12 +1,11 @@ use editor::{ - display_map::ToDisplayPoint, Anchor, AnchorRangeExt, Autoscroll, DisplayPoint, Editor, - EditorSettings, ToPoint, + combine_syntax_and_fuzzy_match_highlights, display_map::ToDisplayPoint, Anchor, AnchorRangeExt, + Autoscroll, DisplayPoint, Editor, EditorSettings, ToPoint, }; use fuzzy::StringMatch; use gpui::{ action, elements::*, - fonts::{self, HighlightStyle}, geometry::vector::Vector2F, keymap::{self, Binding}, AppContext, Axis, Entity, MutableAppContext, RenderContext, View, ViewContext, ViewHandle, @@ -17,7 +16,6 @@ use ordered_float::OrderedFloat; use postage::watch; use std::{ cmp::{self, Reverse}, - ops::Range, sync::Arc, }; use workspace::{ @@ -362,7 +360,7 @@ impl OutlineView { .with_highlights(combine_syntax_and_fuzzy_match_highlights( &outline_item.text, style.label.text.clone().into(), - &outline_item.highlight_ranges, + outline_item.highlight_ranges.iter().cloned(), &string_match.positions, )) .contained() @@ -372,153 +370,3 @@ impl OutlineView { .boxed() } } - -fn combine_syntax_and_fuzzy_match_highlights( - text: &str, - default_style: HighlightStyle, - syntax_ranges: &[(Range, HighlightStyle)], - match_indices: &[usize], -) -> Vec<(Range, HighlightStyle)> { - let mut result = Vec::new(); - let mut match_indices = match_indices.iter().copied().peekable(); - - for (range, mut syntax_highlight) in syntax_ranges - .iter() - .cloned() - .chain([(usize::MAX..0, Default::default())]) - { - syntax_highlight.font_properties.weight(Default::default()); - - // Add highlights for any fuzzy match characters before the next - // syntax highlight range. - while let Some(&match_index) = match_indices.peek() { - if match_index >= range.start { - break; - } - match_indices.next(); - let end_index = char_ix_after(match_index, text); - let mut match_style = default_style; - match_style.font_properties.weight(fonts::Weight::BOLD); - result.push((match_index..end_index, match_style)); - } - - if range.start == usize::MAX { - break; - } - - // Add highlights for any fuzzy match characters within the - // syntax highlight range. - let mut offset = range.start; - while let Some(&match_index) = match_indices.peek() { - if match_index >= range.end { - break; - } - - match_indices.next(); - if match_index > offset { - result.push((offset..match_index, syntax_highlight)); - } - - let mut end_index = char_ix_after(match_index, text); - while let Some(&next_match_index) = match_indices.peek() { - if next_match_index == end_index && next_match_index < range.end { - end_index = char_ix_after(next_match_index, text); - match_indices.next(); - } else { - break; - } - } - - let mut match_style = syntax_highlight; - match_style.font_properties.weight(fonts::Weight::BOLD); - result.push((match_index..end_index, match_style)); - offset = end_index; - } - - if offset < range.end { - result.push((offset..range.end, syntax_highlight)); - } - } - - result -} - -fn char_ix_after(ix: usize, text: &str) -> usize { - ix + text[ix..].chars().next().unwrap().len_utf8() -} - -#[cfg(test)] -mod tests { - use super::*; - use gpui::{color::Color, fonts::HighlightStyle}; - - #[test] - fn test_combine_syntax_and_fuzzy_match_highlights() { - let string = "abcdefghijklmnop"; - let default = HighlightStyle::default(); - let syntax_ranges = [ - ( - 0..3, - HighlightStyle { - color: Color::red(), - ..default - }, - ), - ( - 4..8, - HighlightStyle { - color: Color::green(), - ..default - }, - ), - ]; - let match_indices = [4, 6, 7, 8]; - assert_eq!( - combine_syntax_and_fuzzy_match_highlights( - &string, - default, - &syntax_ranges, - &match_indices, - ), - &[ - ( - 0..3, - HighlightStyle { - color: Color::red(), - ..default - }, - ), - ( - 4..5, - HighlightStyle { - color: Color::green(), - font_properties: *fonts::Properties::default().weight(fonts::Weight::BOLD), - ..default - }, - ), - ( - 5..6, - HighlightStyle { - color: Color::green(), - ..default - }, - ), - ( - 6..8, - HighlightStyle { - color: Color::green(), - font_properties: *fonts::Properties::default().weight(fonts::Weight::BOLD), - ..default - }, - ), - ( - 8..9, - HighlightStyle { - font_properties: *fonts::Properties::default().weight(fonts::Weight::BOLD), - ..default - }, - ), - ] - ); - } -} diff --git a/crates/zed/src/language.rs b/crates/zed/src/language.rs index 9a27e117b6fdf50713a37f114366ca480434fe53..b0634b4eee4a6a35997a29a9545a5b69b65c40f8 100644 --- a/crates/zed/src/language.rs +++ b/crates/zed/src/language.rs @@ -32,18 +32,36 @@ impl LspPostProcessor for RustPostProcessor { } } - fn label_for_completion(&self, completion: &lsp::CompletionItem) -> Option { + fn label_for_completion( + &self, + completion: &lsp::CompletionItem, + language: &Language, + ) -> Option { let detail = completion.detail.as_ref()?; match completion.kind { - Some( - lsp::CompletionItemKind::CONSTANT - | lsp::CompletionItemKind::FIELD - | lsp::CompletionItemKind::VARIABLE, - ) => { - let mut label = completion.label.clone(); - label.push_str(": "); - label.push_str(detail); - Some(label) + Some(lsp::CompletionItemKind::FIELD) => { + let name = &completion.label; + let text = format!("{}: {}", name, detail); + let source = Rope::from(format!("struct S {{ {} }}", text).as_str()); + let runs = language.highlight_text(&source, 11..11 + text.len()); + return Some(CompletionLabel { + text, + runs, + filter_range: 0..name.len(), + left_aligned_len: name.len(), + }); + } + Some(lsp::CompletionItemKind::CONSTANT | lsp::CompletionItemKind::VARIABLE) => { + let name = &completion.label; + let text = format!("{}: {}", name, detail); + let source = Rope::from(format!("let {} = ();", text).as_str()); + let runs = language.highlight_text(&source, 4..4 + text.len()); + return Some(CompletionLabel { + text, + runs, + filter_range: 0..name.len(), + left_aligned_len: name.len(), + }); } Some(lsp::CompletionItemKind::FUNCTION | lsp::CompletionItemKind::METHOD) => { lazy_static! { @@ -51,13 +69,20 @@ impl LspPostProcessor for RustPostProcessor { } if detail.starts_with("fn(") { - Some(REGEX.replace(&completion.label, &detail[2..]).to_string()) - } else { - None + let text = REGEX.replace(&completion.label, &detail[2..]).to_string(); + let source = Rope::from(format!("fn {} {{}}", text).as_str()); + let runs = language.highlight_text(&source, 3..3 + text.len()); + return Some(CompletionLabel { + left_aligned_len: text.find("->").unwrap_or(text.len()), + filter_range: 0..completion.label.find('(').unwrap_or(text.len()), + text, + runs, + }); } } - _ => None, + _ => {} } + None } } @@ -100,9 +125,10 @@ fn load_query(path: &str) -> Cow<'static, str> { #[cfg(test)] mod tests { + use super::*; + use gpui::color::Color; use language::LspPostProcessor; - - use super::RustPostProcessor; + use theme::SyntaxTheme; #[test] fn test_process_rust_diagnostics() { @@ -144,4 +170,82 @@ mod tests { "cannot borrow `self.d` as mutable\n`self` is a `&` reference" ); } + + #[test] + fn test_process_rust_completions() { + let language = rust(); + let grammar = language.grammar().unwrap(); + let theme = SyntaxTheme::new(vec![ + ("type".into(), Color::green().into()), + ("keyword".into(), Color::blue().into()), + ("function".into(), Color::red().into()), + ("property".into(), Color::white().into()), + ]); + + language.set_theme(&theme); + + let highlight_function = grammar.highlight_id_for_name("function").unwrap(); + let highlight_type = grammar.highlight_id_for_name("type").unwrap(); + let highlight_keyword = grammar.highlight_id_for_name("keyword").unwrap(); + let highlight_field = grammar.highlight_id_for_name("property").unwrap(); + + assert_eq!( + language.label_for_completion(&lsp::CompletionItem { + kind: Some(lsp::CompletionItemKind::FUNCTION), + label: "hello(…)".to_string(), + detail: Some("fn(&mut Option) -> Vec".to_string()), + ..Default::default() + }), + Some(CompletionLabel { + text: "hello(&mut Option) -> Vec".to_string(), + filter_range: 0..5, + runs: vec![ + (0..5, highlight_function), + (7..10, highlight_keyword), + (11..17, highlight_type), + (18..19, highlight_type), + (25..28, highlight_type), + (29..30, highlight_type), + ], + left_aligned_len: 22, + }) + ); + + assert_eq!( + language.label_for_completion(&lsp::CompletionItem { + kind: Some(lsp::CompletionItemKind::FIELD), + label: "len".to_string(), + detail: Some("usize".to_string()), + ..Default::default() + }), + Some(CompletionLabel { + text: "len: usize".to_string(), + filter_range: 0..3, + runs: vec![(0..3, highlight_field), (5..10, highlight_type),], + left_aligned_len: 3, + }) + ); + + assert_eq!( + language.label_for_completion(&lsp::CompletionItem { + kind: Some(lsp::CompletionItemKind::FUNCTION), + label: "hello(…)".to_string(), + detail: Some("fn(&mut Option) -> Vec".to_string()), + ..Default::default() + }), + Some(CompletionLabel { + text: "hello(&mut Option) -> Vec".to_string(), + filter_range: 0..5, + runs: vec![ + (0..5, highlight_function), + (7..10, highlight_keyword), + (11..17, highlight_type), + (18..19, highlight_type), + (25..28, highlight_type), + (29..30, highlight_type), + ], + left_aligned_len: 22, + }) + ); + } }