Finally: Retain horizontal position when moving vertically.

This commit is contained in:
Blaž Hrastnik 2021-02-12 16:49:24 +09:00
parent de5170dcda
commit 239db79834
5 changed files with 94 additions and 60 deletions

10
TODO.md
View file

@ -12,8 +12,11 @@
- [x] % for whole doc selection
- [x] vertical splits
- [x] input counts (30j)
- [ ] input counts for b, w, e
- [ ] respect view fullscreen flag
- [ ] retain horiz when moving vertically
- [x] retain horiz when moving vertically
- [x] deindent
- [ ] ensure_cursor_in_view always before rendering? or always in app after event process?
- [ ] update lsp on redo/undo
- [ ] Implement marks (superset of Selection/Range)
- [ ] ctrl-v/ctrl-x on file picker
@ -22,12 +25,17 @@
- [ ] nixos packaging
- [ ] CI binary builds
- [ ] regex search / select next
- [ ] f / t mappings
2
- extend selection (treesitter select parent node) (replaces viw, vi(, va( etc )
- bracket pairs
- comment block (gcc)
- completion signature popups/docs
- multiple views into the same file
- selection align
3
- diagnostics popups

View file

@ -23,11 +23,16 @@ pub struct Range {
pub anchor: usize,
/// The head of the range, moved when extending.
pub head: usize,
pub horiz: Option<u32>,
} // TODO: might be cheaper to store normalized as from/to and an inverted flag
impl Range {
pub fn new(anchor: usize, head: usize) -> Self {
Self { anchor, head }
Self {
anchor,
head,
horiz: None,
}
}
/// Start of the range.
@ -83,7 +88,11 @@ impl Range {
if self.anchor == anchor && self.head == head {
return self;
}
Self { anchor, head }
Self {
anchor,
head,
horiz: None,
}
}
/// Extend the range to cover at least `from` `to`.
@ -93,6 +102,7 @@ impl Range {
return Range {
anchor: from,
head: to,
horiz: None,
};
}
@ -103,6 +113,7 @@ impl Range {
} else {
to
},
horiz: None,
}
}
@ -174,7 +185,11 @@ impl Selection {
/// Constructs a selection holding a single range.
pub fn single(anchor: usize, head: usize) -> Self {
Self {
ranges: smallvec![Range { anchor, head }],
ranges: smallvec![Range {
anchor,
head,
horiz: None
}],
primary_index: 0,
}
}

View file

@ -19,9 +19,7 @@ pub enum Direction {
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum Granularity {
Character,
Word,
Line,
// LineBoundary
}
impl State {
@ -87,23 +85,26 @@ impl State {
// 2. compose onto a ongoing transaction
// 3. on insert mode leave, that transaction gets stored into undo history
pub fn move_pos(
pub fn move_range(
&self,
pos: usize,
range: Range,
dir: Direction,
granularity: Granularity,
count: usize,
) -> usize {
extend: bool,
) -> Range {
let text = &self.doc;
let pos = range.head;
match (dir, granularity) {
(Direction::Backward, Granularity::Character) => {
// Clamp to line
let line = text.char_to_line(pos);
let start = text.line_to_char(line);
std::cmp::max(
let pos = std::cmp::max(
nth_prev_grapheme_boundary(&text.slice(..), pos, count),
start,
)
);
Range::new(if extend { range.anchor } else { pos }, pos)
}
(Direction::Forward, Granularity::Character) => {
// Clamp to line
@ -111,16 +112,11 @@ impl State {
// Line end is pos at the start of next line - 1
// subtract another 1 because the line ends with \n
let end = text.line_to_char(line + 1).saturating_sub(2);
std::cmp::min(nth_next_grapheme_boundary(&text.slice(..), pos, count), end)
let pos =
std::cmp::min(nth_next_grapheme_boundary(&text.slice(..), pos, count), end);
Range::new(if extend { range.anchor } else { pos }, pos)
}
(Direction::Forward, Granularity::Word) => {
Self::move_next_word_start(&text.slice(..), pos)
}
(Direction::Backward, Granularity::Word) => {
Self::move_prev_word_start(&text.slice(..), pos)
}
(_, Granularity::Line) => move_vertically(&text.slice(..), dir, pos, count),
_ => pos,
(_, Granularity::Line) => move_vertically(&text.slice(..), dir, range, count, extend),
}
}
@ -205,10 +201,8 @@ impl State {
// move all selections according to normal cursor move semantics by collapsing it
// into cursors and moving them vertically
self.selection.transform(|range| {
let pos = self.move_pos(range.head, dir, granularity, count);
Range::new(pos, pos)
})
self.selection
.transform(|range| self.move_range(range, dir, granularity, count, false))
}
pub fn extend_selection(
@ -217,10 +211,8 @@ impl State {
granularity: Granularity,
count: usize,
) -> Selection {
self.selection.transform(|range| {
let pos = self.move_pos(range.head, dir, granularity, count);
Range::new(range.anchor, pos)
})
self.selection
.transform(|range| self.move_range(range, dir, granularity, count, true))
}
}
@ -239,8 +231,16 @@ pub fn pos_at_coords(text: &RopeSlice, coords: Position) -> usize {
nth_next_grapheme_boundary(text, line_start, col)
}
fn move_vertically(text: &RopeSlice, dir: Direction, pos: usize, count: usize) -> usize {
let Position { row, col } = coords_at_pos(text, pos);
fn move_vertically(
text: &RopeSlice,
dir: Direction,
range: Range,
count: usize,
extend: bool,
) -> Range {
let Position { row, col } = coords_at_pos(text, range.head);
let horiz = range.horiz.unwrap_or(col as u32);
let new_line = match dir {
Direction::Backward => row.saturating_sub(count),
@ -250,14 +250,14 @@ fn move_vertically(text: &RopeSlice, dir: Direction, pos: usize, count: usize) -
// convert to 0-indexed, subtract another 1 because len_chars() counts \n
let new_line_len = text.line(new_line).len_chars().saturating_sub(2);
let new_col = if new_line_len < col {
// TODO: preserve horiz here
new_line_len
} else {
col
};
let new_col = std::cmp::min(horiz as usize, new_line_len);
pos_at_coords(text, Position::new(new_line, new_col))
let pos = pos_at_coords(text, Position::new(new_line, new_col));
let mut range = Range::new(if extend { range.anchor } else { pos }, pos);
use std::convert::TryInto;
range.horiz = Some(horiz);
range
}
// used for by-word movement
@ -346,8 +346,12 @@ mod test {
let pos = pos_at_coords(&text.slice(..), (0, 4).into());
let slice = text.slice(..);
let range = Range::new(pos, pos);
assert_eq!(
coords_at_pos(&slice, move_vertically(&slice, Direction::Forward, pos, 1)),
coords_at_pos(
&slice,
move_vertically(&slice, Direction::Forward, range, 1).head
),
(1, 2).into()
);
}

View file

@ -116,12 +116,8 @@ pub fn move_line_start(cx: &mut Context) {
pub fn move_next_word_start(cx: &mut Context) {
let count = cx.count;
let doc = cx.doc();
let pos = doc.state.move_pos(
doc.selection().cursor(),
Direction::Forward,
Granularity::Word,
count,
);
// TODO: count
let pos = State::move_next_word_start(&doc.text().slice(..), doc.selection().cursor());
doc.set_selection(Selection::point(pos));
}
@ -129,12 +125,7 @@ pub fn move_next_word_start(cx: &mut Context) {
pub fn move_prev_word_start(cx: &mut Context) {
let count = cx.count;
let doc = cx.doc();
let pos = doc.state.move_pos(
doc.selection().cursor(),
Direction::Backward,
Granularity::Word,
count,
);
let pos = State::move_prev_word_start(&doc.text().slice(..), doc.selection().cursor());
doc.set_selection(Selection::point(pos));
}
@ -163,19 +154,36 @@ pub fn move_file_end(cx: &mut Context) {
pub fn extend_next_word_start(cx: &mut Context) {
let count = cx.count;
let selection = cx
.doc()
.state
.extend_selection(Direction::Forward, Granularity::Word, count);
let doc = cx.doc();
let mut selection = doc.selection().transform(|mut range| {
let pos = State::move_next_word_start(&doc.text().slice(..), doc.selection().cursor());
range.head = pos;
range
}); // TODO: count
cx.doc().set_selection(selection);
}
pub fn extend_prev_word_start(cx: &mut Context) {
let count = cx.count;
let selection = cx
.doc()
.state
.extend_selection(Direction::Backward, Granularity::Word, count);
let doc = cx.doc();
let mut selection = doc.selection().transform(|mut range| {
let pos = State::move_prev_word_start(&doc.text().slice(..), doc.selection().cursor());
range.head = pos;
range
}); // TODO: count
cx.doc().set_selection(selection);
}
pub fn extend_next_word_end(cx: &mut Context) {
let count = cx.count;
let doc = cx.doc();
let mut selection = doc.selection().transform(|mut range| {
let pos = State::move_next_word_end(&doc.text().slice(..), doc.selection().cursor(), count);
range.head = pos;
range
}); // TODO: count
cx.doc().set_selection(selection);
}
@ -320,8 +328,6 @@ pub fn split_selection(cx: &mut Context) {
// # update state
// }
let snapshot = cx.doc().state.clone();
let prompt = ui::regex_prompt(cx, "split:".to_string(), |doc, regex| {
let text = &doc.text().slice(..);
let selection = selection::split_on_matches(text, doc.selection(), &regex);

View file

@ -150,6 +150,7 @@ pub fn default() -> Keymaps {
vec![key!('b')] => commands::move_prev_word_start,
vec![shift!('B')] => commands::extend_prev_word_start,
vec![key!('e')] => commands::move_next_word_end,
vec![key!('E')] => commands::extend_next_word_end,
// TODO: E
vec![key!('g')] => commands::goto_mode,
vec![key!('i')] => commands::insert_mode,