Add word_characters
to language overrides & use for more things
Use word_characters to feed completion trigger characters as well and also recognize kebab as a potential sub-word splitter. This is fine for non-kebab-case languages because we'd only ever attempt to split a word with a kebab in it in language scopes which are kebab-cased Co-Authored-By: Max Brunsfeld <max@zed.dev>
This commit is contained in:
parent
a394aaa524
commit
fc457d45f5
13 changed files with 178 additions and 56 deletions
|
@ -2654,6 +2654,7 @@ impl Editor {
|
|||
false
|
||||
});
|
||||
}
|
||||
|
||||
fn completion_query(buffer: &MultiBufferSnapshot, position: impl ToOffset) -> Option<String> {
|
||||
let offset = position.to_offset(buffer);
|
||||
let (word_range, kind) = buffer.surrounding_word(offset);
|
||||
|
@ -8878,6 +8879,7 @@ pub fn split_words<'a>(text: &'a str) -> impl std::iter::Iterator<Item = &'a str
|
|||
None
|
||||
})
|
||||
.flat_map(|word| word.split_inclusive('_'))
|
||||
.flat_map(|word| word.split_inclusive('-'))
|
||||
}
|
||||
|
||||
trait RangeToAnchorExt {
|
||||
|
|
|
@ -19,7 +19,8 @@ use gpui::{
|
|||
use indoc::indoc;
|
||||
use language::{
|
||||
language_settings::{AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent},
|
||||
BracketPairConfig, FakeLspAdapter, LanguageConfig, LanguageRegistry, Point,
|
||||
BracketPairConfig, FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageRegistry,
|
||||
Override, Point,
|
||||
};
|
||||
use parking_lot::Mutex;
|
||||
use project::project_settings::{LspSettings, ProjectSettings};
|
||||
|
@ -7611,6 +7612,99 @@ async fn test_completions_with_additional_edits(cx: &mut gpui::TestAppContext) {
|
|||
cx.assert_editor_state(indoc! {"fn main() { let a = Some(2)ˇ; }"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let mut cx = EditorLspTestContext::new(
|
||||
Language::new(
|
||||
LanguageConfig {
|
||||
path_suffixes: vec!["jsx".into()],
|
||||
overrides: [(
|
||||
"element".into(),
|
||||
LanguageConfigOverride {
|
||||
word_characters: Override::Set(['-'].into_iter().collect()),
|
||||
..Default::default()
|
||||
},
|
||||
)]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_typescript::language_tsx()),
|
||||
)
|
||||
.with_override_query("(jsx_self_closing_element) @element")
|
||||
.unwrap(),
|
||||
Default::default(),
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
cx.lsp
|
||||
.handle_request::<lsp::request::Completion, _, _>(move |_, _| async move {
|
||||
Ok(Some(lsp::CompletionResponse::Array(vec![
|
||||
lsp::CompletionItem {
|
||||
label: "bg-blue".into(),
|
||||
..Default::default()
|
||||
},
|
||||
lsp::CompletionItem {
|
||||
label: "bg-red".into(),
|
||||
..Default::default()
|
||||
},
|
||||
lsp::CompletionItem {
|
||||
label: "bg-yellow".into(),
|
||||
..Default::default()
|
||||
},
|
||||
])))
|
||||
});
|
||||
|
||||
cx.set_state(r#"<p class="bgˇ" />"#);
|
||||
|
||||
// Trigger completion when typing a dash, because the dash is an extra
|
||||
// word character in the 'element' scope, which contains the cursor.
|
||||
cx.simulate_keystroke("-");
|
||||
cx.foreground().run_until_parked();
|
||||
cx.update_editor(|editor, _| {
|
||||
if let Some(ContextMenu::Completions(menu)) = &editor.context_menu {
|
||||
assert_eq!(
|
||||
menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
|
||||
&["bg-red", "bg-blue", "bg-yellow"]
|
||||
);
|
||||
} else {
|
||||
panic!("expected completion menu to be open");
|
||||
}
|
||||
});
|
||||
|
||||
cx.simulate_keystroke("l");
|
||||
cx.foreground().run_until_parked();
|
||||
cx.update_editor(|editor, _| {
|
||||
if let Some(ContextMenu::Completions(menu)) = &editor.context_menu {
|
||||
assert_eq!(
|
||||
menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
|
||||
&["bg-blue", "bg-yellow"]
|
||||
);
|
||||
} else {
|
||||
panic!("expected completion menu to be open");
|
||||
}
|
||||
});
|
||||
|
||||
// When filtering completions, consider the character after the '-' to
|
||||
// be the start of a subword.
|
||||
cx.set_state(r#"<p class="yelˇ" />"#);
|
||||
cx.simulate_keystroke("l");
|
||||
cx.foreground().run_until_parked();
|
||||
cx.update_editor(|editor, _| {
|
||||
if let Some(ContextMenu::Completions(menu)) = &editor.context_menu {
|
||||
assert_eq!(
|
||||
menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
|
||||
&["bg-blue", "bg-yellow"]
|
||||
);
|
||||
} else {
|
||||
panic!("expected completion menu to be open");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
|
||||
let point = DisplayPoint::new(row as u32, column as u32);
|
||||
point..point
|
||||
|
|
|
@ -177,20 +177,20 @@ pub fn line_end(
|
|||
|
||||
pub fn previous_word_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
|
||||
let raw_point = point.to_point(map);
|
||||
let language = map.buffer_snapshot.language_at(raw_point);
|
||||
let scope = map.buffer_snapshot.language_scope_at(raw_point);
|
||||
|
||||
find_preceding_boundary(map, point, |left, right| {
|
||||
(char_kind(language, left) != char_kind(language, right) && !right.is_whitespace())
|
||||
(char_kind(&scope, left) != char_kind(&scope, right) && !right.is_whitespace())
|
||||
|| left == '\n'
|
||||
})
|
||||
}
|
||||
|
||||
pub fn previous_subword_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
|
||||
let raw_point = point.to_point(map);
|
||||
let language = map.buffer_snapshot.language_at(raw_point);
|
||||
let scope = map.buffer_snapshot.language_scope_at(raw_point);
|
||||
find_preceding_boundary(map, point, |left, right| {
|
||||
let is_word_start =
|
||||
char_kind(language, left) != char_kind(language, right) && !right.is_whitespace();
|
||||
char_kind(&scope, left) != char_kind(&scope, right) && !right.is_whitespace();
|
||||
let is_subword_start =
|
||||
left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
|
||||
is_word_start || is_subword_start || left == '\n'
|
||||
|
@ -199,19 +199,19 @@ pub fn previous_subword_start(map: &DisplaySnapshot, point: DisplayPoint) -> Dis
|
|||
|
||||
pub fn next_word_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
|
||||
let raw_point = point.to_point(map);
|
||||
let language = map.buffer_snapshot.language_at(raw_point);
|
||||
let scope = map.buffer_snapshot.language_scope_at(raw_point);
|
||||
find_boundary(map, point, |left, right| {
|
||||
(char_kind(language, left) != char_kind(language, right) && !left.is_whitespace())
|
||||
(char_kind(&scope, left) != char_kind(&scope, right) && !left.is_whitespace())
|
||||
|| right == '\n'
|
||||
})
|
||||
}
|
||||
|
||||
pub fn next_subword_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
|
||||
let raw_point = point.to_point(map);
|
||||
let language = map.buffer_snapshot.language_at(raw_point);
|
||||
let scope = map.buffer_snapshot.language_scope_at(raw_point);
|
||||
find_boundary(map, point, |left, right| {
|
||||
let is_word_end =
|
||||
(char_kind(language, left) != char_kind(language, right)) && !left.is_whitespace();
|
||||
(char_kind(&scope, left) != char_kind(&scope, right)) && !left.is_whitespace();
|
||||
let is_subword_end =
|
||||
left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
|
||||
is_word_end || is_subword_end || right == '\n'
|
||||
|
@ -399,14 +399,14 @@ pub fn find_boundary_in_line(
|
|||
|
||||
pub fn is_inside_word(map: &DisplaySnapshot, point: DisplayPoint) -> bool {
|
||||
let raw_point = point.to_point(map);
|
||||
let language = map.buffer_snapshot.language_at(raw_point);
|
||||
let scope = map.buffer_snapshot.language_scope_at(raw_point);
|
||||
let ix = map.clip_point(point, Bias::Left).to_offset(map, Bias::Left);
|
||||
let text = &map.buffer_snapshot;
|
||||
let next_char_kind = text.chars_at(ix).next().map(|c| char_kind(language, c));
|
||||
let next_char_kind = text.chars_at(ix).next().map(|c| char_kind(&scope, c));
|
||||
let prev_char_kind = text
|
||||
.reversed_chars_at(ix)
|
||||
.next()
|
||||
.map(|c| char_kind(language, c));
|
||||
.map(|c| char_kind(&scope, c));
|
||||
prev_char_kind.zip(next_char_kind) == Some((CharKind::Word, CharKind::Word))
|
||||
}
|
||||
|
||||
|
|
|
@ -1360,11 +1360,13 @@ impl MultiBuffer {
|
|||
return false;
|
||||
}
|
||||
|
||||
if char.is_alphanumeric() || char == '_' {
|
||||
let snapshot = self.snapshot(cx);
|
||||
let position = position.to_offset(&snapshot);
|
||||
let scope = snapshot.language_scope_at(position);
|
||||
if char_kind(&scope, char) == CharKind::Word {
|
||||
return true;
|
||||
}
|
||||
|
||||
let snapshot = self.snapshot(cx);
|
||||
let anchor = snapshot.anchor_before(position);
|
||||
anchor
|
||||
.buffer_id
|
||||
|
@ -1866,8 +1868,8 @@ impl MultiBufferSnapshot {
|
|||
let mut next_chars = self.chars_at(start).peekable();
|
||||
let mut prev_chars = self.reversed_chars_at(start).peekable();
|
||||
|
||||
let language = self.language_at(start);
|
||||
let kind = |c| char_kind(language, c);
|
||||
let scope = self.language_scope_at(start);
|
||||
let kind = |c| char_kind(&scope, c);
|
||||
let word_kind = cmp::max(
|
||||
prev_chars.peek().copied().map(kind),
|
||||
next_chars.peek().copied().map(kind),
|
||||
|
|
|
@ -50,7 +50,7 @@ impl<'a> EditorLspTestContext<'a> {
|
|||
language
|
||||
.path_suffixes()
|
||||
.first()
|
||||
.unwrap_or(&"txt".to_string())
|
||||
.expect("language must have a path suffix for EditorLspTestContext")
|
||||
);
|
||||
|
||||
let mut fake_servers = language
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue