on master help sign in
ui: prompt: Better unicode support
9275021Blaž Hrastnik
Cargo.lock+2
@@ -329,6 +329,8 @@ dependencies = [
329 "serde_json",
329 "serde_json",
330 "tokio",
330 "tokio",
331 "toml",
331 "toml",
332+ "unicode-segmentation",
333+ "unicode-width",
332]
334]
333
335
334[[package]]
336[[package]]
helix-core/Cargo.toml+2−2
@@ -19,9 +19,9 @@ helix-syntax = { version = "0.2", path = "../helix-syntax" }
19ropey = "1.3"
19ropey = "1.3"
20smallvec = "1.4"
20smallvec = "1.4"
21tendril = "0.4.2"
21tendril = "0.4.2"
22−unicode-segmentation = "1.7.1"
22+unicode-segmentation = "1.7"
23unicode-width = "0.1"
23unicode-width = "0.1"
24−unicode-general-category = "0.4.0"
24+unicode-general-category = "0.4"
25# slab = "0.4.2"
25# slab = "0.4.2"
26tree-sitter = "0.19"
26tree-sitter = "0.19"
27once_cell = "1.8"
27once_cell = "1.8"
helix-term/Cargo.toml+3
@@ -54,3 +54,6 @@ toml = "0.5"
54
54
55serde_json = "1.0"
55serde_json = "1.0"
56serde = { version = "1.0", features = ["derive"] }
56serde = { 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};
6use std::{borrow::Cow, ops::RangeFrom};
6use std::{borrow::Cow, ops::RangeFrom};
7use tui::terminal::CursorKind;
7use tui::terminal::CursorKind;
8
8
9+use unicode_segmentation::{GraphemeCursor, GraphemeIncomplete};
10+use unicode_width::UnicodeWidthStr;
11+
9pub type Completion = (RangeFrom<usize>, Cow<'static, str>);
12pub type Completion = (RangeFrom<usize>, Cow<'static, str>);
10
13
11pub struct Prompt {
14pub struct Prompt {
@@ -34,6 +37,17 @@ pub enum CompletionDirection {
34 Backward,
37 Backward,
35}
38}
36
39
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+
37impl Prompt {
51impl Prompt {
38 pub fn new(
52 pub fn new(
39 prompt: String,
53 prompt: String,
@@ -52,30 +66,125 @@ impl Prompt {
52 }
66 }
53 }
67 }
54
68
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+
55 pub fn insert_char(&mut self, c: char) {
170 pub fn insert_char(&mut self, c: char) {
56− let pos = if self.line.is_empty() {
171+ self.line.insert(self.cursor, c);
57− 0
172+ let mut cursor = GraphemeCursor::new(self.cursor, self.line.len(), false);
58− } else {
173+ if let Ok(Some(pos)) = cursor.next_boundary(&self.line, 0) {
59− self.line
60− .char_indices()
61− .nth(self.cursor)
174+ self.cursor = pos;
62− .map(|(pos, _)| pos)
63− .unwrap_or_else(|| self.line.len())
64− };
175+ }
65− self.line.insert(pos, c);
66− self.cursor += 1;
67 self.completion = (self.completion_fn)(&self.line);
176 self.completion = (self.completion_fn)(&self.line);
68 self.exit_selection();
177 self.exit_selection();
69 }
178 }
70
179
71 pub fn move_char_left(&mut self) {
180 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
73 }
183 }
74
184
75 pub fn move_char_right(&mut self) {
185 pub fn move_char_right(&mut self) {
76− if self.cursor < self.line.len() {
186+ let pos = self.eval_movement(Movement::ForwardChar(1));
77− self.cursor += 1;
187+ self.cursor = pos;
78− }
79 }
188 }
80
189
81 pub fn move_start(&mut self) {
190 pub fn move_start(&mut self) {
@@ -87,39 +196,21 @@ impl Prompt {
87 }
196 }
88
197
89 pub fn delete_char_backwards(&mut self) {
198 pub fn delete_char_backwards(&mut self) {
90− if self.cursor > 0 {
199+ let pos = self.eval_movement(Movement::BackwardChar(1));
91− let pos = self
92− .line
93− .char_indices()
94− .nth(self.cursor - 1)
200+ self.line.replace_range(pos..self.cursor, "");
95− .map(|(pos, _)| pos)
96− .expect("line is not empty");
97− self.line.remove(pos);
98− self.cursor -= 1;
201+ self.cursor = pos;
99− self.completion = (self.completion_fn)(&self.line);
202+
100− }
101 self.exit_selection();
203 self.exit_selection();
204+ self.completion = (self.completion_fn)(&self.line);
102 }
205 }
103
206
104 pub fn delete_word_backwards(&mut self) {
207 pub fn delete_word_backwards(&mut self) {
105− use helix_core::get_general_category;
208+ let pos = self.eval_movement(Movement::BackwardWord(1));
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;
209+ self.line.replace_range(pos..self.cursor, "");
113− for (nn, nc) in chars {
210+ self.cursor = pos;
114− if get_general_category(nc) != cat {
211+
115− break;
116− }
117− i = nn;
118− self.cursor -= 1;
119− }
120− self.line.drain(i..);
121− self.completion = (self.completion_fn)(&self.line);
122 self.exit_selection();
212 self.exit_selection();
213+ self.completion = (self.completion_fn)(&self.line);
123 }
214 }
124
215
125 pub fn clear(&mut self) {
216 pub fn clear(&mut self) {
@@ -363,7 +454,9 @@ impl Component for Prompt {
363 (
454 (
364 Some(Position::new(
455 Some(Position::new(
365 area.y as usize + line,
456 area.y as usize + line,
457+ area.x as usize
366− area.x as usize + self.prompt.len() + self.cursor,
458+ + self.prompt.len()
459+ + UnicodeWidthStr::width(&self.line[..self.cursor]),
367 )),
460 )),
368 CursorKind::Block,
461 CursorKind::Block,
369 )
462 )