on master help sign in
Finally: Retain horizontal position when moving vertically.
239db79Blaž Hrastnik
TODO.md+9−1
@@ -12,8 +12,11 @@
12- [x] % for whole doc selection
12- [x] % for whole doc selection
13- [x] vertical splits
13- [x] vertical splits
14- [x] input counts (30j)
14- [x] input counts (30j)
15+ - [ ] input counts for b, w, e
15- [ ] respect view fullscreen flag
16- [ ] 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?
17- [ ] update lsp on redo/undo
20- [ ] update lsp on redo/undo
18- [ ] Implement marks (superset of Selection/Range)
21- [ ] Implement marks (superset of Selection/Range)
19- [ ] ctrl-v/ctrl-x on file picker
22- [ ] ctrl-v/ctrl-x on file picker
@@ -22,12 +25,17 @@
22- [ ] nixos packaging
25- [ ] nixos packaging
23- [ ] CI binary builds
26- [ ] CI binary builds
24
27
28+- [ ] regex search / select next
29+- [ ] f / t mappings
30+
31+
252
322
26- extend selection (treesitter select parent node) (replaces viw, vi(, va( etc )
33- extend selection (treesitter select parent node) (replaces viw, vi(, va( etc )
27- bracket pairs
34- bracket pairs
28- comment block (gcc)
35- comment block (gcc)
29- completion signature popups/docs
36- completion signature popups/docs
30- multiple views into the same file
37- multiple views into the same file
38+- selection align
31
39
323
403
33- diagnostics popups
41- diagnostics popups
helix-core/src/selection.rs+18−3
@@ -23,11 +23,16 @@ pub struct Range {
23 pub anchor: usize,
23 pub anchor: usize,
24 /// The head of the range, moved when extending.
24 /// The head of the range, moved when extending.
25 pub head: usize,
25 pub head: usize,
26+ pub horiz: Option<u32>,
26} // TODO: might be cheaper to store normalized as from/to and an inverted flag
27} // TODO: might be cheaper to store normalized as from/to and an inverted flag
27
28
28impl Range {
29impl Range {
29 pub fn new(anchor: usize, head: usize) -> Self {
30 pub fn new(anchor: usize, head: usize) -> Self {
31+ Self {
30− Self { anchor, head }
32+ anchor,
33+ head,
34+ horiz: None,
35+ }
31 }
36 }
32
37
33 /// Start of the range.
38 /// Start of the range.
@@ -83,7 +88,11 @@ impl Range {
83 if self.anchor == anchor && self.head == head {
88 if self.anchor == anchor && self.head == head {
84 return self;
89 return self;
85 }
90 }
91+ Self {
86− Self { anchor, head }
92+ anchor,
93+ head,
94+ horiz: None,
95+ }
87 }
96 }
88
97
89 /// Extend the range to cover at least `from` `to`.
98 /// Extend the range to cover at least `from` `to`.
@@ -93,6 +102,7 @@ impl Range {
93 return Range {
102 return Range {
94 anchor: from,
103 anchor: from,
95 head: to,
104 head: to,
105+ horiz: None,
96 };
106 };
97 }
107 }
98
108
@@ -103,6 +113,7 @@ impl Range {
103 } else {
113 } else {
104 to
114 to
105 },
115 },
116+ horiz: None,
106 }
117 }
107 }
118 }
108
119
@@ -174,7 +185,11 @@ impl Selection {
174 /// Constructs a selection holding a single range.
185 /// Constructs a selection holding a single range.
175 pub fn single(anchor: usize, head: usize) -> Self {
186 pub fn single(anchor: usize, head: usize) -> Self {
176 Self {
187 Self {
177− ranges: smallvec![Range { anchor, head }],
188+ ranges: smallvec![Range {
189+ anchor,
190+ head,
191+ horiz: None
192+ }],
178 primary_index: 0,
193 primary_index: 0,
179 }
194 }
180 }
195 }
helix-core/src/state.rs+38−34
@@ -19,9 +19,7 @@ pub enum Direction {
19#[derive(Copy, Clone, PartialEq, Eq)]
19#[derive(Copy, Clone, PartialEq, Eq)]
20pub enum Granularity {
20pub enum Granularity {
21 Character,
21 Character,
22− Word,
23 Line,
22 Line,
24− // LineBoundary
25}
23}
26
24
27impl State {
25impl State {
@@ -87,23 +85,26 @@ impl State {
87 // 2. compose onto a ongoing transaction
85 // 2. compose onto a ongoing transaction
88 // 3. on insert mode leave, that transaction gets stored into undo history
86 // 3. on insert mode leave, that transaction gets stored into undo history
89
87
90− pub fn move_pos(
88+ pub fn move_range(
91 &self,
89 &self,
92− pos: usize,
90+ range: Range,
93 dir: Direction,
91 dir: Direction,
94 granularity: Granularity,
92 granularity: Granularity,
95 count: usize,
93 count: usize,
96− ) -> usize {
94+ extend: bool,
95+ ) -> Range {
97 let text = &self.doc;
96 let text = &self.doc;
97+ let pos = range.head;
98 match (dir, granularity) {
98 match (dir, granularity) {
99 (Direction::Backward, Granularity::Character) => {
99 (Direction::Backward, Granularity::Character) => {
100 // Clamp to line
100 // Clamp to line
101 let line = text.char_to_line(pos);
101 let line = text.char_to_line(pos);
102 let start = text.line_to_char(line);
102 let start = text.line_to_char(line);
103− std::cmp::max(
103+ let pos = std::cmp::max(
104 nth_prev_grapheme_boundary(&text.slice(..), pos, count),
104 nth_prev_grapheme_boundary(&text.slice(..), pos, count),
105 start,
105 start,
106− )
106+ );
107+ Range::new(if extend { range.anchor } else { pos }, pos)
107 }
108 }
108 (Direction::Forward, Granularity::Character) => {
109 (Direction::Forward, Granularity::Character) => {
109 // Clamp to line
110 // Clamp to line
@@ -111,16 +112,11 @@ impl State {
111 // Line end is pos at the start of next line - 1
112 // Line end is pos at the start of next line - 1
112 // subtract another 1 because the line ends with \n
113 // subtract another 1 because the line ends with \n
113 let end = text.line_to_char(line + 1).saturating_sub(2);
114 let end = text.line_to_char(line + 1).saturating_sub(2);
115+ let pos =
114− std::cmp::min(nth_next_grapheme_boundary(&text.slice(..), pos, count), end)
116+ std::cmp::min(nth_next_grapheme_boundary(&text.slice(..), pos, count), end);
115− }
117+ Range::new(if extend { range.anchor } else { pos }, pos)
116− (Direction::Forward, Granularity::Word) => {
117− Self::move_next_word_start(&text.slice(..), pos)
118 }
118 }
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),
119+ (_, Granularity::Line) => move_vertically(&text.slice(..), dir, range, count, extend),
123− _ => pos,
124 }
120 }
125 }
121 }
126
122
@@ -205,10 +201,8 @@ impl State {
205 // move all selections according to normal cursor move semantics by collapsing it
201 // move all selections according to normal cursor move semantics by collapsing it
206 // into cursors and moving them vertically
202 // into cursors and moving them vertically
207
203
208− self.selection.transform(|range| {
204+ self.selection
209− let pos = self.move_pos(range.head, dir, granularity, count);
205+ .transform(|range| self.move_range(range, dir, granularity, count, false))
210− Range::new(pos, pos)
211− })
212 }
206 }
213
207
214 pub fn extend_selection(
208 pub fn extend_selection(
@@ -217,10 +211,8 @@ impl State {
217 granularity: Granularity,
211 granularity: Granularity,
218 count: usize,
212 count: usize,
219 ) -> Selection {
213 ) -> Selection {
220− self.selection.transform(|range| {
214+ self.selection
221− let pos = self.move_pos(range.head, dir, granularity, count);
215+ .transform(|range| self.move_range(range, dir, granularity, count, true))
222− Range::new(range.anchor, pos)
223− })
224 }
216 }
225}
217}
226
218
@@ -239,8 +231,16 @@ pub fn pos_at_coords(text: &RopeSlice, coords: Position) -> usize {
239 nth_next_grapheme_boundary(text, line_start, col)
231 nth_next_grapheme_boundary(text, line_start, col)
240}
232}
241
233
242−fn move_vertically(text: &RopeSlice, dir: Direction, pos: usize, count: usize) -> usize {
234+fn move_vertically(
235+ text: &RopeSlice,
236+ dir: Direction,
237+ range: Range,
238+ count: usize,
239+ extend: bool,
240+) -> Range {
243− let Position { row, col } = coords_at_pos(text, pos);
241+ let Position { row, col } = coords_at_pos(text, range.head);
242+
243+ let horiz = range.horiz.unwrap_or(col as u32);
244
244
245 let new_line = match dir {
245 let new_line = match dir {
246 Direction::Backward => row.saturating_sub(count),
246 Direction::Backward => row.saturating_sub(count),
@@ -250,14 +250,14 @@ fn move_vertically(text: &RopeSlice, dir: Direction, pos: usize, count: usize) -
250 // convert to 0-indexed, subtract another 1 because len_chars() counts \n
250 // convert to 0-indexed, subtract another 1 because len_chars() counts \n
251 let new_line_len = text.line(new_line).len_chars().saturating_sub(2);
251 let new_line_len = text.line(new_line).len_chars().saturating_sub(2);
252
252
253− let new_col = if new_line_len < col {
253+ let new_col = std::cmp::min(horiz as usize, new_line_len);
254− // TODO: preserve horiz here
254+
255− new_line_len
255+ let pos = pos_at_coords(text, Position::new(new_line, new_col));
256− } else {
257− col
258− };
259
256
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
261}
261}
262
262
263// used for by-word movement
263// used for by-word movement
@@ -346,8 +346,12 @@ mod test {
346 let pos = pos_at_coords(&text.slice(..), (0, 4).into());
346 let pos = pos_at_coords(&text.slice(..), (0, 4).into());
347 let slice = text.slice(..);
347 let slice = text.slice(..);
348
348
349+ let range = Range::new(pos, pos);
349 assert_eq!(
350 assert_eq!(
351+ coords_at_pos(
352+ &slice,
350− coords_at_pos(&slice, move_vertically(&slice, Direction::Forward, pos, 1)),
353+ move_vertically(&slice, Direction::Forward, range, 1).head
354+ ),
351 (1, 2).into()
355 (1, 2).into()
352 );
356 );
353 }
357 }
helix-term/src/commands.rs+28−22
@@ -116,12 +116,8 @@ pub fn move_line_start(cx: &mut Context) {
116pub fn move_next_word_start(cx: &mut Context) {
116pub fn move_next_word_start(cx: &mut Context) {
117 let count = cx.count;
117 let count = cx.count;
118 let doc = cx.doc();
118 let doc = cx.doc();
119− let pos = doc.state.move_pos(
119+ // TODO: count
120− doc.selection().cursor(),
120+ let pos = State::move_next_word_start(&doc.text().slice(..), doc.selection().cursor());
121− Direction::Forward,
122− Granularity::Word,
123− count,
124− );
125
121
126 doc.set_selection(Selection::point(pos));
122 doc.set_selection(Selection::point(pos));
127}
123}
@@ -129,12 +125,7 @@ pub fn move_next_word_start(cx: &mut Context) {
129pub fn move_prev_word_start(cx: &mut Context) {
125pub fn move_prev_word_start(cx: &mut Context) {
130 let count = cx.count;
126 let count = cx.count;
131 let doc = cx.doc();
127 let doc = cx.doc();
132− let pos = doc.state.move_pos(
133− doc.selection().cursor(),
128+ let pos = State::move_prev_word_start(&doc.text().slice(..), doc.selection().cursor());
134− Direction::Backward,
135− Granularity::Word,
136− count,
137− );
138
129
139 doc.set_selection(Selection::point(pos));
130 doc.set_selection(Selection::point(pos));
140}
131}
@@ -163,19 +154,36 @@ pub fn move_file_end(cx: &mut Context) {
163
154
164pub fn extend_next_word_start(cx: &mut Context) {
155pub fn extend_next_word_start(cx: &mut Context) {
165 let count = cx.count;
156 let count = cx.count;
166− let selection = cx
157+ let doc = cx.doc();
167− .doc()
158+ let mut selection = doc.selection().transform(|mut range| {
168− .state
159+ let pos = State::move_next_word_start(&doc.text().slice(..), doc.selection().cursor());
169− .extend_selection(Direction::Forward, Granularity::Word, count);
160+ range.head = pos;
161+ range
162+ }); // TODO: count
163+
170 cx.doc().set_selection(selection);
164 cx.doc().set_selection(selection);
171}
165}
172
166
173pub fn extend_prev_word_start(cx: &mut Context) {
167pub fn extend_prev_word_start(cx: &mut Context) {
174 let count = cx.count;
168 let count = cx.count;
175− let selection = cx
169+ let doc = cx.doc();
176− .doc()
170+ let mut selection = doc.selection().transform(|mut range| {
177− .state
171+ let pos = State::move_prev_word_start(&doc.text().slice(..), doc.selection().cursor());
178− .extend_selection(Direction::Backward, Granularity::Word, count);
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+
179 cx.doc().set_selection(selection);
187 cx.doc().set_selection(selection);
180}
188}
181
189
@@ -320,8 +328,6 @@ pub fn split_selection(cx: &mut Context) {
320 // # update state
328 // # update state
321 // }
329 // }
322
330
323− let snapshot = cx.doc().state.clone();
324−
325 let prompt = ui::regex_prompt(cx, "split:".to_string(), |doc, regex| {
331 let prompt = ui::regex_prompt(cx, "split:".to_string(), |doc, regex| {
326 let text = &doc.text().slice(..);
332 let text = &doc.text().slice(..);
327 let selection = selection::split_on_matches(text, doc.selection(), &regex);
333 let selection = selection::split_on_matches(text, doc.selection(), &regex);
helix-term/src/keymap.rs+1
@@ -150,6 +150,7 @@ pub fn default() -> Keymaps {
150 vec![key!('b')] => commands::move_prev_word_start,
150 vec![key!('b')] => commands::move_prev_word_start,
151 vec![shift!('B')] => commands::extend_prev_word_start,
151 vec![shift!('B')] => commands::extend_prev_word_start,
152 vec![key!('e')] => commands::move_next_word_end,
152 vec![key!('e')] => commands::move_next_word_end,
153+ vec![key!('E')] => commands::extend_next_word_end,
153 // TODO: E
154 // TODO: E
154 vec![key!('g')] => commands::goto_mode,
155 vec![key!('g')] => commands::goto_mode,
155 vec![key!('i')] => commands::insert_mode,
156 vec![key!('i')] => commands::insert_mode,