Filter and sort suggestions in autocomplete
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
This commit is contained in:
parent
bcbd265de9
commit
b89a39bcb3
11 changed files with 258 additions and 171 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -1547,12 +1547,14 @@ dependencies = [
|
||||||
"collections",
|
"collections",
|
||||||
"ctor",
|
"ctor",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
|
"fuzzy",
|
||||||
"gpui",
|
"gpui",
|
||||||
"itertools",
|
"itertools",
|
||||||
"language",
|
"language",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"log",
|
"log",
|
||||||
"lsp",
|
"lsp",
|
||||||
|
"ordered-float",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"postage",
|
"postage",
|
||||||
"project",
|
"project",
|
||||||
|
|
|
@ -19,6 +19,7 @@ test-support = [
|
||||||
text = { path = "../text" }
|
text = { path = "../text" }
|
||||||
clock = { path = "../clock" }
|
clock = { path = "../clock" }
|
||||||
collections = { path = "../collections" }
|
collections = { path = "../collections" }
|
||||||
|
fuzzy = { path = "../fuzzy" }
|
||||||
gpui = { path = "../gpui" }
|
gpui = { path = "../gpui" }
|
||||||
language = { path = "../language" }
|
language = { path = "../language" }
|
||||||
project = { path = "../project" }
|
project = { path = "../project" }
|
||||||
|
@ -31,6 +32,7 @@ anyhow = "1.0"
|
||||||
itertools = "0.10"
|
itertools = "0.10"
|
||||||
lazy_static = "1.4"
|
lazy_static = "1.4"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
|
ordered-float = "2.1.1"
|
||||||
parking_lot = "0.11"
|
parking_lot = "0.11"
|
||||||
postage = { version = "0.4", features = ["futures-traits"] }
|
postage = { version = "0.4", features = ["futures-traits"] }
|
||||||
rand = { version = "0.8.3", optional = true }
|
rand = { version = "0.8.3", optional = true }
|
||||||
|
|
|
@ -13,6 +13,7 @@ use collections::{BTreeMap, HashMap, HashSet};
|
||||||
pub use display_map::DisplayPoint;
|
pub use display_map::DisplayPoint;
|
||||||
use display_map::*;
|
use display_map::*;
|
||||||
pub use element::*;
|
pub use element::*;
|
||||||
|
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
action,
|
action,
|
||||||
color::Color,
|
color::Color,
|
||||||
|
@ -31,16 +32,17 @@ use language::{
|
||||||
};
|
};
|
||||||
use multi_buffer::MultiBufferChunks;
|
use multi_buffer::MultiBufferChunks;
|
||||||
pub use multi_buffer::{
|
pub use multi_buffer::{
|
||||||
Anchor, AnchorRangeExt, ExcerptId, ExcerptProperties, MultiBuffer, MultiBufferSnapshot,
|
char_kind, Anchor, AnchorRangeExt, CharKind, ExcerptId, ExcerptProperties, MultiBuffer,
|
||||||
ToOffset, ToPoint,
|
MultiBufferSnapshot, ToOffset, ToPoint,
|
||||||
};
|
};
|
||||||
|
use ordered_float::OrderedFloat;
|
||||||
use postage::watch;
|
use postage::watch;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
use smol::Timer;
|
use smol::Timer;
|
||||||
use std::{
|
use std::{
|
||||||
any::TypeId,
|
any::TypeId,
|
||||||
cmp::{self, Ordering},
|
cmp::{self, Ordering, Reverse},
|
||||||
iter::{self, FromIterator},
|
iter::{self, FromIterator},
|
||||||
mem,
|
mem,
|
||||||
ops::{Deref, Range, RangeInclusive, Sub},
|
ops::{Deref, Range, RangeInclusive, Sub},
|
||||||
|
@ -50,7 +52,7 @@ use std::{
|
||||||
pub use sum_tree::Bias;
|
pub use sum_tree::Bias;
|
||||||
use text::rope::TextDimension;
|
use text::rope::TextDimension;
|
||||||
use theme::{DiagnosticStyle, EditorStyle};
|
use theme::{DiagnosticStyle, EditorStyle};
|
||||||
use util::post_inc;
|
use util::{post_inc, ResultExt};
|
||||||
use workspace::{ItemNavHistory, PathOpener, Workspace};
|
use workspace::{ItemNavHistory, PathOpener, Workspace};
|
||||||
|
|
||||||
const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
|
const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
|
||||||
|
@ -430,6 +432,7 @@ struct BracketPairState {
|
||||||
struct CompletionState {
|
struct CompletionState {
|
||||||
initial_position: Anchor,
|
initial_position: Anchor,
|
||||||
completions: Arc<[Completion<Anchor>]>,
|
completions: Arc<[Completion<Anchor>]>,
|
||||||
|
matches: Arc<[StringMatch]>,
|
||||||
selected_item: usize,
|
selected_item: usize,
|
||||||
list: UniformListState,
|
list: UniformListState,
|
||||||
}
|
}
|
||||||
|
@ -453,14 +456,6 @@ pub struct NavigationData {
|
||||||
offset: usize,
|
offset: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Debug)]
|
|
||||||
pub enum CharKind {
|
|
||||||
Newline,
|
|
||||||
Punctuation,
|
|
||||||
Whitespace,
|
|
||||||
Word,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Editor {
|
impl Editor {
|
||||||
pub fn single_line(build_settings: BuildSettings, cx: &mut ViewContext<Self>) -> Self {
|
pub fn single_line(build_settings: BuildSettings, cx: &mut ViewContext<Self>) -> Self {
|
||||||
let buffer = cx.add_model(|cx| Buffer::new(0, String::new(), cx));
|
let buffer = cx.add_model(|cx| Buffer::new(0, String::new(), cx));
|
||||||
|
@ -888,7 +883,7 @@ impl Editor {
|
||||||
mode = SelectMode::Character;
|
mode = SelectMode::Character;
|
||||||
}
|
}
|
||||||
2 => {
|
2 => {
|
||||||
let (range, _) = movement::surrounding_word(&display_map, position);
|
let range = movement::surrounding_word(&display_map, position);
|
||||||
start = buffer.anchor_before(range.start.to_point(&display_map));
|
start = buffer.anchor_before(range.start.to_point(&display_map));
|
||||||
end = buffer.anchor_before(range.end.to_point(&display_map));
|
end = buffer.anchor_before(range.end.to_point(&display_map));
|
||||||
mode = SelectMode::Word(start.clone()..end.clone());
|
mode = SelectMode::Word(start.clone()..end.clone());
|
||||||
|
@ -991,7 +986,7 @@ impl Editor {
|
||||||
if movement::is_inside_word(&display_map, position)
|
if movement::is_inside_word(&display_map, position)
|
||||||
|| original_display_range.contains(&position)
|
|| original_display_range.contains(&position)
|
||||||
{
|
{
|
||||||
let (word_range, _) = movement::surrounding_word(&display_map, position);
|
let word_range = movement::surrounding_word(&display_map, position);
|
||||||
if word_range.start < original_display_range.start {
|
if word_range.start < original_display_range.start {
|
||||||
head = word_range.start.to_point(&display_map);
|
head = word_range.start.to_point(&display_map);
|
||||||
} else {
|
} else {
|
||||||
|
@ -1538,27 +1533,90 @@ impl Editor {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let query = {
|
||||||
|
let buffer = self.buffer.read(cx).read(cx);
|
||||||
|
let offset = position.to_offset(&buffer);
|
||||||
|
let (word_range, kind) = buffer.surrounding_word(offset);
|
||||||
|
if offset > word_range.start && kind == Some(CharKind::Word) {
|
||||||
|
Some(
|
||||||
|
buffer
|
||||||
|
.text_for_range(word_range.start..offset)
|
||||||
|
.collect::<String>(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let completions = self
|
let completions = self
|
||||||
.buffer
|
.buffer
|
||||||
.update(cx, |buffer, cx| buffer.completions(position.clone(), cx));
|
.update(cx, |buffer, cx| buffer.completions(position.clone(), cx));
|
||||||
|
|
||||||
cx.spawn_weak(|this, mut cx| async move {
|
cx.spawn_weak(|this, mut cx| async move {
|
||||||
let completions = completions.await?;
|
let completions = completions.await?;
|
||||||
if !completions.is_empty() {
|
let candidates = completions
|
||||||
if let Some(this) = cx.read(|cx| this.upgrade(cx)) {
|
.iter()
|
||||||
this.update(&mut cx, |this, cx| {
|
.enumerate()
|
||||||
if this.focused {
|
.map(|(id, completion)| {
|
||||||
this.completion_state = Some(CompletionState {
|
StringMatchCandidate::new(
|
||||||
initial_position: position,
|
id,
|
||||||
completions: completions.into(),
|
completion.label()[completion.filter_range()].into(),
|
||||||
selected_item: 0,
|
)
|
||||||
list: Default::default(),
|
})
|
||||||
});
|
.collect::<Vec<_>>();
|
||||||
cx.notify();
|
let mut matches = if let Some(query) = query.as_ref() {
|
||||||
}
|
fuzzy::match_strings(
|
||||||
});
|
&candidates,
|
||||||
|
query,
|
||||||
|
false,
|
||||||
|
100,
|
||||||
|
&Default::default(),
|
||||||
|
cx.background(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
} else {
|
||||||
|
candidates
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(candidate_id, candidate)| StringMatch {
|
||||||
|
candidate_id,
|
||||||
|
score: Default::default(),
|
||||||
|
positions: Default::default(),
|
||||||
|
string: candidate.string,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
|
matches.sort_unstable_by_key(|mat| {
|
||||||
|
(
|
||||||
|
Reverse(OrderedFloat(mat.score)),
|
||||||
|
completions[mat.candidate_id].sort_key(),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
for mat in &mut matches {
|
||||||
|
let filter_start = completions[mat.candidate_id].filter_range().start;
|
||||||
|
for position in &mut mat.positions {
|
||||||
|
*position += filter_start;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(this) = cx.read(|cx| this.upgrade(cx)) {
|
||||||
|
this.update(&mut cx, |this, cx| {
|
||||||
|
if matches.is_empty() {
|
||||||
|
this.completion_state.take();
|
||||||
|
} else if this.focused {
|
||||||
|
this.completion_state = Some(CompletionState {
|
||||||
|
initial_position: position,
|
||||||
|
completions: completions.into(),
|
||||||
|
matches: matches.into(),
|
||||||
|
selected_item: 0,
|
||||||
|
list: Default::default(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
}
|
||||||
Ok::<_, anyhow::Error>(())
|
Ok::<_, anyhow::Error>(())
|
||||||
})
|
})
|
||||||
.detach_and_log_err(cx);
|
.detach_and_log_err(cx);
|
||||||
|
@ -1590,21 +1648,33 @@ impl Editor {
|
||||||
let build_settings = self.build_settings.clone();
|
let build_settings = self.build_settings.clone();
|
||||||
let settings = build_settings(cx);
|
let settings = build_settings(cx);
|
||||||
let completions = state.completions.clone();
|
let completions = state.completions.clone();
|
||||||
|
let matches = state.matches.clone();
|
||||||
let selected_item = state.selected_item;
|
let selected_item = state.selected_item;
|
||||||
UniformList::new(
|
UniformList::new(
|
||||||
state.list.clone(),
|
state.list.clone(),
|
||||||
state.completions.len(),
|
matches.len(),
|
||||||
move |range, items, cx| {
|
move |range, items, cx| {
|
||||||
let settings = build_settings(cx);
|
let settings = build_settings(cx);
|
||||||
let start_ix = range.start;
|
let start_ix = range.start;
|
||||||
for (ix, completion) in completions[range].iter().enumerate() {
|
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 {
|
let item_style = if start_ix + ix == selected_item {
|
||||||
settings.style.autocomplete.selected_item
|
settings.style.autocomplete.selected_item
|
||||||
} else {
|
} else {
|
||||||
settings.style.autocomplete.item
|
settings.style.autocomplete.item
|
||||||
};
|
};
|
||||||
|
let completion = &completions[mat.candidate_id];
|
||||||
items.push(
|
items.push(
|
||||||
Label::new(completion.label().to_string(), settings.style.text.clone())
|
Label::new(completion.label().to_string(), label_style.clone())
|
||||||
|
.with_highlights(mat.positions.clone())
|
||||||
.contained()
|
.contained()
|
||||||
.with_style(item_style)
|
.with_style(item_style)
|
||||||
.boxed(),
|
.boxed(),
|
||||||
|
@ -1614,10 +1684,12 @@ impl Editor {
|
||||||
)
|
)
|
||||||
.with_width_from_item(
|
.with_width_from_item(
|
||||||
state
|
state
|
||||||
.completions
|
.matches
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.max_by_key(|(_, completion)| completion.label().chars().count())
|
.max_by_key(|(_, mat)| {
|
||||||
|
state.completions[mat.candidate_id].label().chars().count()
|
||||||
|
})
|
||||||
.map(|(ix, _)| ix),
|
.map(|(ix, _)| ix),
|
||||||
)
|
)
|
||||||
.contained()
|
.contained()
|
||||||
|
@ -2914,7 +2986,7 @@ impl Editor {
|
||||||
} else if selections.len() == 1 {
|
} else if selections.len() == 1 {
|
||||||
let selection = selections.last_mut().unwrap();
|
let selection = selections.last_mut().unwrap();
|
||||||
if selection.start == selection.end {
|
if selection.start == selection.end {
|
||||||
let (word_range, _) = movement::surrounding_word(
|
let word_range = movement::surrounding_word(
|
||||||
&display_map,
|
&display_map,
|
||||||
selection.start.to_display_point(&display_map),
|
selection.start.to_display_point(&display_map),
|
||||||
);
|
);
|
||||||
|
@ -3534,8 +3606,7 @@ impl Editor {
|
||||||
) where
|
) where
|
||||||
T: ToOffset + ToPoint + Ord + std::marker::Copy + std::fmt::Debug,
|
T: ToOffset + ToPoint + Ord + std::marker::Copy + std::fmt::Debug,
|
||||||
{
|
{
|
||||||
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
let buffer = self.buffer.read(cx).snapshot(cx);
|
||||||
let buffer = &display_map.buffer_snapshot;
|
|
||||||
let old_cursor_position = self.newest_anchor_selection().map(|s| s.head());
|
let old_cursor_position = self.newest_anchor_selection().map(|s| s.head());
|
||||||
selections.sort_unstable_by_key(|s| s.start);
|
selections.sort_unstable_by_key(|s| s.start);
|
||||||
|
|
||||||
|
@ -3580,29 +3651,14 @@ impl Editor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let new_cursor_position = selections
|
let new_cursor_position = selections.iter().max_by_key(|s| s.id).map(|s| s.head());
|
||||||
.iter()
|
|
||||||
.max_by_key(|s| s.id)
|
|
||||||
.map(|s| s.head().to_point(&buffer));
|
|
||||||
if let Some(old_cursor_position) = old_cursor_position {
|
if let Some(old_cursor_position) = old_cursor_position {
|
||||||
if new_cursor_position.is_some() {
|
if let Some(new_cursor_position) = new_cursor_position {
|
||||||
self.push_to_nav_history(old_cursor_position, new_cursor_position, cx);
|
self.push_to_nav_history(
|
||||||
}
|
old_cursor_position,
|
||||||
}
|
Some(new_cursor_position.to_point(&buffer)),
|
||||||
|
cx,
|
||||||
if let Some((completion_state, cursor_position)) =
|
);
|
||||||
self.completion_state.as_ref().zip(new_cursor_position)
|
|
||||||
{
|
|
||||||
let cursor_position = cursor_position.to_display_point(&display_map);
|
|
||||||
let initial_position = completion_state
|
|
||||||
.initial_position
|
|
||||||
.to_display_point(&display_map);
|
|
||||||
|
|
||||||
let (word_range, kind) = movement::surrounding_word(&display_map, initial_position);
|
|
||||||
if kind != Some(CharKind::Word) || !word_range.to_inclusive().contains(&cursor_position)
|
|
||||||
{
|
|
||||||
self.completion_state.take();
|
|
||||||
cx.notify();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3628,6 +3684,21 @@ impl Editor {
|
||||||
})),
|
})),
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if let Some((completion_state, cursor_position)) =
|
||||||
|
self.completion_state.as_ref().zip(new_cursor_position)
|
||||||
|
{
|
||||||
|
let cursor_position = cursor_position.to_offset(&buffer);
|
||||||
|
let (word_range, kind) =
|
||||||
|
buffer.surrounding_word(completion_state.initial_position.clone());
|
||||||
|
if kind == Some(CharKind::Word) && word_range.to_inclusive().contains(&cursor_position)
|
||||||
|
{
|
||||||
|
self.show_completions(&ShowCompletions, cx);
|
||||||
|
} else {
|
||||||
|
self.completion_state.take();
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compute new ranges for any selections that were located in excerpts that have
|
/// Compute new ranges for any selections that were located in excerpts that have
|
||||||
|
@ -4424,18 +4495,6 @@ pub fn settings_builder(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn char_kind(c: char) -> CharKind {
|
|
||||||
if c == '\n' {
|
|
||||||
CharKind::Newline
|
|
||||||
} else if c.is_whitespace() {
|
|
||||||
CharKind::Whitespace
|
|
||||||
} else if c.is_alphanumeric() || c == '_' {
|
|
||||||
CharKind::Word
|
|
||||||
} else {
|
|
||||||
CharKind::Punctuation
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint};
|
use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint};
|
||||||
use crate::{char_kind, CharKind, ToPoint};
|
use crate::{char_kind, CharKind, ToPoint};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use std::{cmp, ops::Range};
|
use std::ops::Range;
|
||||||
|
|
||||||
pub fn left(map: &DisplaySnapshot, mut point: DisplayPoint) -> Result<DisplayPoint> {
|
pub fn left(map: &DisplaySnapshot, mut point: DisplayPoint) -> Result<DisplayPoint> {
|
||||||
if point.column() > 0 {
|
if point.column() > 0 {
|
||||||
|
@ -183,42 +183,20 @@ pub fn is_inside_word(map: &DisplaySnapshot, point: DisplayPoint) -> bool {
|
||||||
prev_char_kind.zip(next_char_kind) == Some((CharKind::Word, CharKind::Word))
|
prev_char_kind.zip(next_char_kind) == Some((CharKind::Word, CharKind::Word))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn surrounding_word(
|
pub fn surrounding_word(map: &DisplaySnapshot, position: DisplayPoint) -> Range<DisplayPoint> {
|
||||||
map: &DisplaySnapshot,
|
let position = map
|
||||||
point: DisplayPoint,
|
.clip_point(position, Bias::Left)
|
||||||
) -> (Range<DisplayPoint>, Option<CharKind>) {
|
.to_offset(map, Bias::Left);
|
||||||
let mut start = map.clip_point(point, Bias::Left).to_offset(map, Bias::Left);
|
let (range, _) = map.buffer_snapshot.surrounding_word(position);
|
||||||
let mut end = start;
|
let start = range
|
||||||
|
.start
|
||||||
let text = &map.buffer_snapshot;
|
.to_point(&map.buffer_snapshot)
|
||||||
let mut next_chars = text.chars_at(start).peekable();
|
.to_display_point(map);
|
||||||
let mut prev_chars = text.reversed_chars_at(start).peekable();
|
let end = range
|
||||||
let word_kind = cmp::max(
|
.end
|
||||||
prev_chars.peek().copied().map(char_kind),
|
.to_point(&map.buffer_snapshot)
|
||||||
next_chars.peek().copied().map(char_kind),
|
.to_display_point(map);
|
||||||
);
|
start..end
|
||||||
|
|
||||||
for ch in prev_chars {
|
|
||||||
if Some(char_kind(ch)) == word_kind {
|
|
||||||
start -= ch.len_utf8();
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for ch in next_chars {
|
|
||||||
if Some(char_kind(ch)) == word_kind {
|
|
||||||
end += ch.len_utf8();
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
(
|
|
||||||
start.to_point(&map.buffer_snapshot).to_display_point(map)
|
|
||||||
..end.to_point(&map.buffer_snapshot).to_display_point(map),
|
|
||||||
word_kind,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@ -412,101 +390,59 @@ mod tests {
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
surrounding_word(&snapshot, DisplayPoint::new(0, 0)),
|
surrounding_word(&snapshot, DisplayPoint::new(0, 0)),
|
||||||
(
|
DisplayPoint::new(0, 0)..DisplayPoint::new(0, 5),
|
||||||
DisplayPoint::new(0, 0)..DisplayPoint::new(0, 5),
|
|
||||||
Some(CharKind::Word)
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
surrounding_word(&snapshot, DisplayPoint::new(0, 2)),
|
surrounding_word(&snapshot, DisplayPoint::new(0, 2)),
|
||||||
(
|
DisplayPoint::new(0, 0)..DisplayPoint::new(0, 5),
|
||||||
DisplayPoint::new(0, 0)..DisplayPoint::new(0, 5),
|
|
||||||
Some(CharKind::Word)
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
surrounding_word(&snapshot, DisplayPoint::new(0, 5)),
|
surrounding_word(&snapshot, DisplayPoint::new(0, 5)),
|
||||||
(
|
DisplayPoint::new(0, 0)..DisplayPoint::new(0, 5),
|
||||||
DisplayPoint::new(0, 0)..DisplayPoint::new(0, 5),
|
|
||||||
Some(CharKind::Word)
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
surrounding_word(&snapshot, DisplayPoint::new(0, 6)),
|
surrounding_word(&snapshot, DisplayPoint::new(0, 6)),
|
||||||
(
|
DisplayPoint::new(0, 6)..DisplayPoint::new(0, 11),
|
||||||
DisplayPoint::new(0, 6)..DisplayPoint::new(0, 11),
|
|
||||||
Some(CharKind::Word)
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
surrounding_word(&snapshot, DisplayPoint::new(0, 7)),
|
surrounding_word(&snapshot, DisplayPoint::new(0, 7)),
|
||||||
(
|
DisplayPoint::new(0, 6)..DisplayPoint::new(0, 11),
|
||||||
DisplayPoint::new(0, 6)..DisplayPoint::new(0, 11),
|
|
||||||
Some(CharKind::Word)
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
surrounding_word(&snapshot, DisplayPoint::new(0, 11)),
|
surrounding_word(&snapshot, DisplayPoint::new(0, 11)),
|
||||||
(
|
DisplayPoint::new(0, 6)..DisplayPoint::new(0, 11),
|
||||||
DisplayPoint::new(0, 6)..DisplayPoint::new(0, 11),
|
|
||||||
Some(CharKind::Word)
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
surrounding_word(&snapshot, DisplayPoint::new(0, 13)),
|
surrounding_word(&snapshot, DisplayPoint::new(0, 13)),
|
||||||
(
|
DisplayPoint::new(0, 11)..DisplayPoint::new(0, 14),
|
||||||
DisplayPoint::new(0, 11)..DisplayPoint::new(0, 14),
|
|
||||||
Some(CharKind::Whitespace)
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
surrounding_word(&snapshot, DisplayPoint::new(0, 14)),
|
surrounding_word(&snapshot, DisplayPoint::new(0, 14)),
|
||||||
(
|
DisplayPoint::new(0, 14)..DisplayPoint::new(0, 19),
|
||||||
DisplayPoint::new(0, 14)..DisplayPoint::new(0, 19),
|
|
||||||
Some(CharKind::Word)
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
surrounding_word(&snapshot, DisplayPoint::new(0, 17)),
|
surrounding_word(&snapshot, DisplayPoint::new(0, 17)),
|
||||||
(
|
DisplayPoint::new(0, 14)..DisplayPoint::new(0, 19),
|
||||||
DisplayPoint::new(0, 14)..DisplayPoint::new(0, 19),
|
|
||||||
Some(CharKind::Word)
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
surrounding_word(&snapshot, DisplayPoint::new(0, 19)),
|
surrounding_word(&snapshot, DisplayPoint::new(0, 19)),
|
||||||
(
|
DisplayPoint::new(0, 14)..DisplayPoint::new(0, 19),
|
||||||
DisplayPoint::new(0, 14)..DisplayPoint::new(0, 19),
|
|
||||||
Some(CharKind::Word)
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
surrounding_word(&snapshot, DisplayPoint::new(1, 0)),
|
surrounding_word(&snapshot, DisplayPoint::new(1, 0)),
|
||||||
(
|
DisplayPoint::new(1, 0)..DisplayPoint::new(1, 4),
|
||||||
DisplayPoint::new(1, 0)..DisplayPoint::new(1, 4),
|
|
||||||
Some(CharKind::Whitespace)
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
surrounding_word(&snapshot, DisplayPoint::new(1, 1)),
|
surrounding_word(&snapshot, DisplayPoint::new(1, 1)),
|
||||||
(
|
DisplayPoint::new(1, 0)..DisplayPoint::new(1, 4),
|
||||||
DisplayPoint::new(1, 0)..DisplayPoint::new(1, 4),
|
|
||||||
Some(CharKind::Whitespace)
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
surrounding_word(&snapshot, DisplayPoint::new(1, 6)),
|
surrounding_word(&snapshot, DisplayPoint::new(1, 6)),
|
||||||
(
|
DisplayPoint::new(1, 4)..DisplayPoint::new(1, 7),
|
||||||
DisplayPoint::new(1, 4)..DisplayPoint::new(1, 7),
|
|
||||||
Some(CharKind::Word)
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
surrounding_word(&snapshot, DisplayPoint::new(1, 7)),
|
surrounding_word(&snapshot, DisplayPoint::new(1, 7)),
|
||||||
(
|
DisplayPoint::new(1, 4)..DisplayPoint::new(1, 7),
|
||||||
DisplayPoint::new(1, 4)..DisplayPoint::new(1, 7),
|
|
||||||
Some(CharKind::Word)
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,6 +50,14 @@ struct History {
|
||||||
group_interval: Duration,
|
group_interval: Duration,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Debug)]
|
||||||
|
pub enum CharKind {
|
||||||
|
Newline,
|
||||||
|
Punctuation,
|
||||||
|
Whitespace,
|
||||||
|
Word,
|
||||||
|
}
|
||||||
|
|
||||||
struct Transaction {
|
struct Transaction {
|
||||||
id: usize,
|
id: usize,
|
||||||
buffer_transactions: HashSet<(usize, text::TransactionId)>,
|
buffer_transactions: HashSet<(usize, text::TransactionId)>,
|
||||||
|
@ -1155,6 +1163,35 @@ impl MultiBufferSnapshot {
|
||||||
.eq(needle.bytes())
|
.eq(needle.bytes())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn surrounding_word<T: ToOffset>(&self, start: T) -> (Range<usize>, Option<CharKind>) {
|
||||||
|
let mut start = start.to_offset(self);
|
||||||
|
let mut end = start;
|
||||||
|
let mut next_chars = self.chars_at(start).peekable();
|
||||||
|
let mut prev_chars = self.reversed_chars_at(start).peekable();
|
||||||
|
let word_kind = cmp::max(
|
||||||
|
prev_chars.peek().copied().map(char_kind),
|
||||||
|
next_chars.peek().copied().map(char_kind),
|
||||||
|
);
|
||||||
|
|
||||||
|
for ch in prev_chars {
|
||||||
|
if Some(char_kind(ch)) == word_kind {
|
||||||
|
start -= ch.len_utf8();
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for ch in next_chars {
|
||||||
|
if Some(char_kind(ch)) == word_kind {
|
||||||
|
end += ch.len_utf8();
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(start..end, word_kind)
|
||||||
|
}
|
||||||
|
|
||||||
fn as_singleton(&self) -> Option<&Excerpt> {
|
fn as_singleton(&self) -> Option<&Excerpt> {
|
||||||
if self.singleton {
|
if self.singleton {
|
||||||
self.excerpts.iter().next()
|
self.excerpts.iter().next()
|
||||||
|
@ -2418,6 +2455,18 @@ impl ToPoint for Point {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn char_kind(c: char) -> CharKind {
|
||||||
|
if c == '\n' {
|
||||||
|
CharKind::Newline
|
||||||
|
} else if c.is_whitespace() {
|
||||||
|
CharKind::Whitespace
|
||||||
|
} else if c.is_alphanumeric() || c == '_' {
|
||||||
|
CharKind::Word
|
||||||
|
} else {
|
||||||
|
CharKind::Punctuation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
|
@ -296,7 +296,7 @@ impl FindBar {
|
||||||
let mut text: String;
|
let mut text: String;
|
||||||
if selection.start == selection.end {
|
if selection.start == selection.end {
|
||||||
let point = selection.start.to_display_point(&display_map);
|
let point = selection.start.to_display_point(&display_map);
|
||||||
let (range, _) = editor::movement::surrounding_word(&display_map, point);
|
let range = editor::movement::surrounding_word(&display_map, point);
|
||||||
let range = range.start.to_offset(&display_map, Bias::Left)
|
let range = range.start.to_offset(&display_map, Bias::Left)
|
||||||
..range.end.to_offset(&display_map, Bias::Right);
|
..range.end.to_offset(&display_map, Bias::Right);
|
||||||
text = display_map.buffer_snapshot.text_for_range(range).collect();
|
text = display_map.buffer_snapshot.text_for_range(range).collect();
|
||||||
|
|
|
@ -181,6 +181,10 @@ pub async fn match_strings(
|
||||||
cancel_flag: &AtomicBool,
|
cancel_flag: &AtomicBool,
|
||||||
background: Arc<executor::Background>,
|
background: Arc<executor::Background>,
|
||||||
) -> Vec<StringMatch> {
|
) -> Vec<StringMatch> {
|
||||||
|
if candidates.is_empty() {
|
||||||
|
return Default::default();
|
||||||
|
}
|
||||||
|
|
||||||
let lowercase_query = query.to_lowercase().chars().collect::<Vec<_>>();
|
let lowercase_query = query.to_lowercase().chars().collect::<Vec<_>>();
|
||||||
let query = query.chars().collect::<Vec<_>>();
|
let query = query.chars().collect::<Vec<_>>();
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ use crate::{
|
||||||
text_layout::RunStyle,
|
text_layout::RunStyle,
|
||||||
FontCache,
|
FontCache,
|
||||||
};
|
};
|
||||||
use anyhow::anyhow;
|
use anyhow::{anyhow, Result};
|
||||||
pub use font_kit::{
|
pub use font_kit::{
|
||||||
metrics::Metrics,
|
metrics::Metrics,
|
||||||
properties::{Properties, Stretch, Style, Weight},
|
properties::{Properties, Stretch, Style, Weight},
|
||||||
|
@ -107,7 +107,7 @@ impl TextStyle {
|
||||||
underline: Option<Underline>,
|
underline: Option<Underline>,
|
||||||
color: Color,
|
color: Color,
|
||||||
font_cache: &FontCache,
|
font_cache: &FontCache,
|
||||||
) -> anyhow::Result<Self> {
|
) -> Result<Self> {
|
||||||
let font_family_name = font_family_name.into();
|
let font_family_name = font_family_name.into();
|
||||||
let font_family_id = font_cache.load_family(&[&font_family_name])?;
|
let font_family_id = font_cache.load_family(&[&font_family_name])?;
|
||||||
let font_id = font_cache.select_font(font_family_id, &font_properties)?;
|
let font_id = font_cache.select_font(font_family_id, &font_properties)?;
|
||||||
|
@ -127,6 +127,15 @@ impl TextStyle {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn highlight(mut self, style: HighlightStyle, font_cache: &FontCache) -> Result<Self> {
|
||||||
|
if self.font_properties != style.font_properties {
|
||||||
|
self.font_id = font_cache.select_font(self.font_family_id, &style.font_properties)?;
|
||||||
|
}
|
||||||
|
self.color = style.color;
|
||||||
|
self.underline = style.underline;
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn to_run(&self) -> RunStyle {
|
pub fn to_run(&self) -> RunStyle {
|
||||||
RunStyle {
|
RunStyle {
|
||||||
font_id: self.font_id,
|
font_id: self.font_id,
|
||||||
|
@ -135,7 +144,7 @@ impl TextStyle {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn from_json(json: TextStyleJson) -> anyhow::Result<Self> {
|
fn from_json(json: TextStyleJson) -> Result<Self> {
|
||||||
FONT_CACHE.with(|font_cache| {
|
FONT_CACHE.with(|font_cache| {
|
||||||
if let Some(font_cache) = font_cache.borrow().as_ref() {
|
if let Some(font_cache) = font_cache.borrow().as_ref() {
|
||||||
let font_properties = properties_from_json(json.weight, json.italic);
|
let font_properties = properties_from_json(json.weight, json.italic);
|
||||||
|
|
|
@ -114,6 +114,7 @@ pub struct Diagnostic {
|
||||||
pub is_disk_based: bool,
|
pub is_disk_based: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct Completion<T> {
|
pub struct Completion<T> {
|
||||||
pub old_range: Range<T>,
|
pub old_range: Range<T>,
|
||||||
pub new_text: String,
|
pub new_text: String,
|
||||||
|
@ -230,7 +231,7 @@ impl File for FakeFile {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn path(&self) -> &Arc<Path> {
|
fn path(&self) -> &Arc<Path> {
|
||||||
&self.path
|
&self.path
|
||||||
}
|
}
|
||||||
|
|
||||||
fn full_path(&self, _: &AppContext) -> PathBuf {
|
fn full_path(&self, _: &AppContext) -> PathBuf {
|
||||||
|
@ -255,8 +256,11 @@ impl File for FakeFile {
|
||||||
cx.spawn(|_| async move { Ok((Default::default(), SystemTime::UNIX_EPOCH)) })
|
cx.spawn(|_| async move { Ok((Default::default(), SystemTime::UNIX_EPOCH)) })
|
||||||
}
|
}
|
||||||
|
|
||||||
fn format_remote(&self, buffer_id: u64, cx: &mut MutableAppContext)
|
fn format_remote(
|
||||||
-> Option<Task<Result<()>>> {
|
&self,
|
||||||
|
buffer_id: u64,
|
||||||
|
cx: &mut MutableAppContext,
|
||||||
|
) -> Option<Task<Result<()>>> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1759,7 +1763,7 @@ impl Buffer {
|
||||||
};
|
};
|
||||||
|
|
||||||
let old_range = this.anchor_before(old_range.start)..this.anchor_after(old_range.end);
|
let old_range = this.anchor_before(old_range.start)..this.anchor_after(old_range.end);
|
||||||
|
|
||||||
Some(Completion {
|
Some(Completion {
|
||||||
old_range,
|
old_range,
|
||||||
new_text,
|
new_text,
|
||||||
|
@ -2511,6 +2515,26 @@ impl<T> Completion<T> {
|
||||||
pub fn label(&self) -> &str {
|
pub fn label(&self) -> &str {
|
||||||
&self.lsp_completion.label
|
&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()])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn contiguous_ranges(
|
pub fn contiguous_ranges(
|
||||||
|
|
|
@ -328,6 +328,7 @@ pub struct AutocompleteStyle {
|
||||||
pub container: ContainerStyle,
|
pub container: ContainerStyle,
|
||||||
pub item: ContainerStyle,
|
pub item: ContainerStyle,
|
||||||
pub selected_item: ContainerStyle,
|
pub selected_item: ContainerStyle,
|
||||||
|
pub match_highlight: HighlightStyle,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Default, Deserialize)]
|
#[derive(Clone, Copy, Default, Deserialize)]
|
||||||
|
|
|
@ -188,7 +188,7 @@ corner_radius = 6
|
||||||
|
|
||||||
[project_panel]
|
[project_panel]
|
||||||
extends = "$panel"
|
extends = "$panel"
|
||||||
padding.top = 6 # ($workspace.tab.height - $project_panel.entry.height) / 2
|
padding.top = 6 # ($workspace.tab.height - $project_panel.entry.height) / 2
|
||||||
|
|
||||||
[project_panel.entry]
|
[project_panel.entry]
|
||||||
text = "$text.1"
|
text = "$text.1"
|
||||||
|
@ -318,6 +318,7 @@ message.highlight_text.color = "$text.3.color"
|
||||||
background = "$surface.2"
|
background = "$surface.2"
|
||||||
border = { width = 1, color = "$border.1" }
|
border = { width = 1, color = "$border.1" }
|
||||||
item.padding = 2
|
item.padding = 2
|
||||||
|
match_highlight = { color = "$editor.syntax.keyword.color", weight = "$editor.syntax.keyword.weight" }
|
||||||
|
|
||||||
[editor.autocomplete.selected_item]
|
[editor.autocomplete.selected_item]
|
||||||
extends = "$editor.autocomplete.item"
|
extends = "$editor.autocomplete.item"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue