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.
pull/328/head
Blaž Hrastnik 3 years ago
parent 59c59deb46
commit 9275021497

2
Cargo.lock generated

@ -329,6 +329,8 @@ dependencies = [
"serde_json",
"tokio",
"toml",
"unicode-segmentation",
"unicode-width",
]
[[package]]

@ -19,9 +19,9 @@ helix-syntax = { version = "0.2", path = "../helix-syntax" }
ropey = "1.3"
smallvec = "1.4"
tendril = "0.4.2"
unicode-segmentation = "1.7.1"
unicode-segmentation = "1.7"
unicode-width = "0.1"
unicode-general-category = "0.4.0"
unicode-general-category = "0.4"
# slab = "0.4.2"
tree-sitter = "0.19"
once_cell = "1.8"

@ -54,3 +54,6 @@ toml = "0.5"
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
unicode-segmentation = "1.7"
unicode-width = "0.1"

@ -6,6 +6,9 @@ use helix_view::{Editor, Theme};
use std::{borrow::Cow, ops::RangeFrom};
use tui::terminal::CursorKind;
use unicode_segmentation::{GraphemeCursor, GraphemeIncomplete};
use unicode_width::UnicodeWidthStr;
pub type Completion = (RangeFrom<usize>, Cow<'static, str>);
pub struct Prompt {
@ -34,6 +37,17 @@ pub enum CompletionDirection {
Backward,
}
#[derive(Debug, Clone, Copy)]
pub enum Movement {
BackwardChar(usize),
BackwardWord(usize),
ForwardChar(usize),
ForwardWord(usize),
StartOfLine,
EndOfLine,
None,
}
impl Prompt {
pub fn new(
prompt: String,
@ -52,30 +66,125 @@ impl Prompt {
}
}
/// Compute the cursor position after applying movement
/// Taken from: https://github.com/wez/wezterm/blob/e0b62d07ca9bf8ce69a61e30a3c20e7abc48ce7e/termwiz/src/lineedit/mod.rs#L516-L611
fn eval_movement(&self, movement: Movement) -> usize {
match movement {
Movement::BackwardChar(rep) => {
let mut position = self.cursor;
for _ in 0..rep {
let mut cursor = GraphemeCursor::new(position, self.line.len(), false);
if let Ok(Some(pos)) = cursor.prev_boundary(&self.line, 0) {
position = pos;
} else {
break;
}
}
position
}
Movement::BackwardWord(rep) => {
let char_indices: Vec<(usize, char)> = self.line.char_indices().collect();
if char_indices.is_empty() {
return self.cursor;
}
let mut char_position = char_indices
.iter()
.position(|(idx, _)| *idx == self.cursor)
.unwrap_or(char_indices.len() - 1);
for _ in 0..rep {
if char_position == 0 {
break;
}
let mut found = None;
for prev in (0..char_position - 1).rev() {
if char_indices[prev].1.is_whitespace() {
found = Some(prev + 1);
break;
}
}
char_position = found.unwrap_or(0);
}
char_indices[char_position].0
}
Movement::ForwardWord(rep) => {
let char_indices: Vec<(usize, char)> = self.line.char_indices().collect();
if char_indices.is_empty() {
return self.cursor;
}
let mut char_position = char_indices
.iter()
.position(|(idx, _)| *idx == self.cursor)
.unwrap_or_else(|| char_indices.len());
for _ in 0..rep {
// Skip any non-whitespace characters
while char_position < char_indices.len()
&& !char_indices[char_position].1.is_whitespace()
{
char_position += 1;
}
// Skip any whitespace characters
while char_position < char_indices.len()
&& char_indices[char_position].1.is_whitespace()
{
char_position += 1;
}
// We are now on the start of the next word
}
char_indices
.get(char_position)
.map(|(i, _)| *i)
.unwrap_or_else(|| self.line.len())
}
Movement::ForwardChar(rep) => {
let mut position = self.cursor;
for _ in 0..rep {
let mut cursor = GraphemeCursor::new(position, self.line.len(), false);
if let Ok(Some(pos)) = cursor.next_boundary(&self.line, 0) {
position = pos;
} else {
break;
}
}
position
}
Movement::StartOfLine => 0,
Movement::EndOfLine => {
let mut cursor =
GraphemeCursor::new(self.line.len().saturating_sub(1), self.line.len(), false);
if let Ok(Some(pos)) = cursor.next_boundary(&self.line, 0) {
pos
} else {
self.cursor
}
}
Movement::None => self.cursor,
}
}
pub fn insert_char(&mut self, c: char) {
let pos = if self.line.is_empty() {
0
} else {
self.line
.char_indices()
.nth(self.cursor)
.map(|(pos, _)| pos)
.unwrap_or_else(|| self.line.len())
};
self.line.insert(pos, c);
self.cursor += 1;
self.line.insert(self.cursor, c);
let mut cursor = GraphemeCursor::new(self.cursor, self.line.len(), false);
if let Ok(Some(pos)) = cursor.next_boundary(&self.line, 0) {
self.cursor = pos;
}
self.completion = (self.completion_fn)(&self.line);
self.exit_selection();
}
pub fn move_char_left(&mut self) {
self.cursor = self.cursor.saturating_sub(1)
let pos = self.eval_movement(Movement::BackwardChar(1));
self.cursor = pos
}
pub fn move_char_right(&mut self) {
if self.cursor < self.line.len() {
self.cursor += 1;
}
let pos = self.eval_movement(Movement::ForwardChar(1));
self.cursor = pos;
}
pub fn move_start(&mut self) {
@ -87,39 +196,21 @@ impl Prompt {
}
pub fn delete_char_backwards(&mut self) {
if self.cursor > 0 {
let pos = self
.line
.char_indices()
.nth(self.cursor - 1)
.map(|(pos, _)| pos)
.expect("line is not empty");
self.line.remove(pos);
self.cursor -= 1;
self.completion = (self.completion_fn)(&self.line);
}
let pos = self.eval_movement(Movement::BackwardChar(1));
self.line.replace_range(pos..self.cursor, "");
self.cursor = pos;
self.exit_selection();
self.completion = (self.completion_fn)(&self.line);
}
pub fn delete_word_backwards(&mut self) {
use helix_core::get_general_category;
let mut chars = self.line.char_indices().rev();
// TODO add skipping whitespace logic here
let (mut i, cat) = match chars.next() {
Some((i, c)) => (i, get_general_category(c)),
None => return,
};
self.cursor -= 1;
for (nn, nc) in chars {
if get_general_category(nc) != cat {
break;
}
i = nn;
self.cursor -= 1;
}
self.line.drain(i..);
self.completion = (self.completion_fn)(&self.line);
let pos = self.eval_movement(Movement::BackwardWord(1));
self.line.replace_range(pos..self.cursor, "");
self.cursor = pos;
self.exit_selection();
self.completion = (self.completion_fn)(&self.line);
}
pub fn clear(&mut self) {
@ -363,7 +454,9 @@ impl Component for Prompt {
(
Some(Position::new(
area.y as usize + line,
area.x as usize + self.prompt.len() + self.cursor,
area.x as usize
+ self.prompt.len()
+ UnicodeWidthStr::width(&self.line[..self.cursor]),
)),
CursorKind::Block,
)

Loading…
Cancel
Save