@@ -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;
9+use unicode_segmentation::{GraphemeCursor, GraphemeIncomplete};
10+use unicode_width::UnicodeWidthStr;
9pub type Completion = (RangeFrom<usize>, Cow<'static, str>);
12pub type Completion = (RangeFrom<usize>, Cow<'static, str>);
11pub struct Prompt {
14pub struct Prompt {
@@ -34,6 +37,17 @@ pub enum CompletionDirection {
40+#[derive(Debug, Clone, Copy)]
37impl Prompt {
51impl Prompt {
38 pub fn new(
52 pub fn new(
39 prompt: String,
53 prompt: String,
@@ -52,30 +66,125 @@ impl Prompt {
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 {
73+ Movement::BackwardChar(rep) => {
74+ let mut position = self.cursor;
76+ let mut cursor = GraphemeCursor::new(position, self.line.len(), false);
77+ if let Ok(Some(pos)) = cursor.prev_boundary(&self.line, 0) {
85+ Movement::BackwardWord(rep) => {
86+ let char_indices: Vec<(usize, char)> = self.line.char_indices().collect();
87+ if char_indices.is_empty() {
90+ let mut char_position = char_indices
92+ .position(|(idx, _)| *idx == self.cursor)
93+ .unwrap_or(char_indices.len() - 1);
96+ if char_position == 0 {
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);
108+ char_position = found.unwrap_or(0);
110+ char_indices[char_position].0
112+ Movement::ForwardWord(rep) => {
113+ let char_indices: Vec<(usize, char)> = self.line.char_indices().collect();
114+ if char_indices.is_empty() {
117+ let mut char_position = char_indices
119+ .position(|(idx, _)| *idx == self.cursor)
120+ .unwrap_or_else(|| char_indices.len());
123+ // Skip any non-whitespace characters
124+ while char_position < char_indices.len()
125+ && !char_indices[char_position].1.is_whitespace()
130+ // Skip any whitespace characters
131+ while char_position < char_indices.len()
132+ && char_indices[char_position].1.is_whitespace()
137+ // We are now on the start of the next word
142+ .unwrap_or_else(|| self.line.len())
144+ Movement::ForwardChar(rep) => {
145+ let mut position = self.cursor;
147+ let mut cursor = GraphemeCursor::new(position, self.line.len(), false);
148+ if let Ok(Some(pos)) = cursor.next_boundary(&self.line, 0) {
156+ Movement::StartOfLine => 0,
157+ Movement::EndOfLine => {
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) {
166+ Movement::None => self.cursor,
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) {
61− .nth(self.cursor)
174+ self.cursor = pos;
63− .unwrap_or_else(|| self.line.len())
65− self.line.insert(pos, c);
67 self.completion = (self.completion_fn)(&self.line);
176 self.completion = (self.completion_fn)(&self.line);
68 self.exit_selection();
177 self.exit_selection();
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));
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;
81 pub fn move_start(&mut self) {
190 pub fn move_start(&mut self) {
@@ -87,39 +196,21 @@ impl Prompt {
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));
94− .nth(self.cursor - 1)
200+ self.line.replace_range(pos..self.cursor, "");
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+
101 self.exit_selection();
203 self.exit_selection();
204+ self.completion = (self.completion_fn)(&self.line);
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)),
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+
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);
125 pub fn clear(&mut self) {
216 pub fn clear(&mut self) {
@@ -363,7 +454,9 @@ impl Component for Prompt {
364 Some(Position::new(
455 Some(Position::new(
365 area.y as usize + line,
456 area.y as usize + line,
366− area.x as usize + self.prompt.len() + self.cursor,
458+ + self.prompt.len()
459+ + UnicodeWidthStr::width(&self.line[..self.cursor]),
368 CursorKind::Block,
461 CursorKind::Block,