mirror of https://github.com/helix-editor/helix
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
parent
59c59deb46
commit
9275021497
|
@ -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 {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn insert_char(&mut self, c: char) {
|
||||
let pos = if self.line.is_empty() {
|
||||
0
|
||||
/// 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 {
|
||||
self.line
|
||||
.char_indices()
|
||||
.nth(self.cursor)
|
||||
.map(|(pos, _)| pos)
|
||||
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())
|
||||
};
|
||||
self.line.insert(pos, c);
|
||||
self.cursor += 1;
|
||||
}
|
||||
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) {
|
||||
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…
Reference in New Issue