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] % for whole doc selection
- [x] vertical splits - [x] vertical splits
- [x] input counts (30j) - [x] input counts (30j)
- [ ] input counts for b, w, e
- [ ] respect view fullscreen flag - [ ] 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 - [ ] update lsp on redo/undo
- [ ] Implement marks (superset of Selection/Range) - [ ] Implement marks (superset of Selection/Range)
- [ ] ctrl-v/ctrl-x on file picker - [ ] ctrl-v/ctrl-x on file picker
@ -22,12 +25,17 @@
- [ ] nixos packaging - [ ] nixos packaging
- [ ] CI binary builds - [ ] CI binary builds
- [ ] regex search / select next
- [ ] f / t mappings
2 2
- extend selection (treesitter select parent node) (replaces viw, vi(, va( etc ) - extend selection (treesitter select parent node) (replaces viw, vi(, va( etc )
- bracket pairs - bracket pairs
- comment block (gcc) - comment block (gcc)
- completion signature popups/docs - completion signature popups/docs
- multiple views into the same file - multiple views into the same file
- selection align
3 3
- diagnostics popups - diagnostics popups

View File

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

View File

@ -19,9 +19,7 @@ pub enum Direction {
#[derive(Copy, Clone, PartialEq, Eq)] #[derive(Copy, Clone, PartialEq, Eq)]
pub enum Granularity { pub enum Granularity {
Character, Character,
Word,
Line, Line,
// LineBoundary
} }
impl State { impl State {
@ -87,23 +85,26 @@ pub fn selection(&self) -> &Selection {
// 2. compose onto a ongoing transaction // 2. compose onto a ongoing transaction
// 3. on insert mode leave, that transaction gets stored into undo history // 3. on insert mode leave, that transaction gets stored into undo history
pub fn move_pos( pub fn move_range(
&self, &self,
pos: usize, range: Range,
dir: Direction, dir: Direction,
granularity: Granularity, granularity: Granularity,
count: usize, count: usize,
) -> usize { extend: bool,
) -> Range {
let text = &self.doc; let text = &self.doc;
let pos = range.head;
match (dir, granularity) { match (dir, granularity) {
(Direction::Backward, Granularity::Character) => { (Direction::Backward, Granularity::Character) => {
// Clamp to line // Clamp to line
let line = text.char_to_line(pos); let line = text.char_to_line(pos);
let start = text.line_to_char(line); let start = text.line_to_char(line);
std::cmp::max( let pos = std::cmp::max(
nth_prev_grapheme_boundary(&text.slice(..), pos, count), nth_prev_grapheme_boundary(&text.slice(..), pos, count),
start, start,
) );
Range::new(if extend { range.anchor } else { pos }, pos)
} }
(Direction::Forward, Granularity::Character) => { (Direction::Forward, Granularity::Character) => {
// Clamp to line // Clamp to line
@ -111,16 +112,11 @@ pub fn move_pos(
// Line end is pos at the start of next line - 1 // Line end is pos at the start of next line - 1
// subtract another 1 because the line ends with \n // subtract another 1 because the line ends with \n
let end = text.line_to_char(line + 1).saturating_sub(2); 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) => { (_, Granularity::Line) => move_vertically(&text.slice(..), dir, range, count, extend),
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,
} }
} }
@ -205,10 +201,8 @@ pub fn move_selection(
// move all selections according to normal cursor move semantics by collapsing it // move all selections according to normal cursor move semantics by collapsing it
// into cursors and moving them vertically // into cursors and moving them vertically
self.selection.transform(|range| { self.selection
let pos = self.move_pos(range.head, dir, granularity, count); .transform(|range| self.move_range(range, dir, granularity, count, false))
Range::new(pos, pos)
})
} }
pub fn extend_selection( pub fn extend_selection(
@ -217,10 +211,8 @@ pub fn extend_selection(
granularity: Granularity, granularity: Granularity,
count: usize, count: usize,
) -> Selection { ) -> Selection {
self.selection.transform(|range| { self.selection
let pos = self.move_pos(range.head, dir, granularity, count); .transform(|range| self.move_range(range, dir, granularity, count, true))
Range::new(range.anchor, pos)
})
} }
} }
@ -239,8 +231,16 @@ pub fn pos_at_coords(text: &RopeSlice, coords: Position) -> usize {
nth_next_grapheme_boundary(text, line_start, col) nth_next_grapheme_boundary(text, line_start, col)
} }
fn move_vertically(text: &RopeSlice, dir: Direction, pos: usize, count: usize) -> usize { fn move_vertically(
let Position { row, col } = coords_at_pos(text, pos); 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 { let new_line = match dir {
Direction::Backward => row.saturating_sub(count), 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 // 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_line_len = text.line(new_line).len_chars().saturating_sub(2);
let new_col = if new_line_len < col { let new_col = std::cmp::min(horiz as usize, new_line_len);
// TODO: preserve horiz here
new_line_len
} else {
col
};
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 // used for by-word movement
@ -346,8 +346,12 @@ fn test_vertical_move() {
let pos = pos_at_coords(&text.slice(..), (0, 4).into()); let pos = pos_at_coords(&text.slice(..), (0, 4).into());
let slice = text.slice(..); let slice = text.slice(..);
let range = Range::new(pos, pos);
assert_eq!( 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() (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) { pub fn move_next_word_start(cx: &mut Context) {
let count = cx.count; let count = cx.count;
let doc = cx.doc(); let doc = cx.doc();
let pos = doc.state.move_pos( // TODO: count
doc.selection().cursor(), let pos = State::move_next_word_start(&doc.text().slice(..), doc.selection().cursor());
Direction::Forward,
Granularity::Word,
count,
);
doc.set_selection(Selection::point(pos)); 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) { pub fn move_prev_word_start(cx: &mut Context) {
let count = cx.count; let count = cx.count;
let doc = cx.doc(); let doc = cx.doc();
let pos = doc.state.move_pos( let pos = State::move_prev_word_start(&doc.text().slice(..), doc.selection().cursor());
doc.selection().cursor(),
Direction::Backward,
Granularity::Word,
count,
);
doc.set_selection(Selection::point(pos)); 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) { pub fn extend_next_word_start(cx: &mut Context) {
let count = cx.count; let count = cx.count;
let selection = cx let doc = cx.doc();
.doc() let mut selection = doc.selection().transform(|mut range| {
.state let pos = State::move_next_word_start(&doc.text().slice(..), doc.selection().cursor());
.extend_selection(Direction::Forward, Granularity::Word, count); range.head = pos;
range
}); // TODO: count
cx.doc().set_selection(selection); cx.doc().set_selection(selection);
} }
pub fn extend_prev_word_start(cx: &mut Context) { pub fn extend_prev_word_start(cx: &mut Context) {
let count = cx.count; let count = cx.count;
let selection = cx let doc = cx.doc();
.doc() let mut selection = doc.selection().transform(|mut range| {
.state let pos = State::move_prev_word_start(&doc.text().slice(..), doc.selection().cursor());
.extend_selection(Direction::Backward, Granularity::Word, count); 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); cx.doc().set_selection(selection);
} }
@ -320,8 +328,6 @@ pub fn split_selection(cx: &mut Context) {
// # update state // # update state
// } // }
let snapshot = cx.doc().state.clone();
let prompt = ui::regex_prompt(cx, "split:".to_string(), |doc, regex| { let prompt = ui::regex_prompt(cx, "split:".to_string(), |doc, regex| {
let text = &doc.text().slice(..); let text = &doc.text().slice(..);
let selection = selection::split_on_matches(text, doc.selection(), &regex); 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![key!('b')] => commands::move_prev_word_start,
vec![shift!('B')] => commands::extend_prev_word_start, vec![shift!('B')] => commands::extend_prev_word_start,
vec![key!('e')] => commands::move_next_word_end, vec![key!('e')] => commands::move_next_word_end,
vec![key!('E')] => commands::extend_next_word_end,
// TODO: E // TODO: E
vec![key!('g')] => commands::goto_mode, vec![key!('g')] => commands::goto_mode,
vec![key!('i')] => commands::insert_mode, vec![key!('i')] => commands::insert_mode,