on master help sign in
Move things out of state.rs.
commitdbcc099f484ad186782370ed0cf61b3e8d27282b authorBlaž Hrastnik <blaz@mxxn.io> dateThu 18 Mar 2021, 13:39 (6 years ago) parent0f66244
6 files changed+245−233
helix-core/src/lib.rs+2−1 helix-core/src/movement.rs+157 helix-core/src/position.rs+74 helix-core/src/state.rs+1−221 helix-core/src/syntax.rs+1−1 helix-term/src/commands.rs+10−10
helix-core/src/lib.rs+2−1
@@ -5,6 +5,7 @@ pub mod graphemes;
55mod history;
66pub mod indent;
77pub mod macros;
8+pub mod movement;
89pub mod object;
910mod position;
1011pub mod register;
@@ -48,7 +49,7 @@ pub use tendril::StrTendril as Tendril;
4849#[doc(inline)]
4950pub use {regex, tree_sitter};
5051
51−pub use position::Position;
52+pub use position::{coords_at_pos, pos_at_coords, Position};
5253pub use selection::Range;
5354pub use selection::Selection;
5455pub use syntax::Syntax;
helix-core/src/movement.rsnew file+157
@@ -0,0 +1,157 @@
1+use crate::graphemes::{nth_next_grapheme_boundary, nth_prev_grapheme_boundary, RopeGraphemes};
2+use crate::{coords_at_pos, pos_at_coords, ChangeSet, Position, Range, Rope, RopeSlice, Selection};
3+
4+pub fn move_next_word_start(slice: RopeSlice, mut pos: usize, count: usize) -> usize {
5+ for _ in 0..count {
6+ if pos + 1 == slice.len_chars() {
7+ return pos;
8+ }
9+
10+ let mut ch = slice.char(pos);
11+ let next = slice.char(pos + 1);
12+
13+ // if we're at the end of a word, or on whitespce right before new one
14+ if categorize(ch) != categorize(next) {
15+ pos += 1;
16+ ch = next;
17+ }
18+
19+ if is_word(ch) {
20+ skip_over_next(slice, &mut pos, is_word);
21+ } else if ch.is_ascii_punctuation() {
22+ skip_over_next(slice, &mut pos, |ch| ch.is_ascii_punctuation());
23+ }
24+
25+ // TODO: don't include newline?
26+ skip_over_next(slice, &mut pos, |ch| ch.is_ascii_whitespace());
27+ }
28+
29+ pos
30+}
31+
32+pub fn move_prev_word_start(slice: RopeSlice, mut pos: usize, count: usize) -> usize {
33+ for _ in 0..count {
34+ if pos == 0 {
35+ return pos;
36+ }
37+
38+ let ch = slice.char(pos);
39+ let prev = slice.char(pos - 1);
40+
41+ if categorize(ch) != categorize(prev) {
42+ pos -= 1;
43+ }
44+
45+ // match (category c1, category c2) => {
46+ // if c1 != c2 {
47+ // }
48+ // }
49+
50+ // TODO: skip while eol
51+
52+ // TODO: don't include newline?
53+ skip_over_prev(slice, &mut pos, |ch| ch.is_ascii_whitespace());
54+
55+ // refetch
56+ let ch = slice.char(pos);
57+
58+ if is_word(ch) {
59+ skip_over_prev(slice, &mut pos, is_word);
60+ } else if ch.is_ascii_punctuation() {
61+ skip_over_prev(slice, &mut pos, |ch| ch.is_ascii_punctuation());
62+ }
63+ pos = pos.saturating_add(1)
64+ }
65+
66+ pos
67+}
68+
69+pub fn move_next_word_end(slice: RopeSlice, mut pos: usize, count: usize) -> usize {
70+ for _ in 0..count {
71+ if pos + 1 == slice.len_chars() {
72+ return pos;
73+ }
74+
75+ let ch = slice.char(pos);
76+ let next = slice.char(pos + 1);
77+
78+ if categorize(ch) != categorize(next) {
79+ pos += 1;
80+ }
81+
82+ // TODO: don't include newline?
83+ skip_over_next(slice, &mut pos, |ch| ch.is_ascii_whitespace());
84+
85+ // refetch
86+ let ch = slice.char(pos);
87+
88+ if is_word(ch) {
89+ skip_over_next(slice, &mut pos, is_word);
90+ } else if ch.is_ascii_punctuation() {
91+ skip_over_next(slice, &mut pos, |ch| ch.is_ascii_punctuation());
92+ }
93+ pos -= 1
94+ }
95+
96+ pos
97+}
98+
99+// ---- util ------------
100+
101+// used for by-word movement
102+
103+fn is_word(ch: char) -> bool {
104+ ch.is_alphanumeric() || ch == '_'
105+}
106+
107+#[derive(Debug, Eq, PartialEq)]
108+enum Category {
109+ Whitespace,
110+ Eol,
111+ Word,
112+ Punctuation,
113+}
114+fn categorize(ch: char) -> Category {
115+ if ch == '\n' {
116+ Category::Eol
117+ } else if ch.is_ascii_whitespace() {
118+ Category::Whitespace
119+ } else if ch.is_ascii_punctuation() {
120+ Category::Punctuation
121+ } else if ch.is_ascii_alphanumeric() {
122+ Category::Word
123+ } else {
124+ unreachable!()
125+ }
126+}
127+
128+#[inline]
129+pub fn skip_over_next<F>(slice: RopeSlice, pos: &mut usize, fun: F)
130+where
131+ F: Fn(char) -> bool,
132+{
133+ let mut chars = slice.chars_at(*pos);
134+
135+ for ch in chars {
136+ if !fun(ch) {
137+ break;
138+ }
139+ *pos += 1;
140+ }
141+}
142+
143+#[inline]
144+pub fn skip_over_prev<F>(slice: RopeSlice, pos: &mut usize, fun: F)
145+where
146+ F: Fn(char) -> bool,
147+{
148+ // need to +1 so that prev() includes current char
149+ let mut chars = slice.chars_at(*pos + 1);
150+
151+ while let Some(ch) = chars.prev() {
152+ if !fun(ch) {
153+ break;
154+ }
155+ *pos = pos.saturating_sub(1);
156+ }
157+}
helix-core/src/position.rs+74
@@ -1,3 +1,8 @@
1+use crate::{
2+ graphemes::{nth_next_grapheme_boundary, RopeGraphemes},
3+ Rope, RopeSlice,
4+};
5+
16/// Represents a single point in a text buffer. Zero indexed.
27#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
38pub struct Position {
@@ -44,6 +49,21 @@ impl From<Position> for tree_sitter::Point {
4449 Self::new(pos.row, pos.col)
4550 }
4651}
52+/// Convert a character index to (line, column) coordinates.
53+pub fn coords_at_pos(text: RopeSlice, pos: usize) -> Position {
54+ let line = text.char_to_line(pos);
55+ let line_start = text.line_to_char(line);
56+ let col = RopeGraphemes::new(text.slice(line_start..pos)).count();
57+ Position::new(line, col)
58+}
59+
60+/// Convert (line, column) coordinates to a character index.
61+pub fn pos_at_coords(text: RopeSlice, coords: Position) -> usize {
62+ let Position { row, col } = coords;
63+ let line_start = text.line_to_char(row);
64+ // line_start + col
65+ nth_next_grapheme_boundary(text, line_start, col)
66+}
4767
4868#[cfg(test)]
4969mod test {
@@ -54,4 +74,58 @@ mod test {
5474 // (0, 5) is less than (1, 0)
5575 assert!(Position::new(0, 5) < Position::new(1, 0));
5676 }
77+
78+ #[test]
79+ fn test_coords_at_pos() {
80+ let text = Rope::from("ḧëḷḷö\nẅöṛḷḋ");
81+ let slice = text.slice(..);
82+ // assert_eq!(coords_at_pos(slice, 0), (0, 0).into());
83+ // assert_eq!(coords_at_pos(slice, 5), (0, 5).into()); // position on \n
84+ // assert_eq!(coords_at_pos(slice, 6), (1, 0).into()); // position on w
85+ // assert_eq!(coords_at_pos(slice, 7), (1, 1).into()); // position on o
86+ // assert_eq!(coords_at_pos(slice, 10), (1, 4).into()); // position on d
87+
88+ // test with grapheme clusters
89+ let text = Rope::from("a̐éö̲\r\n");
90+ let slice = text.slice(..);
91+ assert_eq!(coords_at_pos(slice, 0), (0, 0).into());
92+ assert_eq!(coords_at_pos(slice, 2), (0, 1).into());
93+ assert_eq!(coords_at_pos(slice, 4), (0, 2).into());
94+ assert_eq!(coords_at_pos(slice, 7), (0, 3).into());
95+
96+ let text = Rope::from("किमपि");
97+ let slice = text.slice(..);
98+ assert_eq!(coords_at_pos(slice, 0), (0, 0).into());
99+ assert_eq!(coords_at_pos(slice, 2), (0, 1).into());
100+ assert_eq!(coords_at_pos(slice, 3), (0, 2).into());
101+ assert_eq!(coords_at_pos(slice, 5), (0, 3).into());
102+ }
103+
104+ #[test]
105+ fn test_pos_at_coords() {
106+ let text = Rope::from("ḧëḷḷö\nẅöṛḷḋ");
107+ let slice = text.slice(..);
108+ assert_eq!(pos_at_coords(slice, (0, 0).into()), 0);
109+ assert_eq!(pos_at_coords(slice, (0, 5).into()), 5); // position on \n
110+ assert_eq!(pos_at_coords(slice, (1, 0).into()), 6); // position on w
111+ assert_eq!(pos_at_coords(slice, (1, 1).into()), 7); // position on o
112+ assert_eq!(pos_at_coords(slice, (1, 4).into()), 10); // position on d
113+
114+ // test with grapheme clusters
115+ let text = Rope::from("a̐éö̲\r\n");
116+ let slice = text.slice(..);
117+ assert_eq!(pos_at_coords(slice, (0, 0).into()), 0);
118+ assert_eq!(pos_at_coords(slice, (0, 1).into()), 2);
119+ assert_eq!(pos_at_coords(slice, (0, 2).into()), 4);
120+ assert_eq!(pos_at_coords(slice, (0, 3).into()), 7); // \r\n is one char here
121+ assert_eq!(pos_at_coords(slice, (0, 4).into()), 9);
122+ let text = Rope::from("किमपि");
123+ // 2 - 1 - 2 codepoints
124+ // TODO: delete handling as per https://news.ycombinator.com/item?id=20058454
125+ let slice = text.slice(..);
126+ assert_eq!(pos_at_coords(slice, (0, 0).into()), 0);
127+ assert_eq!(pos_at_coords(slice, (0, 1).into()), 2);
128+ assert_eq!(pos_at_coords(slice, (0, 2).into()), 3);
129+ assert_eq!(pos_at_coords(slice, (0, 3).into()), 5); // eol
130+ }
57131}
helix-core/src/state.rs+1−221
@@ -1,7 +1,5 @@
11use crate::graphemes::{nth_next_grapheme_boundary, nth_prev_grapheme_boundary, RopeGraphemes};
2−use crate::syntax::LOADER;
3−use crate::{ChangeSet, Diagnostic, Position, Range, Rope, RopeSlice, Selection, Syntax};
4−use anyhow::Error;
2+use crate::{coords_at_pos, pos_at_coords, ChangeSet, Position, Range, Rope, RopeSlice, Selection};
53
64/// A state represents the current editor state of a single buffer.
75#[derive(Clone)]
@@ -83,96 +81,6 @@ impl State {
8381 Range::new(if extend { range.anchor } else { pos }, pos)
8482 }
8583
86− pub fn move_next_word_start(slice: RopeSlice, mut pos: usize, count: usize) -> usize {
87− for _ in 0..count {
88− if pos + 1 == slice.len_chars() {
89− return pos;
90− }
91−
92− let mut ch = slice.char(pos);
93− let next = slice.char(pos + 1);
94−
95− // if we're at the end of a word, or on whitespce right before new one
96− if categorize(ch) != categorize(next) {
97− pos += 1;
98− ch = next;
99− }
100−
101− if is_word(ch) {
102− skip_over_next(slice, &mut pos, is_word);
103− } else if ch.is_ascii_punctuation() {
104− skip_over_next(slice, &mut pos, |ch| ch.is_ascii_punctuation());
105− }
106−
107− // TODO: don't include newline?
108− skip_over_next(slice, &mut pos, |ch| ch.is_ascii_whitespace());
109− }
110−
111− pos
112− }
113−
114− pub fn move_prev_word_start(slice: RopeSlice, mut pos: usize, count: usize) -> usize {
115− for _ in 0..count {
116− if pos == 0 {
117− return pos;
118− }
119−
120− let ch = slice.char(pos);
121− let prev = slice.char(pos - 1);
122−
123− if categorize(ch) != categorize(prev) {
124− pos -= 1;
125− }
126−
127− // TODO: skip while eol
128−
129− // TODO: don't include newline?
130− skip_over_prev(slice, &mut pos, |ch| ch.is_ascii_whitespace());
131−
132− // refetch
133− let ch = slice.char(pos);
134−
135− if is_word(ch) {
136− skip_over_prev(slice, &mut pos, is_word);
137− } else if ch.is_ascii_punctuation() {
138− skip_over_prev(slice, &mut pos, |ch| ch.is_ascii_punctuation());
139− }
140− pos = pos.saturating_add(1)
141− }
142−
143− pos
144− }
145−
146− pub fn move_next_word_end(slice: RopeSlice, mut pos: usize, count: usize) -> usize {
147− for _ in 0..count {
148− if pos + 1 == slice.len_chars() {
149− return pos;
150− }
151−
152− let ch = slice.char(pos);
153− let next = slice.char(pos + 1);
154−
155− if categorize(ch) != categorize(next) {
156− pos += 1;
157− }
158−
159− // TODO: don't include newline?
160− skip_over_next(slice, &mut pos, |ch| ch.is_ascii_whitespace());
161−
162− // refetch
163− let ch = slice.char(pos);
164−
165− if is_word(ch) {
166− skip_over_next(slice, &mut pos, is_word);
167− } else if ch.is_ascii_punctuation() {
168− skip_over_next(slice, &mut pos, |ch| ch.is_ascii_punctuation());
169− }
170− pos -= 1
171− }
172−
173− pos
174− }
175−
17684 pub fn move_selection(
17785 &self,
17886 dir: Direction,
@@ -194,22 +102,6 @@ impl State {
194102 }
195103}
196104
197−/// Convert a character index to (line, column) coordinates.
198−pub fn coords_at_pos(text: RopeSlice, pos: usize) -> Position {
199− let line = text.char_to_line(pos);
200− let line_start = text.line_to_char(line);
201− let col = RopeGraphemes::new(text.slice(line_start..pos)).count();
202− Position::new(line, col)
203−}
204−
205−/// Convert (line, column) coordinates to a character index.
206−pub fn pos_at_coords(text: RopeSlice, coords: Position) -> usize {
207− let Position { row, col } = coords;
208− let line_start = text.line_to_char(row);
209− // line_start + col
210− nth_next_grapheme_boundary(text, line_start, col)
211−}
212−
213105fn move_vertically(
214106 text: RopeSlice,
215107 dir: Direction,
@@ -238,122 +130,10 @@ fn move_vertically(
238130 range
239131}
240132
241−// used for by-word movement
242−
243−fn is_word(ch: char) -> bool {
244− ch.is_alphanumeric() || ch == '_'
245−}
246−
247−#[derive(Debug, Eq, PartialEq)]
248−enum Category {
249− Whitespace,
250− Eol,
251− Word,
252− Punctuation,
253−}
254−fn categorize(ch: char) -> Category {
255− if ch == '\n' {
256− Category::Eol
257− } else if ch.is_ascii_whitespace() {
258− Category::Whitespace
259− } else if ch.is_ascii_punctuation() {
260− Category::Punctuation
261− } else if ch.is_ascii_alphanumeric() {
262− Category::Word
263− } else {
264− unreachable!()
265− }
266−}
267−
268−#[inline]
269−pub fn skip_over_next<F>(slice: RopeSlice, pos: &mut usize, fun: F)
270−where
271− F: Fn(char) -> bool,
272−{
273− let mut chars = slice.chars_at(*pos);
274−
275− for ch in chars {
276− if !fun(ch) {
277− break;
278− }
279− *pos += 1;
280− }
281−}
282−
283−#[inline]
284−pub fn skip_over_prev<F>(slice: RopeSlice, pos: &mut usize, fun: F)
285−where
286− F: Fn(char) -> bool,
287−{
288− // need to +1 so that prev() includes current char
289− let mut chars = slice.chars_at(*pos + 1);
290−
291− while let Some(ch) = chars.prev() {
292− if !fun(ch) {
293− break;
294− }
295− *pos = pos.saturating_sub(1);
296− }
297−}
298−
299133#[cfg(test)]
300134mod test {
301135 use super::*;
302136
303− #[test]
304− fn test_coords_at_pos() {
305− let text = Rope::from("ḧëḷḷö\nẅöṛḷḋ");
306− let slice = text.slice(..);
307− // assert_eq!(coords_at_pos(slice, 0), (0, 0).into());
308− // assert_eq!(coords_at_pos(slice, 5), (0, 5).into()); // position on \n
309− // assert_eq!(coords_at_pos(slice, 6), (1, 0).into()); // position on w
310− // assert_eq!(coords_at_pos(slice, 7), (1, 1).into()); // position on o
311− // assert_eq!(coords_at_pos(slice, 10), (1, 4).into()); // position on d
312−
313− // test with grapheme clusters
314− let text = Rope::from("a̐éö̲\r\n");
315− let slice = text.slice(..);
316− assert_eq!(coords_at_pos(slice, 0), (0, 0).into());
317− assert_eq!(coords_at_pos(slice, 2), (0, 1).into());
318− assert_eq!(coords_at_pos(slice, 4), (0, 2).into());
319− assert_eq!(coords_at_pos(slice, 7), (0, 3).into());
320−
321− let text = Rope::from("किमपि");
322− let slice = text.slice(..);
323− assert_eq!(coords_at_pos(slice, 0), (0, 0).into());
324− assert_eq!(coords_at_pos(slice, 2), (0, 1).into());
325− assert_eq!(coords_at_pos(slice, 3), (0, 2).into());
326− assert_eq!(coords_at_pos(slice, 5), (0, 3).into());
327− }
328−
329− #[test]
330− fn test_pos_at_coords() {
331− let text = Rope::from("ḧëḷḷö\nẅöṛḷḋ");
332− let slice = text.slice(..);
333− assert_eq!(pos_at_coords(slice, (0, 0).into()), 0);
334− assert_eq!(pos_at_coords(slice, (0, 5).into()), 5); // position on \n
335− assert_eq!(pos_at_coords(slice, (1, 0).into()), 6); // position on w
336− assert_eq!(pos_at_coords(slice, (1, 1).into()), 7); // position on o
337− assert_eq!(pos_at_coords(slice, (1, 4).into()), 10); // position on d
338−
339− // test with grapheme clusters
340− let text = Rope::from("a̐éö̲\r\n");
341− let slice = text.slice(..);
342− assert_eq!(pos_at_coords(slice, (0, 0).into()), 0);
343− assert_eq!(pos_at_coords(slice, (0, 1).into()), 2);
344− assert_eq!(pos_at_coords(slice, (0, 2).into()), 4);
345− assert_eq!(pos_at_coords(slice, (0, 3).into()), 7); // \r\n is one char here
346− assert_eq!(pos_at_coords(slice, (0, 4).into()), 9);
347− let text = Rope::from("किमपि");
348− // 2 - 1 - 2 codepoints
349− // TODO: delete handling as per https://news.ycombinator.com/item?id=20058454
350− let slice = text.slice(..);
351− assert_eq!(pos_at_coords(slice, (0, 0).into()), 0);
352− assert_eq!(pos_at_coords(slice, (0, 1).into()), 2);
353− assert_eq!(pos_at_coords(slice, (0, 2).into()), 3);
354− assert_eq!(pos_at_coords(slice, (0, 3).into()), 5); // eol
355− }
356−
357137 #[test]
358138 fn test_vertical_move() {
359139 let text = Rope::from("abcd\nefg\nwrs");
helix-core/src/syntax.rs+1−1
@@ -355,7 +355,7 @@ pub struct LanguageLayer {
355355 pub(crate) tree: Option<Tree>,
356356}
357357
358−use crate::state::coords_at_pos;
358+use crate::coords_at_pos;
359359use crate::transaction::{ChangeSet, Operation};
360360use crate::Tendril;
361361
helix-term/src/commands.rs+10−10
@@ -1,10 +1,10 @@
11use helix_core::{
2− comment, graphemes,
2+ comment, coords_at_pos, graphemes,
33 indent::TAB_WIDTH,
4− object,
4+ movement, object, pos_at_coords,
55 regex::{self, Regex},
66 register, search, selection,
7− state::{coords_at_pos, pos_at_coords, Direction, Granularity, State},
7+ state::{Direction, Granularity, State},
88 Change, ChangeSet, Position, Range, RopeSlice, Selection, Tendril, Transaction,
99};
1010
@@ -152,7 +152,7 @@ pub fn move_next_word_start(cx: &mut Context) {
152152 let text = doc.text().slice(..);
153153
154154 let selection = doc.selection().transform(|range| {
155− let pos = State::move_next_word_start(text, range.head, count);
155+ let pos = movement::move_next_word_start(text, range.head, count);
156156 Range::new(pos, pos)
157157 });
158158
@@ -165,7 +165,7 @@ pub fn move_prev_word_start(cx: &mut Context) {
165165 let text = doc.text().slice(..);
166166
167167 let selection = doc.selection().transform(|range| {
168− let pos = State::move_prev_word_start(text, range.head, count);
168+ let pos = movement::move_prev_word_start(text, range.head, count);
169169 Range::new(pos, pos)
170170 });
171171
@@ -178,7 +178,7 @@ pub fn move_next_word_end(cx: &mut Context) {
178178 let text = doc.text().slice(..);
179179
180180 let selection = doc.selection().transform(|range| {
181− let pos = State::move_next_word_end(text, range.head, count);
181+ let pos = movement::move_next_word_end(text, range.head, count);
182182 Range::new(pos, pos)
183183 });
184184
@@ -207,7 +207,7 @@ pub fn extend_next_word_start(cx: &mut Context) {
207207 let text = doc.text().slice(..);
208208
209209 let selection = doc.selection().transform(|mut range| {
210− let pos = State::move_next_word_start(text, range.head, count);
210+ let pos = movement::move_next_word_start(text, range.head, count);
211211 Range::new(range.anchor, pos)
212212 });
213213
@@ -220,7 +220,7 @@ pub fn extend_prev_word_start(cx: &mut Context) {
220220 let text = doc.text().slice(..);
221221
222222 let selection = doc.selection().transform(|mut range| {
223− let pos = State::move_prev_word_start(text, range.head, count);
223+ let pos = movement::move_prev_word_start(text, range.head, count);
224224 Range::new(range.anchor, pos)
225225 });
226226 doc.set_selection(selection);
@@ -232,7 +232,7 @@ pub fn extend_next_word_end(cx: &mut Context) {
232232 let text = doc.text().slice(..);
233233
234234 let selection = doc.selection().transform(|mut range| {
235− let pos = State::move_next_word_end(text, range.head, count);
235+ let pos = movement::move_next_word_end(text, range.head, count);
236236 Range::new(range.anchor, pos)
237237 });
238238
@@ -1199,7 +1199,7 @@ pub fn format_selections(cx: &mut Context) {
11991199}
12001200
12011201pub fn join_selections(cx: &mut Context) {
1202− use helix_core::state::skip_over_next;
1202+ use movement::skip_over_next;
12031203 let doc = cx.doc();
12041204 let text = doc.text();
12051205 let slice = doc.text().slice(..);