Fix -
being a word character for selections (#17171)
Co-Authored-By: Mikayla <mikayla@zed.dev> Co-Authored-By: Nate <nate@zed.dev> Closes #15606 Closes #13515 Release Notes: - Fixes `-` being considered a word character for selections in some languages Co-authored-by: Mikayla <mikayla@zed.dev> Co-authored-by: Nate <nate@zed.dev>
This commit is contained in:
parent
c0731bfa28
commit
ee6ec50b15
12 changed files with 239 additions and 143 deletions
|
@ -7,7 +7,7 @@ use editor::{
|
|||
Anchor, Bias, DisplayPoint, Editor, RowExt, ToOffset,
|
||||
};
|
||||
use gpui::{actions, impl_actions, px, ViewContext};
|
||||
use language::{char_kind, CharKind, Point, Selection, SelectionGoal};
|
||||
use language::{CharKind, Point, Selection, SelectionGoal};
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use serde::Deserialize;
|
||||
use std::ops::Range;
|
||||
|
@ -1131,12 +1131,15 @@ pub(crate) fn next_word_start(
|
|||
ignore_punctuation: bool,
|
||||
times: usize,
|
||||
) -> DisplayPoint {
|
||||
let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
|
||||
let classifier = map
|
||||
.buffer_snapshot
|
||||
.char_classifier_at(point.to_point(map))
|
||||
.ignore_punctuation(ignore_punctuation);
|
||||
for _ in 0..times {
|
||||
let mut crossed_newline = false;
|
||||
let new_point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
|
||||
let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
|
||||
let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
|
||||
let left_kind = classifier.kind(left);
|
||||
let right_kind = classifier.kind(right);
|
||||
let at_newline = right == '\n';
|
||||
|
||||
let found = (left_kind != right_kind && right_kind != CharKind::Whitespace)
|
||||
|
@ -1161,7 +1164,10 @@ pub(crate) fn next_word_end(
|
|||
times: usize,
|
||||
allow_cross_newline: bool,
|
||||
) -> DisplayPoint {
|
||||
let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
|
||||
let classifier = map
|
||||
.buffer_snapshot
|
||||
.char_classifier_at(point.to_point(map))
|
||||
.ignore_punctuation(ignore_punctuation);
|
||||
for _ in 0..times {
|
||||
let new_point = next_char(map, point, allow_cross_newline);
|
||||
let mut need_next_char = false;
|
||||
|
@ -1170,8 +1176,8 @@ pub(crate) fn next_word_end(
|
|||
new_point,
|
||||
FindRange::MultiLine,
|
||||
|left, right| {
|
||||
let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
|
||||
let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
|
||||
let left_kind = classifier.kind(left);
|
||||
let right_kind = classifier.kind(right);
|
||||
let at_newline = right == '\n';
|
||||
|
||||
if !allow_cross_newline && at_newline {
|
||||
|
@ -1202,7 +1208,10 @@ fn previous_word_start(
|
|||
ignore_punctuation: bool,
|
||||
times: usize,
|
||||
) -> DisplayPoint {
|
||||
let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
|
||||
let classifier = map
|
||||
.buffer_snapshot
|
||||
.char_classifier_at(point.to_point(map))
|
||||
.ignore_punctuation(ignore_punctuation);
|
||||
for _ in 0..times {
|
||||
// This works even though find_preceding_boundary is called for every character in the line containing
|
||||
// cursor because the newline is checked only once.
|
||||
|
@ -1211,8 +1220,8 @@ fn previous_word_start(
|
|||
point,
|
||||
FindRange::MultiLine,
|
||||
|left, right| {
|
||||
let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
|
||||
let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
|
||||
let left_kind = classifier.kind(left);
|
||||
let right_kind = classifier.kind(right);
|
||||
|
||||
(left_kind != right_kind && !right.is_whitespace()) || left == '\n'
|
||||
},
|
||||
|
@ -1231,7 +1240,10 @@ fn previous_word_end(
|
|||
ignore_punctuation: bool,
|
||||
times: usize,
|
||||
) -> DisplayPoint {
|
||||
let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
|
||||
let classifier = map
|
||||
.buffer_snapshot
|
||||
.char_classifier_at(point.to_point(map))
|
||||
.ignore_punctuation(ignore_punctuation);
|
||||
let mut point = point.to_point(map);
|
||||
|
||||
if point.column < map.buffer_snapshot.line_len(MultiBufferRow(point.row)) {
|
||||
|
@ -1243,8 +1255,8 @@ fn previous_word_end(
|
|||
point,
|
||||
FindRange::MultiLine,
|
||||
|left, right| {
|
||||
let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
|
||||
let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
|
||||
let left_kind = classifier.kind(left);
|
||||
let right_kind = classifier.kind(right);
|
||||
match (left_kind, right_kind) {
|
||||
(CharKind::Punctuation, CharKind::Whitespace)
|
||||
| (CharKind::Punctuation, CharKind::Word)
|
||||
|
@ -1269,12 +1281,15 @@ fn next_subword_start(
|
|||
ignore_punctuation: bool,
|
||||
times: usize,
|
||||
) -> DisplayPoint {
|
||||
let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
|
||||
let classifier = map
|
||||
.buffer_snapshot
|
||||
.char_classifier_at(point.to_point(map))
|
||||
.ignore_punctuation(ignore_punctuation);
|
||||
for _ in 0..times {
|
||||
let mut crossed_newline = false;
|
||||
let new_point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
|
||||
let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
|
||||
let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
|
||||
let left_kind = classifier.kind(left);
|
||||
let right_kind = classifier.kind(right);
|
||||
let at_newline = right == '\n';
|
||||
|
||||
let is_word_start = (left_kind != right_kind) && !left.is_alphanumeric();
|
||||
|
@ -1303,7 +1318,10 @@ pub(crate) fn next_subword_end(
|
|||
times: usize,
|
||||
allow_cross_newline: bool,
|
||||
) -> DisplayPoint {
|
||||
let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
|
||||
let classifier = map
|
||||
.buffer_snapshot
|
||||
.char_classifier_at(point.to_point(map))
|
||||
.ignore_punctuation(ignore_punctuation);
|
||||
for _ in 0..times {
|
||||
let new_point = next_char(map, point, allow_cross_newline);
|
||||
|
||||
|
@ -1311,8 +1329,8 @@ pub(crate) fn next_subword_end(
|
|||
let mut need_backtrack = false;
|
||||
let new_point =
|
||||
movement::find_boundary(map, new_point, FindRange::MultiLine, |left, right| {
|
||||
let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
|
||||
let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
|
||||
let left_kind = classifier.kind(left);
|
||||
let right_kind = classifier.kind(right);
|
||||
let at_newline = right == '\n';
|
||||
|
||||
if !allow_cross_newline && at_newline {
|
||||
|
@ -1350,7 +1368,10 @@ fn previous_subword_start(
|
|||
ignore_punctuation: bool,
|
||||
times: usize,
|
||||
) -> DisplayPoint {
|
||||
let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
|
||||
let classifier = map
|
||||
.buffer_snapshot
|
||||
.char_classifier_at(point.to_point(map))
|
||||
.ignore_punctuation(ignore_punctuation);
|
||||
for _ in 0..times {
|
||||
let mut crossed_newline = false;
|
||||
// This works even though find_preceding_boundary is called for every character in the line containing
|
||||
|
@ -1360,8 +1381,8 @@ fn previous_subword_start(
|
|||
point,
|
||||
FindRange::MultiLine,
|
||||
|left, right| {
|
||||
let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
|
||||
let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
|
||||
let left_kind = classifier.kind(left);
|
||||
let right_kind = classifier.kind(right);
|
||||
let at_newline = right == '\n';
|
||||
|
||||
let is_word_start = (left_kind != right_kind) && !left.is_alphanumeric();
|
||||
|
@ -1391,7 +1412,10 @@ fn previous_subword_end(
|
|||
ignore_punctuation: bool,
|
||||
times: usize,
|
||||
) -> DisplayPoint {
|
||||
let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
|
||||
let classifier = map
|
||||
.buffer_snapshot
|
||||
.char_classifier_at(point.to_point(map))
|
||||
.ignore_punctuation(ignore_punctuation);
|
||||
let mut point = point.to_point(map);
|
||||
|
||||
if point.column < map.buffer_snapshot.line_len(MultiBufferRow(point.row)) {
|
||||
|
@ -1403,8 +1427,8 @@ fn previous_subword_end(
|
|||
point,
|
||||
FindRange::MultiLine,
|
||||
|left, right| {
|
||||
let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
|
||||
let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
|
||||
let left_kind = classifier.kind(left);
|
||||
let right_kind = classifier.kind(right);
|
||||
|
||||
let is_subword_end =
|
||||
left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
|
||||
|
@ -1435,7 +1459,7 @@ pub(crate) fn first_non_whitespace(
|
|||
from: DisplayPoint,
|
||||
) -> DisplayPoint {
|
||||
let mut start_offset = start_of_line(map, display_lines, from).to_offset(map, Bias::Left);
|
||||
let scope = map.buffer_snapshot.language_scope_at(from.to_point(map));
|
||||
let classifier = map.buffer_snapshot.char_classifier_at(from.to_point(map));
|
||||
for (ch, offset) in map.buffer_chars_at(start_offset) {
|
||||
if ch == '\n' {
|
||||
return from;
|
||||
|
@ -1443,7 +1467,7 @@ pub(crate) fn first_non_whitespace(
|
|||
|
||||
start_offset = offset;
|
||||
|
||||
if char_kind(&scope, ch) != CharKind::Whitespace {
|
||||
if classifier.kind(ch) != CharKind::Whitespace {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -1457,11 +1481,11 @@ pub(crate) fn last_non_whitespace(
|
|||
count: usize,
|
||||
) -> DisplayPoint {
|
||||
let mut end_of_line = end_of_line(map, false, from, count).to_offset(map, Bias::Left);
|
||||
let scope = map.buffer_snapshot.language_scope_at(from.to_point(map));
|
||||
let classifier = map.buffer_snapshot.char_classifier_at(from.to_point(map));
|
||||
|
||||
// NOTE: depending on clip_at_line_end we may already be one char back from the end.
|
||||
if let Some((ch, _)) = map.buffer_chars_at(end_of_line).next() {
|
||||
if char_kind(&scope, ch) != CharKind::Whitespace {
|
||||
if classifier.kind(ch) != CharKind::Whitespace {
|
||||
return end_of_line.to_display_point(map);
|
||||
}
|
||||
}
|
||||
|
@ -1471,7 +1495,7 @@ pub(crate) fn last_non_whitespace(
|
|||
break;
|
||||
}
|
||||
end_of_line = offset;
|
||||
if char_kind(&scope, ch) != CharKind::Whitespace || ch == '\n' {
|
||||
if classifier.kind(ch) != CharKind::Whitespace || ch == '\n' {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -1787,14 +1811,6 @@ fn window_bottom(
|
|||
}
|
||||
}
|
||||
|
||||
pub fn coerce_punctuation(kind: CharKind, treat_punctuation_as_word: bool) -> CharKind {
|
||||
if treat_punctuation_as_word && kind == CharKind::Punctuation {
|
||||
CharKind::Word
|
||||
} else {
|
||||
kind
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ use editor::{
|
|||
scroll::Autoscroll,
|
||||
Bias, DisplayPoint,
|
||||
};
|
||||
use language::{char_kind, CharKind, Selection};
|
||||
use language::Selection;
|
||||
use ui::ViewContext;
|
||||
|
||||
impl Vim {
|
||||
|
@ -59,13 +59,11 @@ impl Vim {
|
|||
if let Motion::CurrentLine = motion {
|
||||
let mut start_offset =
|
||||
selection.start.to_offset(map, Bias::Left);
|
||||
let scope = map
|
||||
let classifier = map
|
||||
.buffer_snapshot
|
||||
.language_scope_at(selection.start.to_point(&map));
|
||||
.char_classifier_at(selection.start.to_point(&map));
|
||||
for (ch, offset) in map.buffer_chars_at(start_offset) {
|
||||
if ch == '\n'
|
||||
|| char_kind(&scope, ch) != CharKind::Whitespace
|
||||
{
|
||||
if ch == '\n' || !classifier.is_whitespace(ch) {
|
||||
break;
|
||||
}
|
||||
start_offset = offset + ch.len_utf8();
|
||||
|
@ -130,13 +128,13 @@ fn expand_changed_word_selection(
|
|||
use_subword: bool,
|
||||
) -> bool {
|
||||
let is_in_word = || {
|
||||
let scope = map
|
||||
let classifier = map
|
||||
.buffer_snapshot
|
||||
.language_scope_at(selection.start.to_point(map));
|
||||
.char_classifier_at(selection.start.to_point(map));
|
||||
let in_word = map
|
||||
.buffer_chars_at(selection.head().to_offset(map, Bias::Left))
|
||||
.next()
|
||||
.map(|(c, _)| char_kind(&scope, c) != CharKind::Whitespace)
|
||||
.map(|(c, _)| !classifier.is_whitespace(c))
|
||||
.unwrap_or_default();
|
||||
return in_word;
|
||||
};
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
use std::ops::Range;
|
||||
|
||||
use crate::{
|
||||
motion::{coerce_punctuation, right},
|
||||
state::Mode,
|
||||
Vim,
|
||||
};
|
||||
use crate::{motion::right, state::Mode, Vim};
|
||||
use editor::{
|
||||
display_map::{DisplaySnapshot, ToDisplayPoint},
|
||||
movement::{self, FindRange},
|
||||
|
@ -14,7 +10,7 @@ use editor::{
|
|||
use itertools::Itertools;
|
||||
|
||||
use gpui::{actions, impl_actions, ViewContext};
|
||||
use language::{char_kind, BufferSnapshot, CharKind, Point, Selection};
|
||||
use language::{BufferSnapshot, CharKind, Point, Selection};
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use serde::Deserialize;
|
||||
|
||||
|
@ -248,22 +244,19 @@ fn in_word(
|
|||
ignore_punctuation: bool,
|
||||
) -> Option<Range<DisplayPoint>> {
|
||||
// Use motion::right so that we consider the character under the cursor when looking for the start
|
||||
let scope = map
|
||||
let classifier = map
|
||||
.buffer_snapshot
|
||||
.language_scope_at(relative_to.to_point(map));
|
||||
.char_classifier_at(relative_to.to_point(map))
|
||||
.ignore_punctuation(ignore_punctuation);
|
||||
let start = movement::find_preceding_boundary_display_point(
|
||||
map,
|
||||
right(map, relative_to, 1),
|
||||
movement::FindRange::SingleLine,
|
||||
|left, right| {
|
||||
coerce_punctuation(char_kind(&scope, left), ignore_punctuation)
|
||||
!= coerce_punctuation(char_kind(&scope, right), ignore_punctuation)
|
||||
},
|
||||
|left, right| classifier.kind(left) != classifier.kind(right),
|
||||
);
|
||||
|
||||
let end = movement::find_boundary(map, relative_to, FindRange::SingleLine, |left, right| {
|
||||
coerce_punctuation(char_kind(&scope, left), ignore_punctuation)
|
||||
!= coerce_punctuation(char_kind(&scope, right), ignore_punctuation)
|
||||
classifier.kind(left) != classifier.kind(right)
|
||||
});
|
||||
|
||||
Some(start..end)
|
||||
|
@ -362,11 +355,14 @@ fn around_word(
|
|||
ignore_punctuation: bool,
|
||||
) -> Option<Range<DisplayPoint>> {
|
||||
let offset = relative_to.to_offset(map, Bias::Left);
|
||||
let scope = map.buffer_snapshot.language_scope_at(offset);
|
||||
let classifier = map
|
||||
.buffer_snapshot
|
||||
.char_classifier_at(offset)
|
||||
.ignore_punctuation(ignore_punctuation);
|
||||
let in_word = map
|
||||
.buffer_chars_at(offset)
|
||||
.next()
|
||||
.map(|(c, _)| char_kind(&scope, c) != CharKind::Whitespace)
|
||||
.map(|(c, _)| !classifier.is_whitespace(c))
|
||||
.unwrap_or(false);
|
||||
|
||||
if in_word {
|
||||
|
@ -390,24 +386,22 @@ fn around_next_word(
|
|||
relative_to: DisplayPoint,
|
||||
ignore_punctuation: bool,
|
||||
) -> Option<Range<DisplayPoint>> {
|
||||
let scope = map
|
||||
let classifier = map
|
||||
.buffer_snapshot
|
||||
.language_scope_at(relative_to.to_point(map));
|
||||
.char_classifier_at(relative_to.to_point(map))
|
||||
.ignore_punctuation(ignore_punctuation);
|
||||
// Get the start of the word
|
||||
let start = movement::find_preceding_boundary_display_point(
|
||||
map,
|
||||
right(map, relative_to, 1),
|
||||
FindRange::SingleLine,
|
||||
|left, right| {
|
||||
coerce_punctuation(char_kind(&scope, left), ignore_punctuation)
|
||||
!= coerce_punctuation(char_kind(&scope, right), ignore_punctuation)
|
||||
},
|
||||
|left, right| classifier.kind(left) != classifier.kind(right),
|
||||
);
|
||||
|
||||
let mut word_found = false;
|
||||
let end = movement::find_boundary(map, relative_to, FindRange::MultiLine, |left, right| {
|
||||
let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
|
||||
let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
|
||||
let left_kind = classifier.kind(left);
|
||||
let right_kind = classifier.kind(right);
|
||||
|
||||
let found = (word_found && left_kind != right_kind) || right == '\n' && left == '\n';
|
||||
|
||||
|
|
|
@ -305,6 +305,25 @@ async fn test_word_characters(cx: &mut gpui::TestAppContext) {
|
|||
)
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_kebab_case(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new_html(cx).await;
|
||||
cx.set_state(
|
||||
indoc! { r#"
|
||||
<div><a class="bg-rˇed"></a></div>
|
||||
"#},
|
||||
Mode::Normal,
|
||||
);
|
||||
cx.simulate_keystrokes("v i w");
|
||||
cx.assert_state(
|
||||
indoc! { r#"
|
||||
<div><a class="bg-«redˇ»"></a></div>
|
||||
"#
|
||||
},
|
||||
Mode::Visual,
|
||||
)
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_join_lines(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue