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",
|
"serde_json",
|
||||||
"tokio",
|
"tokio",
|
||||||
"toml",
|
"toml",
|
||||||
|
"unicode-segmentation",
|
||||||
|
"unicode-width",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
@ -19,9 +19,9 @@ helix-syntax = { version = "0.2", path = "../helix-syntax" }
|
||||||
ropey = "1.3"
|
ropey = "1.3"
|
||||||
smallvec = "1.4"
|
smallvec = "1.4"
|
||||||
tendril = "0.4.2"
|
tendril = "0.4.2"
|
||||||
unicode-segmentation = "1.7.1"
|
unicode-segmentation = "1.7"
|
||||||
unicode-width = "0.1"
|
unicode-width = "0.1"
|
||||||
unicode-general-category = "0.4.0"
|
unicode-general-category = "0.4"
|
||||||
# slab = "0.4.2"
|
# slab = "0.4.2"
|
||||||
tree-sitter = "0.19"
|
tree-sitter = "0.19"
|
||||||
once_cell = "1.8"
|
once_cell = "1.8"
|
||||||
|
|
|
@ -54,3 +54,6 @@ toml = "0.5"
|
||||||
|
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
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 std::{borrow::Cow, ops::RangeFrom};
|
||||||
use tui::terminal::CursorKind;
|
use tui::terminal::CursorKind;
|
||||||
|
|
||||||
|
use unicode_segmentation::{GraphemeCursor, GraphemeIncomplete};
|
||||||
|
use unicode_width::UnicodeWidthStr;
|
||||||
|
|
||||||
pub type Completion = (RangeFrom<usize>, Cow<'static, str>);
|
pub type Completion = (RangeFrom<usize>, Cow<'static, str>);
|
||||||
|
|
||||||
pub struct Prompt {
|
pub struct Prompt {
|
||||||
|
@ -34,6 +37,17 @@ pub enum CompletionDirection {
|
||||||
Backward,
|
Backward,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub enum Movement {
|
||||||
|
BackwardChar(usize),
|
||||||
|
BackwardWord(usize),
|
||||||
|
ForwardChar(usize),
|
||||||
|
ForwardWord(usize),
|
||||||
|
StartOfLine,
|
||||||
|
EndOfLine,
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
impl Prompt {
|
impl Prompt {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
prompt: String,
|
prompt: String,
|
||||||
|
@ -52,30 +66,125 @@ impl Prompt {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn insert_char(&mut self, c: char) {
|
/// Compute the cursor position after applying movement
|
||||||
let pos = if self.line.is_empty() {
|
/// Taken from: https://github.com/wez/wezterm/blob/e0b62d07ca9bf8ce69a61e30a3c20e7abc48ce7e/termwiz/src/lineedit/mod.rs#L516-L611
|
||||||
0
|
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 {
|
} else {
|
||||||
self.line
|
break;
|
||||||
.char_indices()
|
}
|
||||||
.nth(self.cursor)
|
}
|
||||||
.map(|(pos, _)| pos)
|
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())
|
.unwrap_or_else(|| self.line.len())
|
||||||
};
|
}
|
||||||
self.line.insert(pos, c);
|
Movement::ForwardChar(rep) => {
|
||||||
self.cursor += 1;
|
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.completion = (self.completion_fn)(&self.line);
|
||||||
self.exit_selection();
|
self.exit_selection();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn move_char_left(&mut self) {
|
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) {
|
pub fn move_char_right(&mut self) {
|
||||||
if self.cursor < self.line.len() {
|
let pos = self.eval_movement(Movement::ForwardChar(1));
|
||||||
self.cursor += 1;
|
self.cursor = pos;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn move_start(&mut self) {
|
pub fn move_start(&mut self) {
|
||||||
|
@ -87,39 +196,21 @@ impl Prompt {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete_char_backwards(&mut self) {
|
pub fn delete_char_backwards(&mut self) {
|
||||||
if self.cursor > 0 {
|
let pos = self.eval_movement(Movement::BackwardChar(1));
|
||||||
let pos = self
|
self.line.replace_range(pos..self.cursor, "");
|
||||||
.line
|
self.cursor = pos;
|
||||||
.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);
|
|
||||||
}
|
|
||||||
self.exit_selection();
|
self.exit_selection();
|
||||||
|
self.completion = (self.completion_fn)(&self.line);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete_word_backwards(&mut self) {
|
pub fn delete_word_backwards(&mut self) {
|
||||||
use helix_core::get_general_category;
|
let pos = self.eval_movement(Movement::BackwardWord(1));
|
||||||
let mut chars = self.line.char_indices().rev();
|
self.line.replace_range(pos..self.cursor, "");
|
||||||
// TODO add skipping whitespace logic here
|
self.cursor = pos;
|
||||||
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);
|
|
||||||
self.exit_selection();
|
self.exit_selection();
|
||||||
|
self.completion = (self.completion_fn)(&self.line);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn clear(&mut self) {
|
pub fn clear(&mut self) {
|
||||||
|
@ -363,7 +454,9 @@ impl Component for Prompt {
|
||||||
(
|
(
|
||||||
Some(Position::new(
|
Some(Position::new(
|
||||||
area.y as usize + line,
|
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,
|
CursorKind::Block,
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in New Issue