Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ui: prompt: Better unicode support #295

Merged
merged 3 commits into from
Jun 20, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions helix-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ helix-syntax = { path = "../helix-syntax" }
ropey = "1.2"
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"
Expand Down
8 changes: 6 additions & 2 deletions helix-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ mod state;
pub mod syntax;
mod transaction;

pub mod unicode {
pub use unicode_general_category as category;
pub use unicode_segmentation as segmentation;
pub use unicode_width as width;
}

static RUNTIME_DIR: once_cell::sync::Lazy<std::path::PathBuf> =
once_cell::sync::Lazy::new(runtime_dir);

Expand Down Expand Up @@ -97,8 +103,6 @@ pub use ropey::{Rope, RopeSlice};

pub use tendril::StrTendril as Tendril;

pub use unicode_general_category::get_general_category;

#[doc(inline)]
pub use {regex, tree_sitter};

Expand Down
236 changes: 187 additions & 49 deletions helix-term/src/ui/prompt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ use helix_view::{Editor, Theme};
use std::{borrow::Cow, ops::RangeFrom};
use tui::terminal::CursorKind;

use helix_core::{
unicode::segmentation::{GraphemeCursor, GraphemeIncomplete},
unicode::width::UnicodeWidthStr,
};

pub type Completion = (RangeFrom<usize>, Cow<'static, str>);

pub struct Prompt {
Expand Down Expand Up @@ -34,6 +39,17 @@ pub enum CompletionDirection {
Backward,
}

#[derive(Debug, Clone, Copy)]
pub enum Movement {
BackwardChar(usize),
BackwardWord(usize),
ForwardChar(usize),
ForwardWord(usize),
archseer marked this conversation as resolved.
Show resolved Hide resolved
Comment on lines +44 to +47
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we support repetition for prompt since we only need one. I think we might be able to remove the usize here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, I'll remove it

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah wezterm also always uses 1, I guess it was added since you could do vi-mode for readline in the future with it

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, but I don't think we will be doing that any time soon right? At least I think vi-mode won't be useful in parts like prompt where a modal editor is not useful.

StartOfLine,
EndOfLine,
None,
Copy link
Contributor

@pickfire pickfire Jun 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In what case do we need this None?

}

impl Prompt {
pub fn new(
prompt: String,
Expand All @@ -52,30 +68,120 @@ 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)
}

pub fn move_char_right(&mut self) {
if self.cursor < self.line.len() {
self.cursor += 1;
}
pub fn move_cursor(&mut self, movement: Movement) {
let pos = self.eval_movement(movement);
self.cursor = pos
}

pub fn move_start(&mut self) {
Expand All @@ -87,39 +193,29 @@ 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..);
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 kill_to_end_of_line(&mut self) {
let pos = self.eval_movement(Movement::EndOfLine);
self.line.replace_range(self.cursor..pos, "");

self.exit_selection();
self.completion = (self.completion_fn)(&self.line);
}

pub fn clear(&mut self) {
Expand Down Expand Up @@ -293,31 +389,71 @@ impl Component for Prompt {
(self.callback_fn)(cx.editor, &self.line, PromptEvent::Update);
}
KeyEvent {
code: KeyCode::Char('c'),
modifiers: KeyModifiers::CONTROL,
}
| KeyEvent {
code: KeyCode::Esc, ..
} => {
(self.callback_fn)(cx.editor, &self.line, PromptEvent::Abort);
return close_fn;
}
KeyEvent {
code: KeyCode::Char('f'),
modifiers: KeyModifiers::CONTROL,
}
| KeyEvent {
code: KeyCode::Right,
..
} => self.move_char_right(),
} => self.move_cursor(Movement::ForwardChar(1)),
KeyEvent {
code: KeyCode::Char('b'),
modifiers: KeyModifiers::CONTROL,
}
| KeyEvent {
code: KeyCode::Left,
..
} => self.move_char_left(),
} => self.move_cursor(Movement::BackwardChar(1)),
archseer marked this conversation as resolved.
Show resolved Hide resolved
KeyEvent {
code: KeyCode::End,
modifiers: KeyModifiers::NONE,
}
| KeyEvent {
code: KeyCode::Char('e'),
modifiers: KeyModifiers::CONTROL,
} => self.move_end(),
KeyEvent {
code: KeyCode::Home,
modifiers: KeyModifiers::NONE,
}
| KeyEvent {
code: KeyCode::Char('a'),
modifiers: KeyModifiers::CONTROL,
} => self.move_start(),
KeyEvent {
code: KeyCode::Left,
modifiers: KeyModifiers::ALT,
}
| KeyEvent {
code: KeyCode::Char('b'),
modifiers: KeyModifiers::ALT,
} => self.move_cursor(Movement::BackwardWord(1)),
KeyEvent {
code: KeyCode::Right,
modifiers: KeyModifiers::ALT,
}
| KeyEvent {
code: KeyCode::Char('f'),
modifiers: KeyModifiers::ALT,
} => self.move_cursor(Movement::ForwardWord(1)),
KeyEvent {
code: KeyCode::Char('w'),
modifiers: KeyModifiers::CONTROL,
} => self.delete_word_backwards(),
KeyEvent {
code: KeyCode::Char('k'),
modifiers: KeyModifiers::CONTROL,
} => self.kill_to_end_of_line(),
KeyEvent {
code: KeyCode::Backspace,
modifiers: KeyModifiers::NONE,
Expand Down Expand Up @@ -363,7 +499,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,
)
Expand Down