on master help sign in
ui: prompt: Better unicode support

We copied over eval_movement from wezterm, that already solves most of our problems. self.cursor is now byte-based.

commit9275021497fc13938317b681319ba571c8d5f478 authorBlaž Hrastnik <blaz@mxxn.io> dateFri 18 Jun 2021, 17:31 (5 years ago) parent59c59de
4 files changed+144−46
Cargo.lock+2 helix-core/Cargo.toml+2−2 helix-term/Cargo.toml+3 helix-term/src/ui/prompt.rs+137−44
Cargo.lock+2
@@ -329,6 +329,8 @@ dependencies = [
329329 "serde_json",
330330 "tokio",
331331 "toml",
332+ "unicode-segmentation",
333+ "unicode-width",
332334]
333335
334336[[package]]
helix-core/Cargo.toml+2−2
@@ -19,9 +19,9 @@ helix-syntax = { version = "0.2", path = "../helix-syntax" }
1919ropey = "1.3"
2020smallvec = "1.4"
2121tendril = "0.4.2"
22−unicode-segmentation = "1.7.1"
22+unicode-segmentation = "1.7"
2323unicode-width = "0.1"
24−unicode-general-category = "0.4.0"
24+unicode-general-category = "0.4"
2525# slab = "0.4.2"
2626tree-sitter = "0.19"
2727once_cell = "1.8"
helix-term/Cargo.toml+3
@@ -54,3 +54,6 @@ toml = "0.5"
5454
5555serde_json = "1.0"
5656serde = { version = "1.0", features = ["derive"] }
57+
58+unicode-segmentation = "1.7"
59+unicode-width = "0.1"
helix-term/src/ui/prompt.rs+137−44
@@ -6,6 +6,9 @@ use helix_view::{Editor, Theme};
66use std::{borrow::Cow, ops::RangeFrom};
77use tui::terminal::CursorKind;
88
9+use unicode_segmentation::{GraphemeCursor, GraphemeIncomplete};
10+use unicode_width::UnicodeWidthStr;
11+
912pub type Completion = (RangeFrom<usize>, Cow<'static, str>);
1013
1114pub struct Prompt {
@@ -34,6 +37,17 @@ pub enum CompletionDirection {
3437 Backward,
3538}
3639
40+#[derive(Debug, Clone, Copy)]
41+pub enum Movement {
42+ BackwardChar(usize),
43+ BackwardWord(usize),
44+ ForwardChar(usize),
45+ ForwardWord(usize),
46+ StartOfLine,
47+ EndOfLine,
48+ None,
49+}
50+
3751impl Prompt {
3852 pub fn new(
3953 prompt: String,
@@ -52,30 +66,125 @@ impl Prompt {
5266 }
5367 }
5468
69+ /// Compute the cursor position after applying movement
70+ /// Taken from: https://github.com/wez/wezterm/blob/e0b62d07ca9bf8ce69a61e30a3c20e7abc48ce7e/termwiz/src/lineedit/mod.rs#L516-L611
71+ fn eval_movement(&self, movement: Movement) -> usize {
72+ match movement {
73+ Movement::BackwardChar(rep) => {
74+ let mut position = self.cursor;
75+ for _ in 0..rep {
76+ let mut cursor = GraphemeCursor::new(position, self.line.len(), false);
77+ if let Ok(Some(pos)) = cursor.prev_boundary(&self.line, 0) {
78+ position = pos;
79+ } else {
80+ break;
81+ }
82+ }
83+ position
84+ }
85+ Movement::BackwardWord(rep) => {
86+ let char_indices: Vec<(usize, char)> = self.line.char_indices().collect();
87+ if char_indices.is_empty() {
88+ return self.cursor;
89+ }
90+ let mut char_position = char_indices
91+ .iter()
92+ .position(|(idx, _)| *idx == self.cursor)
93+ .unwrap_or(char_indices.len() - 1);
94+
95+ for _ in 0..rep {
96+ if char_position == 0 {
97+ break;
98+ }
99+
100+ let mut found = None;
101+ for prev in (0..char_position - 1).rev() {
102+ if char_indices[prev].1.is_whitespace() {
103+ found = Some(prev + 1);
104+ break;
105+ }
106+ }
107+
108+ char_position = found.unwrap_or(0);
109+ }
110+ char_indices[char_position].0
111+ }
112+ Movement::ForwardWord(rep) => {
113+ let char_indices: Vec<(usize, char)> = self.line.char_indices().collect();
114+ if char_indices.is_empty() {
115+ return self.cursor;
116+ }
117+ let mut char_position = char_indices
118+ .iter()
119+ .position(|(idx, _)| *idx == self.cursor)
120+ .unwrap_or_else(|| char_indices.len());
121+
122+ for _ in 0..rep {
123+ // Skip any non-whitespace characters
124+ while char_position < char_indices.len()
125+ && !char_indices[char_position].1.is_whitespace()
126+ {
127+ char_position += 1;
128+ }
129+
130+ // Skip any whitespace characters
131+ while char_position < char_indices.len()
132+ && char_indices[char_position].1.is_whitespace()
133+ {
134+ char_position += 1;
135+ }
136+
137+ // We are now on the start of the next word
138+ }
139+ char_indices
140+ .get(char_position)
141+ .map(|(i, _)| *i)
142+ .unwrap_or_else(|| self.line.len())
143+ }
144+ Movement::ForwardChar(rep) => {
145+ let mut position = self.cursor;
146+ for _ in 0..rep {
147+ let mut cursor = GraphemeCursor::new(position, self.line.len(), false);
148+ if let Ok(Some(pos)) = cursor.next_boundary(&self.line, 0) {
149+ position = pos;
150+ } else {
151+ break;
152+ }
153+ }
154+ position
155+ }
156+ Movement::StartOfLine => 0,
157+ Movement::EndOfLine => {
158+ let mut cursor =
159+ GraphemeCursor::new(self.line.len().saturating_sub(1), self.line.len(), false);
160+ if let Ok(Some(pos)) = cursor.next_boundary(&self.line, 0) {
161+ pos
162+ } else {
163+ self.cursor
164+ }
165+ }
166+ Movement::None => self.cursor,
167+ }
168+ }
169+
55170 pub fn insert_char(&mut self, c: char) {
56− let pos = if self.line.is_empty() {
57− 0
58− } else {
59− self.line
60− .char_indices()
61− .nth(self.cursor)
62− .map(|(pos, _)| pos)
63− .unwrap_or_else(|| self.line.len())
64− };
65− self.line.insert(pos, c);
66− self.cursor += 1;
171+ self.line.insert(self.cursor, c);
172+ let mut cursor = GraphemeCursor::new(self.cursor, self.line.len(), false);
173+ if let Ok(Some(pos)) = cursor.next_boundary(&self.line, 0) {
174+ self.cursor = pos;
175+ }
67176 self.completion = (self.completion_fn)(&self.line);
68177 self.exit_selection();
69178 }
70179
71180 pub fn move_char_left(&mut self) {
72− self.cursor = self.cursor.saturating_sub(1)
181+ let pos = self.eval_movement(Movement::BackwardChar(1));
182+ self.cursor = pos
73183 }
74184
75185 pub fn move_char_right(&mut self) {
76− if self.cursor < self.line.len() {
77− self.cursor += 1;
78− }
186+ let pos = self.eval_movement(Movement::ForwardChar(1));
187+ self.cursor = pos;
79188 }
80189
81190 pub fn move_start(&mut self) {
@@ -87,39 +196,21 @@ impl Prompt {
87196 }
88197
89198 pub fn delete_char_backwards(&mut self) {
90− if self.cursor > 0 {
91− let pos = self
92− .line
93− .char_indices()
94− .nth(self.cursor - 1)
95− .map(|(pos, _)| pos)
96− .expect("line is not empty");
97− self.line.remove(pos);
98− self.cursor -= 1;
99− self.completion = (self.completion_fn)(&self.line);
100− }
199+ let pos = self.eval_movement(Movement::BackwardChar(1));
200+ self.line.replace_range(pos..self.cursor, "");
201+ self.cursor = pos;
202+
101203 self.exit_selection();
204+ self.completion = (self.completion_fn)(&self.line);
102205 }
103206
104207 pub fn delete_word_backwards(&mut self) {
105− use helix_core::get_general_category;
106− let mut chars = self.line.char_indices().rev();
107− // TODO add skipping whitespace logic here
108− let (mut i, cat) = match chars.next() {
109− Some((i, c)) => (i, get_general_category(c)),
110− None => return,
111− };
112− self.cursor -= 1;
113− for (nn, nc) in chars {
114− if get_general_category(nc) != cat {
115− break;
116− }
117− i = nn;
118− self.cursor -= 1;
119− }
120− self.line.drain(i..);
121− self.completion = (self.completion_fn)(&self.line);
208+ let pos = self.eval_movement(Movement::BackwardWord(1));
209+ self.line.replace_range(pos..self.cursor, "");
210+ self.cursor = pos;
211+
122212 self.exit_selection();
213+ self.completion = (self.completion_fn)(&self.line);
123214 }
124215
125216 pub fn clear(&mut self) {
@@ -363,7 +454,9 @@ impl Component for Prompt {
363454 (
364455 Some(Position::new(
365456 area.y as usize + line,
366− area.x as usize + self.prompt.len() + self.cursor,
457+ area.x as usize
458+ + self.prompt.len()
459+ + UnicodeWidthStr::width(&self.line[..self.cursor]),
367460 )),
368461 CursorKind::Block,
369462 )