mirror of https://github.com/helix-editor/helix
parent
e4561d1dde
commit
8b02bf2ea8
|
@ -24,6 +24,7 @@ pub mod shellwords;
|
||||||
mod state;
|
mod state;
|
||||||
pub mod surround;
|
pub mod surround;
|
||||||
pub mod syntax;
|
pub mod syntax;
|
||||||
|
pub mod test;
|
||||||
pub mod textobject;
|
pub mod textobject;
|
||||||
mod transaction;
|
mod transaction;
|
||||||
|
|
||||||
|
|
|
@ -119,6 +119,11 @@ pub fn str_is_line_ending(s: &str) -> bool {
|
||||||
LineEnding::from_str(s).is_some()
|
LineEnding::from_str(s).is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn rope_is_line_ending(r: RopeSlice) -> bool {
|
||||||
|
r.chunks().all(str_is_line_ending)
|
||||||
|
}
|
||||||
|
|
||||||
/// Attempts to detect what line ending the passed document uses.
|
/// Attempts to detect what line ending the passed document uses.
|
||||||
pub fn auto_detect_line_ending(doc: &Rope) -> Option<LineEnding> {
|
pub fn auto_detect_line_ending(doc: &Rope) -> Option<LineEnding> {
|
||||||
// Return first matched line ending. Not all possible line endings
|
// Return first matched line ending. Not all possible line endings
|
||||||
|
|
|
@ -10,6 +10,7 @@ use crate::{
|
||||||
next_grapheme_boundary, nth_next_grapheme_boundary, nth_prev_grapheme_boundary,
|
next_grapheme_boundary, nth_next_grapheme_boundary, nth_prev_grapheme_boundary,
|
||||||
prev_grapheme_boundary,
|
prev_grapheme_boundary,
|
||||||
},
|
},
|
||||||
|
line_ending::{rope_is_line_ending, str_is_line_ending},
|
||||||
pos_at_coords,
|
pos_at_coords,
|
||||||
syntax::LanguageConfiguration,
|
syntax::LanguageConfiguration,
|
||||||
textobject::TextObject,
|
textobject::TextObject,
|
||||||
|
@ -149,6 +150,63 @@ fn word_move(slice: RopeSlice, range: Range, count: usize, target: WordMotionTar
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn move_prev_para(slice: RopeSlice, range: Range, count: usize, behavior: Movement) -> Range {
|
||||||
|
let mut line = range.cursor_line(slice);
|
||||||
|
let first_char = slice.line_to_char(line) == range.cursor(slice);
|
||||||
|
let curr_line_empty = rope_is_line_ending(slice.line(line));
|
||||||
|
let last_line_empty = rope_is_line_ending(slice.line(line.saturating_sub(1)));
|
||||||
|
let line_to_empty = last_line_empty && !curr_line_empty;
|
||||||
|
|
||||||
|
// iterate current line if first character after paragraph boundary
|
||||||
|
if line_to_empty && !first_char {
|
||||||
|
line += 1;
|
||||||
|
}
|
||||||
|
let mut lines = slice.lines_at(line);
|
||||||
|
lines.reverse();
|
||||||
|
let mut lines = lines.map(rope_is_line_ending).peekable();
|
||||||
|
for _ in 0..count {
|
||||||
|
while lines.next_if(|&e| e).is_some() {
|
||||||
|
line -= 1;
|
||||||
|
}
|
||||||
|
while lines.next_if(|&e| !e).is_some() {
|
||||||
|
line -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let head = slice.line_to_char(line);
|
||||||
|
let anchor = if behavior == Movement::Move {
|
||||||
|
// exclude first character after paragraph boundary
|
||||||
|
if line_to_empty && first_char {
|
||||||
|
range.cursor(slice)
|
||||||
|
} else {
|
||||||
|
range.head
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
range.put_cursor(slice, head, true).anchor
|
||||||
|
};
|
||||||
|
Range::new(anchor, head)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn move_next_para(slice: RopeSlice, range: Range, count: usize, behavior: Movement) -> Range {
|
||||||
|
let mut line = slice.char_to_line(range.head);
|
||||||
|
let lines = slice.lines_at(line);
|
||||||
|
let mut lines = lines.map(|l| l.chunks().all(str_is_line_ending)).peekable();
|
||||||
|
for _ in 0..count {
|
||||||
|
while lines.next_if(|&e| !e).is_some() {
|
||||||
|
line += 1;
|
||||||
|
}
|
||||||
|
while lines.next_if(|&e| e).is_some() {
|
||||||
|
line += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let anchor = if behavior == Movement::Move {
|
||||||
|
range.cursor(slice)
|
||||||
|
} else {
|
||||||
|
range.anchor
|
||||||
|
};
|
||||||
|
Range::new(anchor, slice.line_to_char(line))
|
||||||
|
}
|
||||||
|
|
||||||
// ---- util ------------
|
// ---- util ------------
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
|
@ -1179,4 +1237,67 @@ mod test {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_behaviour_when_moving_to_prev_paragraph_single() {
|
||||||
|
let tests = [
|
||||||
|
("^@", "@^"),
|
||||||
|
("^s@tart at\nfirst char\n", "@s^tart at\nfirst char\n"),
|
||||||
|
("start at\nlast char^\n@", "@start at\nlast char\n^"),
|
||||||
|
("goto\nfirst\n\n^p@aragraph", "@goto\nfirst\n\n^paragraph"),
|
||||||
|
("goto\nfirst\n^\n@paragraph", "@goto\nfirst\n\n^paragraph"),
|
||||||
|
("goto\nsecond\n\np^a@ragraph", "goto\nsecond\n\n@pa^ragraph"),
|
||||||
|
(
|
||||||
|
"here\n\nhave\nmultiple\nparagraph\n\n\n\n\n^@",
|
||||||
|
"here\n\n@have\nmultiple\nparagraph\n\n\n\n\n^",
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (actual, expected) in tests {
|
||||||
|
let (s, selection) = crate::test::print(actual);
|
||||||
|
let text = Rope::from(s.as_str());
|
||||||
|
let selection =
|
||||||
|
selection.transform(|r| move_prev_para(text.slice(..), r, 1, Movement::Move));
|
||||||
|
let actual = crate::test::plain(&s, selection);
|
||||||
|
assert_eq!(actual, expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[ignore]
|
||||||
|
#[test]
|
||||||
|
fn test_behaviour_when_moving_to_prev_paragraph_double() {}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_behaviour_when_moving_to_next_paragraph_single() {
|
||||||
|
let tests = [
|
||||||
|
("^@", "@^"),
|
||||||
|
("^s@tart at\nfirst char\n", "^start at\nfirst char\n@"),
|
||||||
|
("start at\nlast char^\n@", "start at\nlast char^\n@"),
|
||||||
|
(
|
||||||
|
"a\nb\n\n^g@oto\nthird\n\nparagraph",
|
||||||
|
"a\nb\n\n^goto\nthird\n\n@paragraph",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"a\nb\n^\n@goto\nthird\n\nparagraph",
|
||||||
|
"a\nb\n\n^goto\nthird\n\n@paragraph",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"a\nb^\n@\ngoto\nsecond\n\nparagraph",
|
||||||
|
"a\nb^\n\n@goto\nsecond\n\nparagraph",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"here\n\nhave\n^m@ultiple\nparagraph\n\n\n\n\n",
|
||||||
|
"here\n\nhave\n^multiple\nparagraph\n\n\n\n\n@",
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (actual, expected) in tests {
|
||||||
|
let (s, selection) = crate::test::print(actual);
|
||||||
|
let text = Rope::from(s.as_str());
|
||||||
|
let selection =
|
||||||
|
selection.transform(|r| move_next_para(text.slice(..), r, 1, Movement::Move));
|
||||||
|
let actual = crate::test::plain(&s, selection);
|
||||||
|
assert_eq!(actual, expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,111 @@
|
||||||
|
//! Test helpers.
|
||||||
|
use crate::{Range, Selection};
|
||||||
|
use smallvec::SmallVec;
|
||||||
|
use std::cmp::Reverse;
|
||||||
|
|
||||||
|
/// Convert annotated test string to test string and selection.
|
||||||
|
///
|
||||||
|
/// `^` for `anchor` and `|` for head (`@` for primary), both must appear
|
||||||
|
/// or otherwise it will panic.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use helix_core::{Range, Selection, test::print};
|
||||||
|
/// use smallvec::smallvec;
|
||||||
|
///
|
||||||
|
/// assert_eq!(
|
||||||
|
/// print("^a@b|c^"),
|
||||||
|
/// ("abc".to_owned(), Selection::new(smallvec![Range::new(0, 1), Range::new(3, 2)], 0))
|
||||||
|
/// );
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// Panics when missing primary or appeared more than once.
|
||||||
|
/// Panics when missing head or anchor.
|
||||||
|
/// Panics when head come after head or anchor come after anchor.
|
||||||
|
pub fn print(s: &str) -> (String, Selection) {
|
||||||
|
let mut anchor = None;
|
||||||
|
let mut head = None;
|
||||||
|
let mut primary = None;
|
||||||
|
let mut ranges = SmallVec::new();
|
||||||
|
let mut i = 0;
|
||||||
|
let s = s
|
||||||
|
.chars()
|
||||||
|
.filter(|c| {
|
||||||
|
match c {
|
||||||
|
'^' if anchor != None => panic!("anchor without head {s:?}"),
|
||||||
|
'^' if head == None => anchor = Some(i),
|
||||||
|
'^' => ranges.push(Range::new(i, head.take().unwrap())),
|
||||||
|
'|' if head != None => panic!("head without anchor {s:?}"),
|
||||||
|
'|' if anchor == None => head = Some(i),
|
||||||
|
'|' => ranges.push(Range::new(anchor.take().unwrap(), i)),
|
||||||
|
'@' if primary != None => panic!("head (primary) already appeared {s:?}"),
|
||||||
|
'@' if head != None => panic!("head (primary) without anchor {s:?}"),
|
||||||
|
'@' if anchor == None => {
|
||||||
|
primary = Some(ranges.len());
|
||||||
|
head = Some(i);
|
||||||
|
}
|
||||||
|
'@' => {
|
||||||
|
primary = Some(ranges.len());
|
||||||
|
ranges.push(Range::new(anchor.take().unwrap(), i));
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
i += 1;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
false
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
if head.is_some() {
|
||||||
|
panic!("missing anchor (|) {s:?}");
|
||||||
|
}
|
||||||
|
if anchor.is_some() {
|
||||||
|
panic!("missing head (^) {s:?}");
|
||||||
|
}
|
||||||
|
let primary = match primary {
|
||||||
|
Some(i) => i,
|
||||||
|
None => panic!("missing primary (@) {s:?}"),
|
||||||
|
};
|
||||||
|
let selection = Selection::new(ranges, primary);
|
||||||
|
(s, selection)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert test string and selection to annotated test string.
|
||||||
|
///
|
||||||
|
/// `^` for `anchor` and `|` for head (`@` for primary).
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use helix_core::{Range, Selection, test::plain};
|
||||||
|
/// use smallvec::smallvec;
|
||||||
|
///
|
||||||
|
/// assert_eq!(
|
||||||
|
/// plain("abc", Selection::new(smallvec![Range::new(0, 1), Range::new(3, 2)], 0)),
|
||||||
|
/// "^a@b|c^".to_owned()
|
||||||
|
/// );
|
||||||
|
/// ```
|
||||||
|
pub fn plain(s: &str, selection: Selection) -> String {
|
||||||
|
let primary = selection.primary_index();
|
||||||
|
let mut out = String::with_capacity(s.len() + 2 * selection.len());
|
||||||
|
out.push_str(s);
|
||||||
|
let mut insertion: Vec<_> = selection
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.flat_map(|(i, range)| {
|
||||||
|
[
|
||||||
|
(range.anchor, '^'),
|
||||||
|
(range.head, if i == primary { '@' } else { '|' }),
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
// insert in reverse order
|
||||||
|
insertion.sort_unstable_by_key(|k| Reverse(k.0));
|
||||||
|
for (i, c) in insertion {
|
||||||
|
out.insert(i, c);
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
|
@ -209,6 +209,8 @@ impl MappableCommand {
|
||||||
move_next_long_word_start, "Move to beginning of next long word",
|
move_next_long_word_start, "Move to beginning of next long word",
|
||||||
move_prev_long_word_start, "Move to beginning of previous long word",
|
move_prev_long_word_start, "Move to beginning of previous long word",
|
||||||
move_next_long_word_end, "Move to end of next long word",
|
move_next_long_word_end, "Move to end of next long word",
|
||||||
|
move_prev_para, "Move to previous paragraph",
|
||||||
|
move_next_para, "Move to next paragraph",
|
||||||
extend_next_word_start, "Extend to beginning of next word",
|
extend_next_word_start, "Extend to beginning of next word",
|
||||||
extend_prev_word_start, "Extend to beginning of previous word",
|
extend_prev_word_start, "Extend to beginning of previous word",
|
||||||
extend_next_long_word_start, "Extend to beginning of next long word",
|
extend_next_long_word_start, "Extend to beginning of next long word",
|
||||||
|
@ -902,6 +904,34 @@ fn move_next_long_word_end(cx: &mut Context) {
|
||||||
move_word_impl(cx, movement::move_next_long_word_end)
|
move_word_impl(cx, movement::move_next_long_word_end)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn move_para_impl<F>(cx: &mut Context, move_fn: F)
|
||||||
|
where
|
||||||
|
F: Fn(RopeSlice, Range, usize, Movement) -> Range,
|
||||||
|
{
|
||||||
|
let count = cx.count();
|
||||||
|
let (view, doc) = current!(cx.editor);
|
||||||
|
let text = doc.text().slice(..);
|
||||||
|
let behavior = if doc.mode == Mode::Select {
|
||||||
|
Movement::Extend
|
||||||
|
} else {
|
||||||
|
Movement::Move
|
||||||
|
};
|
||||||
|
|
||||||
|
let selection = doc
|
||||||
|
.selection(view.id)
|
||||||
|
.clone()
|
||||||
|
.transform(|range| move_fn(text, range, count, behavior));
|
||||||
|
doc.set_selection(view.id, selection);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn move_prev_para(cx: &mut Context) {
|
||||||
|
move_para_impl(cx, movement::move_prev_para)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn move_next_para(cx: &mut Context) {
|
||||||
|
move_para_impl(cx, movement::move_next_para)
|
||||||
|
}
|
||||||
|
|
||||||
fn goto_file_start(cx: &mut Context) {
|
fn goto_file_start(cx: &mut Context) {
|
||||||
if cx.count.is_some() {
|
if cx.count.is_some() {
|
||||||
goto_line(cx);
|
goto_line(cx);
|
||||||
|
|
|
@ -104,6 +104,7 @@ pub fn default() -> HashMap<Mode, Keymap> {
|
||||||
"c" => goto_prev_class,
|
"c" => goto_prev_class,
|
||||||
"a" => goto_prev_parameter,
|
"a" => goto_prev_parameter,
|
||||||
"o" => goto_prev_comment,
|
"o" => goto_prev_comment,
|
||||||
|
"p" => move_prev_para,
|
||||||
"space" => add_newline_above,
|
"space" => add_newline_above,
|
||||||
},
|
},
|
||||||
"]" => { "Right bracket"
|
"]" => { "Right bracket"
|
||||||
|
@ -113,6 +114,7 @@ pub fn default() -> HashMap<Mode, Keymap> {
|
||||||
"c" => goto_next_class,
|
"c" => goto_next_class,
|
||||||
"a" => goto_next_parameter,
|
"a" => goto_next_parameter,
|
||||||
"o" => goto_next_comment,
|
"o" => goto_next_comment,
|
||||||
|
"p" => move_next_para,
|
||||||
"space" => add_newline_below,
|
"space" => add_newline_below,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue