mirror of
https://github.com/helix-editor/helix.git
synced 2024-11-22 17:36:19 +04:00
Finally: Retain horizontal position when moving vertically.
This commit is contained in:
parent
de5170dcda
commit
239db79834
10
TODO.md
10
TODO.md
@ -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
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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(), ®ex);
|
let selection = selection::split_on_matches(text, doc.selection(), ®ex);
|
||||||
|
@ -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,
|
||||||
|
Loading…
Reference in New Issue
Block a user