on master help sign in
Finally: Retain horizontal position when moving vertically.
commit239db7983491192ad5abc676481c80f4e33bfb0b authorBlaž Hrastnik <blaz@mxxn.io> dateFri 12 Feb 2021, 16:49 (6 years ago) parentde5170d
5 files changed+94−60
TODO.md+9−1 helix-core/src/selection.rs+18−3 helix-core/src/state.rs+38−34 helix-term/src/commands.rs+28−22 helix-term/src/keymap.rs+1
TODO.md+9−1
@@ -12,8 +12,11 @@
1212- [x] % for whole doc selection
1313- [x] vertical splits
1414- [x] input counts (30j)
15+ - [ ] input counts for b, w, e
1516- [ ] respect view fullscreen flag
16−- [ ] retain horiz when moving vertically
17+- [x] retain horiz when moving vertically
18+- [x] deindent
19+- [ ] ensure_cursor_in_view always before rendering? or always in app after event process?
1720- [ ] update lsp on redo/undo
1821- [ ] Implement marks (superset of Selection/Range)
1922- [ ] ctrl-v/ctrl-x on file picker
@@ -22,12 +25,17 @@
2225- [ ] nixos packaging
2326- [ ] CI binary builds
2427
28+- [ ] regex search / select next
29+- [ ] f / t mappings
30+
31+
25322
2633- extend selection (treesitter select parent node) (replaces viw, vi(, va( etc )
2734- bracket pairs
2835- comment block (gcc)
2936- completion signature popups/docs
3037- multiple views into the same file
38+- selection align
3139
32403
3341- diagnostics popups
helix-core/src/selection.rs+18−3
@@ -23,11 +23,16 @@ pub struct Range {
2323 pub anchor: usize,
2424 /// The head of the range, moved when extending.
2525 pub head: usize,
26+ pub horiz: Option<u32>,
2627} // TODO: might be cheaper to store normalized as from/to and an inverted flag
2728
2829impl Range {
2930 pub fn new(anchor: usize, head: usize) -> Self {
30− Self { anchor, head }
31+ Self {
32+ anchor,
33+ head,
34+ horiz: None,
35+ }
3136 }
3237
3338 /// Start of the range.
@@ -83,7 +88,11 @@ impl Range {
8388 if self.anchor == anchor && self.head == head {
8489 return self;
8590 }
86− Self { anchor, head }
91+ Self {
92+ anchor,
93+ head,
94+ horiz: None,
95+ }
8796 }
8897
8998 /// Extend the range to cover at least `from` `to`.
@@ -93,6 +102,7 @@ impl Range {
93102 return Range {
94103 anchor: from,
95104 head: to,
105+ horiz: None,
96106 };
97107 }
98108
@@ -103,6 +113,7 @@ impl Range {
103113 } else {
104114 to
105115 },
116+ horiz: None,
106117 }
107118 }
108119
@@ -174,7 +185,11 @@ impl Selection {
174185 /// Constructs a selection holding a single range.
175186 pub fn single(anchor: usize, head: usize) -> Self {
176187 Self {
177− ranges: smallvec![Range { anchor, head }],
188+ ranges: smallvec![Range {
189+ anchor,
190+ head,
191+ horiz: None
192+ }],
178193 primary_index: 0,
179194 }
180195 }
helix-core/src/state.rs+38−34
@@ -19,9 +19,7 @@ pub enum Direction {
1919#[derive(Copy, Clone, PartialEq, Eq)]
2020pub enum Granularity {
2121 Character,
22− Word,
2322 Line,
24− // LineBoundary
2523}
2624
2725impl State {
@@ -87,23 +85,26 @@ impl State {
8785 // 2. compose onto a ongoing transaction
8886 // 3. on insert mode leave, that transaction gets stored into undo history
8987
90− pub fn move_pos(
88+ pub fn move_range(
9189 &self,
92− pos: usize,
90+ range: Range,
9391 dir: Direction,
9492 granularity: Granularity,
9593 count: usize,
96− ) -> usize {
94+ extend: bool,
95+ ) -> Range {
9796 let text = &self.doc;
97+ let pos = range.head;
9898 match (dir, granularity) {
9999 (Direction::Backward, Granularity::Character) => {
100100 // Clamp to line
101101 let line = text.char_to_line(pos);
102102 let start = text.line_to_char(line);
103− std::cmp::max(
103+ let pos = std::cmp::max(
104104 nth_prev_grapheme_boundary(&text.slice(..), pos, count),
105105 start,
106− )
106+ );
107+ Range::new(if extend { range.anchor } else { pos }, pos)
107108 }
108109 (Direction::Forward, Granularity::Character) => {
109110 // Clamp to line
@@ -111,16 +112,11 @@ impl State {
111112 // Line end is pos at the start of next line - 1
112113 // subtract another 1 because the line ends with \n
113114 let end = text.line_to_char(line + 1).saturating_sub(2);
114− std::cmp::min(nth_next_grapheme_boundary(&text.slice(..), pos, count), end)
115− }
116− (Direction::Forward, Granularity::Word) => {
117− Self::move_next_word_start(&text.slice(..), pos)
115+ let pos =
116+ std::cmp::min(nth_next_grapheme_boundary(&text.slice(..), pos, count), end);
117+ Range::new(if extend { range.anchor } else { pos }, pos)
118118 }
119− (Direction::Backward, Granularity::Word) => {
120− Self::move_prev_word_start(&text.slice(..), pos)
121− }
122− (_, Granularity::Line) => move_vertically(&text.slice(..), dir, pos, count),
123− _ => pos,
119+ (_, Granularity::Line) => move_vertically(&text.slice(..), dir, range, count, extend),
124120 }
125121 }
126122
@@ -205,10 +201,8 @@ impl State {
205201 // move all selections according to normal cursor move semantics by collapsing it
206202 // into cursors and moving them vertically
207203
208− self.selection.transform(|range| {
209− let pos = self.move_pos(range.head, dir, granularity, count);
210− Range::new(pos, pos)
211− })
204+ self.selection
205+ .transform(|range| self.move_range(range, dir, granularity, count, false))
212206 }
213207
214208 pub fn extend_selection(
@@ -217,10 +211,8 @@ impl State {
217211 granularity: Granularity,
218212 count: usize,
219213 ) -> Selection {
220− self.selection.transform(|range| {
221− let pos = self.move_pos(range.head, dir, granularity, count);
222− Range::new(range.anchor, pos)
223− })
214+ self.selection
215+ .transform(|range| self.move_range(range, dir, granularity, count, true))
224216 }
225217}
226218
@@ -239,8 +231,16 @@ pub fn pos_at_coords(text: &RopeSlice, coords: Position) -> usize {
239231 nth_next_grapheme_boundary(text, line_start, col)
240232}
241233
242−fn move_vertically(text: &RopeSlice, dir: Direction, pos: usize, count: usize) -> usize {
243− let Position { row, col } = coords_at_pos(text, pos);
234+fn move_vertically(
235+ text: &RopeSlice,
236+ dir: Direction,
237+ range: Range,
238+ count: usize,
239+ extend: bool,
240+) -> Range {
241+ let Position { row, col } = coords_at_pos(text, range.head);
242+
243+ let horiz = range.horiz.unwrap_or(col as u32);
244244
245245 let new_line = match dir {
246246 Direction::Backward => row.saturating_sub(count),
@@ -250,14 +250,14 @@ fn move_vertically(text: &RopeSlice, dir: Direction, pos: usize, count: usize) -
250250 // convert to 0-indexed, subtract another 1 because len_chars() counts \n
251251 let new_line_len = text.line(new_line).len_chars().saturating_sub(2);
252252
253− let new_col = if new_line_len < col {
254− // TODO: preserve horiz here
255− new_line_len
256− } else {
257− col
258− };
253+ let new_col = std::cmp::min(horiz as usize, new_line_len);
254+
255+ let pos = pos_at_coords(text, Position::new(new_line, new_col));
259256
260− pos_at_coords(text, Position::new(new_line, new_col))
257+ let mut range = Range::new(if extend { range.anchor } else { pos }, pos);
258+ use std::convert::TryInto;
259+ range.horiz = Some(horiz);
260+ range
261261}
262262
263263// used for by-word movement
@@ -346,8 +346,12 @@ mod test {
346346 let pos = pos_at_coords(&text.slice(..), (0, 4).into());
347347 let slice = text.slice(..);
348348
349+ let range = Range::new(pos, pos);
349350 assert_eq!(
350− coords_at_pos(&slice, move_vertically(&slice, Direction::Forward, pos, 1)),
351+ coords_at_pos(
352+ &slice,
353+ move_vertically(&slice, Direction::Forward, range, 1).head
354+ ),
351355 (1, 2).into()
352356 );
353357 }
helix-term/src/commands.rs+28−22
@@ -116,12 +116,8 @@ pub fn move_line_start(cx: &mut Context) {
116116pub fn move_next_word_start(cx: &mut Context) {
117117 let count = cx.count;
118118 let doc = cx.doc();
119− let pos = doc.state.move_pos(
120− doc.selection().cursor(),
121− Direction::Forward,
122− Granularity::Word,
123− count,
124− );
119+ // TODO: count
120+ let pos = State::move_next_word_start(&doc.text().slice(..), doc.selection().cursor());
125121
126122 doc.set_selection(Selection::point(pos));
127123}
@@ -129,12 +125,7 @@ pub fn move_next_word_start(cx: &mut Context) {
129125pub fn move_prev_word_start(cx: &mut Context) {
130126 let count = cx.count;
131127 let doc = cx.doc();
132− let pos = doc.state.move_pos(
133− doc.selection().cursor(),
134− Direction::Backward,
135− Granularity::Word,
136− count,
137− );
128+ let pos = State::move_prev_word_start(&doc.text().slice(..), doc.selection().cursor());
138129
139130 doc.set_selection(Selection::point(pos));
140131}
@@ -163,19 +154,36 @@ pub fn move_file_end(cx: &mut Context) {
163154
164155pub fn extend_next_word_start(cx: &mut Context) {
165156 let count = cx.count;
166− let selection = cx
167− .doc()
168− .state
169− .extend_selection(Direction::Forward, Granularity::Word, count);
157+ let doc = cx.doc();
158+ let mut selection = doc.selection().transform(|mut range| {
159+ let pos = State::move_next_word_start(&doc.text().slice(..), doc.selection().cursor());
160+ range.head = pos;
161+ range
162+ }); // TODO: count
163+
170164 cx.doc().set_selection(selection);
171165}
172166
173167pub fn extend_prev_word_start(cx: &mut Context) {
174168 let count = cx.count;
175− let selection = cx
176− .doc()
177− .state
178− .extend_selection(Direction::Backward, Granularity::Word, count);
169+ let doc = cx.doc();
170+ let mut selection = doc.selection().transform(|mut range| {
171+ let pos = State::move_prev_word_start(&doc.text().slice(..), doc.selection().cursor());
172+ range.head = pos;
173+ range
174+ }); // TODO: count
175+ cx.doc().set_selection(selection);
176+}
177+
178+pub fn extend_next_word_end(cx: &mut Context) {
179+ let count = cx.count;
180+ let doc = cx.doc();
181+ let mut selection = doc.selection().transform(|mut range| {
182+ let pos = State::move_next_word_end(&doc.text().slice(..), doc.selection().cursor(), count);
183+ range.head = pos;
184+ range
185+ }); // TODO: count
186+
179187 cx.doc().set_selection(selection);
180188}
181189
@@ -320,8 +328,6 @@ pub fn split_selection(cx: &mut Context) {
320328 // # update state
321329 // }
322330
323− let snapshot = cx.doc().state.clone();
324−
325331 let prompt = ui::regex_prompt(cx, "split:".to_string(), |doc, regex| {
326332 let text = &doc.text().slice(..);
327333 let selection = selection::split_on_matches(text, doc.selection(), &regex);
helix-term/src/keymap.rs+1
@@ -150,6 +150,7 @@ pub fn default() -> Keymaps {
150150 vec![key!('b')] => commands::move_prev_word_start,
151151 vec![shift!('B')] => commands::extend_prev_word_start,
152152 vec![key!('e')] => commands::move_next_word_end,
153+ vec![key!('E')] => commands::extend_next_word_end,
153154 // TODO: E
154155 vec![key!('g')] => commands::goto_mode,
155156 vec![key!('i')] => commands::insert_mode,