on master help sign in
Fix Unicode (#135)

* init * wip * wip * fix unicode break * fix unicode break * Update helix-core/src/transaction.rs Co-authored-by: Benoît Cortier <benoit.cortier@fried-world.eu> * clippy * fix * add changes * added test * wip * wip * wip * wip * fix * fix view * fix #88 Co-authored-by: Benoît Cortier <benoit.cortier@fried-world.eu>

commitb873fb9897bb5b24a60cca3d9fa69285446a857f authorKirawi <67773714+kirawi@users.noreply.github.com> dateTue 8 Jun 2021, 00:20 (5 years ago) parent8f1eb7b
7 files changed+94−26
Cargo.lock+7 helix-core/Cargo.toml+1 helix-core/src/movement.rs+72−13 helix-core/src/transaction.rs+1−1 helix-core/src/words.rs+7−7 helix-term/src/commands.rs+5−4 helix-view/src/view.rs+1−1
Cargo.lock+7
@@ -265,6 +265,7 @@ dependencies = [
265265 "tendril",
266266 "toml",
267267 "tree-sitter",
268+ "unicode-general-category",
268269 "unicode-segmentation",
269270 "unicode-width",
270271]
@@ -969,6 +970,12 @@ dependencies = [
969970 "matches",
970971]
971972
973+[[package]]
974+name = "unicode-general-category"
975+version = "0.4.0"
976+source = "registry+https://github.com/rust-lang/crates.io-index"
977+checksum = "07547e3ee45e28326cc23faac56d44f58f16ab23e413db526debce3b0bfd2742"
978+
972979[[package]]
973980name = "unicode-normalization"
974981version = "0.1.19"
helix-core/Cargo.toml+1
@@ -19,6 +19,7 @@ smallvec = "1.4"
1919tendril = "0.4.2"
2020unicode-segmentation = "1.6"
2121unicode-width = "0.1"
22+unicode-general-category = "0.4.0"
2223# slab = "0.4.2"
2324tree-sitter = "0.19"
2425once_cell = "1.4"
helix-core/src/movement.rs+72−13
@@ -88,11 +88,11 @@ pub fn move_next_word_start(slice: RopeSlice, mut begin: usize, count: usize) ->
8888
8989 if is_word(ch) {
9090 skip_over_next(slice, &mut end, is_word);
91− } else if ch.is_ascii_punctuation() {
92− skip_over_next(slice, &mut end, |ch| ch.is_ascii_punctuation());
91+ } else if is_punctuation(ch) {
92+ skip_over_next(slice, &mut end, is_punctuation);
9393 }
9494
95− skip_over_next(slice, &mut end, is_horiz_blank);
95+ skip_over_next(slice, &mut end, char::is_whitespace);
9696 }
9797
9898 Some(Range::new(begin, end - 1))
@@ -119,15 +119,15 @@ pub fn move_prev_word_start(slice: RopeSlice, mut begin: usize, count: usize) ->
119119
120120 end = begin;
121121
122− with_end = skip_over_prev(slice, &mut end, is_horiz_blank);
122+ with_end = skip_over_prev(slice, &mut end, char::is_whitespace);
123123
124124 // refetch
125125 let ch = slice.char(end);
126126
127127 if is_word(ch) {
128128 with_end = skip_over_prev(slice, &mut end, is_word);
129− } else if ch.is_ascii_punctuation() {
130− with_end = skip_over_prev(slice, &mut end, |ch| ch.is_ascii_punctuation());
129+ } else if is_punctuation(ch) {
130+ with_end = skip_over_prev(slice, &mut end, is_punctuation);
131131 }
132132 }
133133
@@ -155,15 +155,15 @@ pub fn move_next_word_end(slice: RopeSlice, mut begin: usize, count: usize) -> O
155155
156156 end = begin;
157157
158− skip_over_next(slice, &mut end, is_horiz_blank);
158+ skip_over_next(slice, &mut end, char::is_whitespace);
159159
160160 // refetch
161161 let ch = slice.char(end);
162162
163163 if is_word(ch) {
164164 skip_over_next(slice, &mut end, is_word);
165− } else if ch.is_ascii_punctuation() {
166− skip_over_next(slice, &mut end, |ch| ch.is_ascii_punctuation());
165+ } else if is_punctuation(ch) {
166+ skip_over_next(slice, &mut end, is_punctuation);
167167 }
168168 }
169169
@@ -174,12 +174,28 @@ pub fn move_next_word_end(slice: RopeSlice, mut begin: usize, count: usize) -> O
174174
175175// used for by-word movement
176176
177+#[inline]
177178pub(crate) fn is_word(ch: char) -> bool {
178179 ch.is_alphanumeric() || ch == '_'
179180}
180181
181−pub(crate) fn is_horiz_blank(ch: char) -> bool {
182− matches!(ch, ' ' | '\t')
182+#[inline]
183+pub(crate) fn is_punctuation(ch: char) -> bool {
184+ use unicode_general_category::{get_general_category, GeneralCategory};
185+
186+ matches!(
187+ get_general_category(ch),
188+ GeneralCategory::OtherPunctuation
189+ | GeneralCategory::OpenPunctuation
190+ | GeneralCategory::ClosePunctuation
191+ | GeneralCategory::InitialPunctuation
192+ | GeneralCategory::FinalPunctuation
193+ | GeneralCategory::ConnectorPunctuation
194+ | GeneralCategory::DashPunctuation
195+ | GeneralCategory::MathSymbol
196+ | GeneralCategory::CurrencySymbol
197+ | GeneralCategory::ModifierSymbol
198+ )
183199}
184200
185201#[derive(Debug, Eq, PartialEq)]
@@ -191,14 +207,15 @@ pub(crate) enum Category {
191207 Unknown,
192208}
193209
210+#[inline]
194211pub(crate) fn categorize(ch: char) -> Category {
195212 if ch == '\n' {
196213 Category::Eol
197− } else if ch.is_ascii_whitespace() {
214+ } else if ch.is_whitespace() {
198215 Category::Whitespace
199216 } else if is_word(ch) {
200217 Category::Word
201− } else if ch.is_ascii_punctuation() {
218+ } else if is_punctuation(ch) {
202219 Category::Punctuation
203220 } else {
204221 Category::Unknown
@@ -213,6 +230,7 @@ where
213230{
214231 let mut chars = slice.chars_at(*pos);
215232
233+ #[allow(clippy::while_let_on_iterator)]
216234 while let Some(ch) = chars.next() {
217235 if !fun(ch) {
218236 break;
@@ -231,6 +249,7 @@ where
231249 // need to +1 so that prev() includes current char
232250 let mut chars = slice.chars_at(*pos + 1);
233251
252+ #[allow(clippy::while_let_on_iterator)]
234253 while let Some(ch) = chars.prev() {
235254 if !fun(ch) {
236255 break;
@@ -259,4 +278,44 @@ mod test {
259278 (1, 2).into()
260279 );
261280 }
281+
282+ #[test]
283+ fn test_categorize() {
284+ const WORD_TEST_CASE: &'static str =
285+ "_hello_world_あいうえおー12345678901234567890";
286+ const PUNCTUATION_TEST_CASE: &'static str = "!\"#$%&\'()*+,-./:;<=>?@[\\]^`{|}~!”#$%&’()*+、。:;<=>?@「」^`{|}~";
287+ const WHITESPACE_TEST_CASE: &'static str = "      ";
288+
289+ assert_eq!(Category::Eol, categorize('\n'));
290+
291+ for ch in WHITESPACE_TEST_CASE.chars() {
292+ assert_eq!(
293+ Category::Whitespace,
294+ categorize(ch),
295+ "Testing '{}', but got `{:?}` instead of `Category::Whitespace`",
296+ ch,
297+ categorize(ch)
298+ );
299+ }
300+
301+ for ch in WORD_TEST_CASE.chars() {
302+ assert_eq!(
303+ Category::Word,
304+ categorize(ch),
305+ "Testing '{}', but got `{:?}` instead of `Category::Word`",
306+ ch,
307+ categorize(ch)
308+ );
309+ }
310+
311+ for ch in PUNCTUATION_TEST_CASE.chars() {
312+ assert_eq!(
313+ Category::Punctuation,
314+ categorize(ch),
315+ "Testing '{}', but got `{:?}` instead of `Category::Punctuation`",
316+ ch,
317+ categorize(ch)
318+ );
319+ }
320+ }
262321}
helix-core/src/transaction.rs+1−1
@@ -758,7 +758,7 @@ mod test {
758758
759759 #[test]
760760 fn combine_with_utf8() {
761− const TEST_CASE: &'static str = "Hello, これはヒレクスエディターです!";
761+ const TEST_CASE: &'static str = "Hello, これはヘリックスエディターです!";
762762
763763 let empty = Rope::from("");
764764 let mut a = ChangeSet::new(&empty);
helix-core/src/words.rs+7−7
@@ -1,4 +1,4 @@
1−use crate::movement::{categorize, is_horiz_blank, is_word, skip_over_prev};
1+use crate::movement::{categorize, is_punctuation, is_word, skip_over_prev};
22use ropey::RopeSlice;
33
44#[must_use]
@@ -13,15 +13,15 @@ pub fn nth_prev_word_boundary(slice: RopeSlice, mut char_idx: usize, count: usiz
1313 // return if not skip while?
1414 skip_over_prev(slice, &mut char_idx, |ch| ch == '\n');
1515
16− with_end = skip_over_prev(slice, &mut char_idx, is_horiz_blank);
16+ with_end = skip_over_prev(slice, &mut char_idx, char::is_whitespace);
1717
1818 // refetch
1919 let ch = slice.char(char_idx);
2020
2121 if is_word(ch) {
2222 with_end = skip_over_prev(slice, &mut char_idx, is_word);
23− } else if ch.is_ascii_punctuation() {
24− with_end = skip_over_prev(slice, &mut char_idx, |ch| ch.is_ascii_punctuation());
23+ } else if is_punctuation(ch) {
24+ with_end = skip_over_prev(slice, &mut char_idx, is_punctuation);
2525 }
2626 }
2727
@@ -47,11 +47,11 @@ fn different_prev_word_boundary() {
4747 t("hello, world", "hello, ");
4848 t("hello, ", "hello");
4949 t("hello", "");
50− t("こんにちは、世界!", "こんにちは、世界!"); // TODO: punctuation
50+ t("こんにちは、世界!", "こんにちは、世界");
5151 t("こんにちは、世界", "こんにちは、");
52− t("こんにちは、", "こんにちは、"); // what?
52+ t("こんにちは、", "こんにちは");
5353 t("こんにちは", "");
54− t("この世界。", "この世界。"); // what?
54+ t("この世界。", "この世界");
5555 t("この世界", "");
5656 t("お前はもう死んでいる", "");
5757 t("その300円です", ""); // TODO: should stop at 300
helix-term/src/commands.rs+5−4
@@ -654,9 +654,10 @@ pub fn split_selection_on_newline(cx: &mut Context) {
654654fn _search(doc: &mut Document, view: &mut View, contents: &str, regex: &Regex, extend: bool) {
655655 let text = doc.text();
656656 let selection = doc.selection(view.id);
657− let start = selection.cursor();
657+ let start = text.char_to_byte(selection.cursor());
658658
659659 // use find_at to find the next match after the cursor, loop around the end
660+ // Careful, `Regex` uses `bytes` as offsets, not character indices!
660661 let mat = regex
661662 .find_at(contents, start)
662663 .or_else(|| regex.find(contents));
@@ -670,7 +671,7 @@ fn _search(doc: &mut Document, view: &mut View, contents: &str, regex: &Regex, e
670671 return;
671672 }
672673
673− let head = end;
674+ let head = end - 1;
674675
675676 let selection = if extend {
676677 selection.clone().push(Range::new(start, head))
@@ -1027,7 +1028,7 @@ pub fn command_mode(cx: &mut Context) {
10271028 let mut prompt = Prompt::new(
10281029 ":".to_owned(),
10291030 |input: &str| {
1030− // we use .this over split_ascii_whitespace() because we care about empty segments
1031+ // we use .this over split_whitespace() because we care about empty segments
10311032 let parts = input.split(' ').collect::<Vec<&str>>();
10321033
10331034 // simple heuristic: if there's no just one part, complete command name.
@@ -1069,7 +1070,7 @@ pub fn command_mode(cx: &mut Context) {
10691070 return;
10701071 }
10711072
1072− let parts = input.split_ascii_whitespace().collect::<Vec<&str>>();
1073+ let parts = input.split_whitespace().collect::<Vec<&str>>();
10731074 if parts.is_empty() {
10741075 return;
10751076 }
helix-view/src/view.rs+1−1
@@ -106,7 +106,7 @@ impl View {
106106 /// Calculates the last visible line on screen
107107 #[inline]
108108 pub fn last_line(&self, doc: &Document) -> usize {
109− let height = self.area.height.saturating_sub(2); // - 2 for statusline
109+ let height = self.area.height.saturating_sub(1); // - 1 for statusline
110110 std::cmp::min(
111111 self.first_line + height as usize,
112112 doc.text().len_lines() - 1,