Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

helix-core/surround: Syntax aware #6893

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 32 additions & 17 deletions helix-core/src/surround.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use std::fmt::Display;

use crate::{movement::Direction, search, Range, Selection};
use crate::{
match_brackets::find_matching_bracket, movement::Direction, search, Range, Selection, Syntax,
};
use ropey::RopeSlice;

pub const PAIRS: &[(char, char)] = &[
Expand Down Expand Up @@ -124,6 +126,7 @@ pub fn find_nth_closest_pairs_pos(
/// or opening pair. `n` will skip n - 1 pairs (eg. n=2 will discard (only)
/// the first pair found and keep looking)
pub fn find_nth_pairs_pos(
syntax: Option<&Syntax>,
text: RopeSlice,
ch: char,
range: Range,
Expand All @@ -141,15 +144,26 @@ pub fn find_nth_pairs_pos(

let (open, close) = if open == close {
if Some(open) == text.get_char(pos) {
// Cursor is directly on match char. We return no match
// because there's no way to know which side of the char
// we should be searching on.
return Err(Error::CursorOnAmbiguousPair);
// Cursor is directly on match char and we don't have any tree-sitter syntax available
// to help identify the matching pair. We return no match because there's no way to know
// which side of the char we should be searching on.
let syntax = syntax.ok_or(Error::CursorOnAmbiguousPair)?;

let match_pos = find_matching_bracket(syntax, &text.into(), pos)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line is the source of the current merge conflict. The RopeSlice can be passed on through now.

Suggested change
let match_pos = find_matching_bracket(syntax, &text.into(), pos)
let match_pos = find_matching_bracket(syntax, text, pos)

.ok_or(Error::CursorOnAmbiguousPair)?;
let close_char = text.char(match_pos);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should be reading the close_char again here. If we run into some edgecase where find_matching_bracket does not return the same close char we should just return an error. Everything else would be unintuitive.

let search_pos = pos.max(match_pos);

(
search::find_nth_prev(text, open, search_pos, n),
search::find_nth_next(text, close_char, search_pos, n),
)
} else {
(
search::find_nth_prev(text, open, pos, n),
search::find_nth_next(text, close, pos, n),
)
}
(
search::find_nth_prev(text, open, pos, n),
search::find_nth_next(text, close, pos, n),
)
} else {
(
find_nth_open_pair(text, open, close, pos, n),
Expand Down Expand Up @@ -245,6 +259,7 @@ fn find_nth_close_pair(
/// are automatically detected around each cursor (note that this may result
/// in them selecting different surround characters for each selection).
pub fn get_surround_pos(
syntax: Option<&Syntax>,
text: RopeSlice,
selection: &Selection,
ch: Option<char>,
Expand All @@ -254,7 +269,7 @@ pub fn get_surround_pos(

for &range in selection {
let (open_pos, close_pos) = match ch {
Some(ch) => find_nth_pairs_pos(text, ch, range, skip)?,
Some(ch) => find_nth_pairs_pos(syntax, text, ch, range, skip)?,
None => find_nth_closest_pairs_pos(text, range, skip)?,
};
if change_pos.contains(&open_pos) || change_pos.contains(&close_pos) {
Expand Down Expand Up @@ -283,7 +298,7 @@ mod test {
);

assert_eq!(
get_surround_pos(doc.slice(..), &selection, Some('('), 1).unwrap(),
get_surround_pos(None, doc.slice(..), &selection, Some('('), 1).unwrap(),
expectations
);
}
Expand All @@ -298,7 +313,7 @@ mod test {
);

assert_eq!(
get_surround_pos(doc.slice(..), &selection, Some('('), 1),
get_surround_pos(None, doc.slice(..), &selection, Some('('), 1),
Err(Error::PairNotFound)
);
}
Expand All @@ -313,7 +328,7 @@ mod test {
);

assert_eq!(
get_surround_pos(doc.slice(..), &selection, Some('('), 1),
get_surround_pos(None, doc.slice(..), &selection, Some('('), 1),
Err(Error::PairNotFound) // overlapping surround chars
);
}
Expand All @@ -328,7 +343,7 @@ mod test {
);

assert_eq!(
get_surround_pos(doc.slice(..), &selection, Some('['), 1),
get_surround_pos(None, doc.slice(..), &selection, Some('['), 1),
Err(Error::CursorOverlap)
);
}
Expand All @@ -344,7 +359,7 @@ mod test {

assert_eq!(2, expectations.len());
assert_eq!(
find_nth_pairs_pos(doc.slice(..), '\'', selection.primary(), 1)
find_nth_pairs_pos(None, doc.slice(..), '\'', selection.primary(), 1)
.expect("find should succeed"),
(expectations[0], expectations[1])
)
Expand All @@ -361,7 +376,7 @@ mod test {

assert_eq!(2, expectations.len());
assert_eq!(
find_nth_pairs_pos(doc.slice(..), '\'', selection.primary(), 2)
find_nth_pairs_pos(None, doc.slice(..), '\'', selection.primary(), 2)
.expect("find should succeed"),
(expectations[0], expectations[1])
)
Expand All @@ -377,7 +392,7 @@ mod test {
);

assert_eq!(
find_nth_pairs_pos(doc.slice(..), '\'', selection.primary(), 1),
find_nth_pairs_pos(None, doc.slice(..), '\'', selection.primary(), 1),
Err(Error::CursorOnAmbiguousPair)
)
}
Expand Down
14 changes: 9 additions & 5 deletions helix-core/src/textobject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ use crate::chars::{categorize_char, char_is_whitespace, CharCategory};
use crate::graphemes::{next_grapheme_boundary, prev_grapheme_boundary};
use crate::line_ending::rope_is_line_ending;
use crate::movement::Direction;
use crate::surround;
use crate::syntax::LanguageConfiguration;
use crate::Range;
use crate::{surround, Syntax};

fn find_word_boundary(slice: RopeSlice, mut pos: usize, direction: Direction, long: bool) -> usize {
use CharCategory::{Eol, Whitespace};
Expand Down Expand Up @@ -199,33 +199,36 @@ pub fn textobject_paragraph(
}

pub fn textobject_pair_surround(
syntax: Option<&Syntax>,
slice: RopeSlice,
range: Range,
textobject: TextObject,
ch: char,
count: usize,
) -> Range {
textobject_pair_surround_impl(slice, range, textobject, Some(ch), count)
textobject_pair_surround_impl(syntax, slice, range, textobject, Some(ch), count)
}

pub fn textobject_pair_surround_closest(
syntax: Option<&Syntax>,
slice: RopeSlice,
range: Range,
textobject: TextObject,
count: usize,
) -> Range {
textobject_pair_surround_impl(slice, range, textobject, None, count)
textobject_pair_surround_impl(syntax, slice, range, textobject, None, count)
}

fn textobject_pair_surround_impl(
syntax: Option<&Syntax>,
slice: RopeSlice,
range: Range,
textobject: TextObject,
ch: Option<char>,
count: usize,
) -> Range {
let pair_pos = match ch {
Some(ch) => surround::find_nth_pairs_pos(slice, ch, range, count),
Some(ch) => surround::find_nth_pairs_pos(syntax, slice, ch, range, count),
// Automatically find the closest surround pairs
None => surround::find_nth_closest_pairs_pos(slice, range, count),
};
Expand Down Expand Up @@ -574,7 +577,8 @@ mod test {
let slice = doc.slice(..);
for &case in scenario {
let (pos, objtype, expected_range, ch, count) = case;
let result = textobject_pair_surround(slice, Range::point(pos), objtype, ch, count);
let result =
textobject_pair_surround(None, slice, Range::point(pos), objtype, ch, count);
assert_eq!(
result,
expected_range.into(),
Expand Down
47 changes: 29 additions & 18 deletions helix-term/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4840,13 +4840,22 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) {
'T' => textobject_treesitter("test", range),
'p' => textobject::textobject_paragraph(text, range, objtype, count),
'm' => textobject::textobject_pair_surround_closest(
text, range, objtype, count,
doc.syntax(),
text,
range,
objtype,
count,
),
'g' => textobject_change(range),
// TODO: cancel new ranges if inconsistent surround matches across lines
ch if !ch.is_ascii_alphanumeric() => {
textobject::textobject_pair_surround(text, range, objtype, ch, count)
}
ch if !ch.is_ascii_alphanumeric() => textobject::textobject_pair_surround(
doc.syntax(),
text,
range,
objtype,
ch,
count,
),
_ => range,
}
});
Expand Down Expand Up @@ -4935,13 +4944,14 @@ fn surround_replace(cx: &mut Context) {
let text = doc.text().slice(..);
let selection = doc.selection(view.id);

let change_pos = match surround::get_surround_pos(text, selection, surround_ch, count) {
Ok(c) => c,
Err(err) => {
cx.editor.set_error(err.to_string());
return;
}
};
let change_pos =
match surround::get_surround_pos(doc.syntax(), text, selection, surround_ch, count) {
Ok(c) => c,
Err(err) => {
cx.editor.set_error(err.to_string());
return;
}
};

cx.on_next_key(move |cx, event| {
let (view, doc) = current!(cx.editor);
Expand Down Expand Up @@ -4976,13 +4986,14 @@ fn surround_delete(cx: &mut Context) {
let text = doc.text().slice(..);
let selection = doc.selection(view.id);

let change_pos = match surround::get_surround_pos(text, selection, surround_ch, count) {
Ok(c) => c,
Err(err) => {
cx.editor.set_error(err.to_string());
return;
}
};
let change_pos =
match surround::get_surround_pos(doc.syntax(), text, selection, surround_ch, count) {
Ok(c) => c,
Err(err) => {
cx.editor.set_error(err.to_string());
return;
}
};

let transaction =
Transaction::change(doc.text(), change_pos.into_iter().map(|p| (p, p + 1, None)));
Expand Down
1 change: 1 addition & 0 deletions helix-term/tests/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,5 @@ mod test {
mod movement;
mod prompt;
mod splits;
mod surround;
}
46 changes: 46 additions & 0 deletions helix-term/tests/test/surround.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
use super::*;

#[tokio::test(flavor = "multi_thread")]
async fn replace_matching_double_quotes_with_treesitter() -> anyhow::Result<()> {
test_with_config(
AppBuilder::new().with_file("foo.rs", None),
(
helpers::platform_line(r#"#["|]#hello world""#),
r#"mr"{"#,
helpers::platform_line(r#"#[{|]#hello world}"#),
),
)
.await?;

Ok(())
}

#[tokio::test(flavor = "multi_thread")]
async fn delete_matching_double_quotes_with_treesitter() -> anyhow::Result<()> {
test_with_config(
AppBuilder::new().with_file("foo.rs", None),
(
helpers::platform_line(r#"#["|]#hello world""#),
r#"md""#,
helpers::platform_line(r#"#[h|]#ello world"#),
),
)
.await?;

Ok(())
}

#[tokio::test(flavor = "multi_thread")]
async fn replace_matching_single_quotes_with_treesitter() -> anyhow::Result<()> {
test_with_config(
AppBuilder::new().with_file("foo.rs", None),
(
helpers::platform_line(r#"#['|]#h'"#),
r#"mr'""#,
helpers::platform_line(r#"#["|]#h""#),
),
)
.await?;

Ok(())
}