Start work on syntax highlighting completions

This commit is contained in:
Max Brunsfeld 2022-02-02 18:14:30 -08:00
parent 45898daf83
commit 439d12cb85
7 changed files with 349 additions and 221 deletions

View file

@ -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<Item = (Range<usize>, HighlightStyle)>,
match_indices: &[usize],
) -> Vec<(Range<usize>, 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<DisplayPoint> {
let point = DisplayPoint::new(row as u32, column as u32);
point..point

View file

@ -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<T> {
pub old_range: Range<T>,
pub new_text: String,
pub label: Option<String>,
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<T> Completion<T> {
pub fn label(&self) -> &str {
self.label.as_deref().unwrap_or(&self.lsp_completion.label)
}
pub fn filter_range(&self) -> Range<usize> {
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 {

View file

@ -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);

View file

@ -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<String> {
fn label_for_completion(
&self,
_: &lsp::CompletionItem,
_: &Language,
) -> Option<CompletionLabel> {
None
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CompletionLabel {
pub text: String,
pub runs: Vec<(Range<usize>, HighlightId)>,
pub filter_range: Range<usize>,
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<String> {
pub fn label_for_completion(
&self,
completion: &lsp::CompletionItem,
) -> Option<CompletionLabel> {
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<usize>, HighlightId)> {
pub fn highlight_text<'a>(
&'a self,
text: &'a Rope,
range: Range<usize>,
) -> Vec<(Range<usize>, 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<Grammar>> {
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<HighlightId> {
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"))]

View file

@ -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,
})
}

View file

@ -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<usize>, HighlightStyle)],
match_indices: &[usize],
) -> Vec<(Range<usize>, 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
},
),
]
);
}
}

View file

@ -32,18 +32,36 @@ impl LspPostProcessor for RustPostProcessor {
}
}
fn label_for_completion(&self, completion: &lsp::CompletionItem) -> Option<String> {
fn label_for_completion(
&self,
completion: &lsp::CompletionItem,
language: &Language,
) -> Option<CompletionLabel> {
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<T>) -> Vec<T>".to_string()),
..Default::default()
}),
Some(CompletionLabel {
text: "hello(&mut Option<T>) -> Vec<T>".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<T>) -> Vec<T>".to_string()),
..Default::default()
}),
Some(CompletionLabel {
text: "hello(&mut Option<T>) -> Vec<T>".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,
})
);
}
}