mirror of
https://github.com/wez/wezterm.git
synced 2024-12-24 13:52:55 +03:00
lineedit: add tab completion support
This commit is contained in:
parent
330c8e8c1f
commit
5976e8d229
@ -27,10 +27,72 @@ impl LineEditorHost for Host {
|
|||||||
fn history(&mut self) -> &mut History {
|
fn history(&mut self) -> &mut History {
|
||||||
&mut self.history
|
&mut self.history
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Demo of the completion API for words starting with "h" or "he"
|
||||||
|
fn complete(&self, line: &str, cursor_position: usize) -> Vec<CompletionCandidate> {
|
||||||
|
let mut candidates = vec![];
|
||||||
|
if let Some((range, word)) = word_at_cursor(line, cursor_position) {
|
||||||
|
let words = &["hello", "help", "he-man"];
|
||||||
|
|
||||||
|
for w in words {
|
||||||
|
if w.starts_with(word) {
|
||||||
|
candidates.push(CompletionCandidate {
|
||||||
|
range: range.clone(),
|
||||||
|
text: w.to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
candidates
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This is a conceptually simple function that computes the bounds
|
||||||
|
/// of the whitespace delimited word at the specified cursor position
|
||||||
|
/// in the supplied line string.
|
||||||
|
/// It returns the range and the corresponding slice out of the line.
|
||||||
|
/// This function is sufficient for example purposes; in a real application
|
||||||
|
/// the equivalent function would need to be aware of quoting and other
|
||||||
|
/// application specific context.
|
||||||
|
fn word_at_cursor(line: &str, cursor_position: usize) -> Option<(std::ops::Range<usize>, &str)> {
|
||||||
|
let char_indices: Vec<(usize, char)> = line.char_indices().collect();
|
||||||
|
if char_indices.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let char_position = char_indices
|
||||||
|
.iter()
|
||||||
|
.position(|(idx, _)| *idx == cursor_position)
|
||||||
|
.unwrap_or(char_indices.len());
|
||||||
|
|
||||||
|
// Look back until we find whitespace
|
||||||
|
let mut start_position = char_position;
|
||||||
|
while start_position > 0
|
||||||
|
&& start_position <= char_indices.len()
|
||||||
|
&& !char_indices[start_position - 1].1.is_whitespace()
|
||||||
|
{
|
||||||
|
start_position -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look forwards until we find whitespace
|
||||||
|
let mut end_position = char_position;
|
||||||
|
while end_position < char_indices.len() && !char_indices[end_position].1.is_whitespace() {
|
||||||
|
end_position += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if end_position > start_position {
|
||||||
|
let range = char_indices[start_position].0
|
||||||
|
..char_indices
|
||||||
|
.get(end_position)
|
||||||
|
.map(|c| c.0 + 1)
|
||||||
|
.unwrap_or(line.len());
|
||||||
|
Some((range.clone(), &line[range]))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> Fallible<()> {
|
fn main() -> Fallible<()> {
|
||||||
println!("Type `exit` to quit this example");
|
println!("Type `exit` to quit this example, or start a word with `h` and press Tab.");
|
||||||
let mut editor = line_editor()?;
|
let mut editor = line_editor()?;
|
||||||
|
|
||||||
let mut host = Host::default();
|
let mut host = Host::default();
|
||||||
|
@ -22,4 +22,5 @@ pub enum Action {
|
|||||||
Kill(Movement),
|
Kill(Movement),
|
||||||
HistoryPrevious,
|
HistoryPrevious,
|
||||||
HistoryNext,
|
HistoryNext,
|
||||||
|
Complete,
|
||||||
}
|
}
|
||||||
|
@ -63,6 +63,26 @@ pub trait LineEditorHost {
|
|||||||
|
|
||||||
/// Returns the history implementation
|
/// Returns the history implementation
|
||||||
fn history(&mut self) -> &mut History;
|
fn history(&mut self) -> &mut History;
|
||||||
|
|
||||||
|
/// Tab completion support.
|
||||||
|
/// The line and current cursor position are provided and it is up to the
|
||||||
|
/// embedding application to produce a list of completion candidates.
|
||||||
|
/// The default implementation is an empty list.
|
||||||
|
fn complete(&self, _line: &str, _cursor_position: usize) -> Vec<CompletionCandidate> {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A candidate for tab completion.
|
||||||
|
/// If the line and cursor look like "why he<CURSOR>" and if "hello" is a valid
|
||||||
|
/// completion of "he" in that context, then the corresponding CompletionCandidate
|
||||||
|
/// would have its range set to [4..6] (the "he" slice range) and its text
|
||||||
|
/// set to "hello".
|
||||||
|
pub struct CompletionCandidate {
|
||||||
|
/// The section of the input line to be replaced
|
||||||
|
pub range: std::ops::Range<usize>,
|
||||||
|
/// The replacement text
|
||||||
|
pub text: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A concrete implementation of `LineEditorHost` that uses the default behaviors.
|
/// A concrete implementation of `LineEditorHost` that uses the default behaviors.
|
||||||
|
@ -76,6 +76,39 @@ pub struct LineEditor<T: Terminal> {
|
|||||||
|
|
||||||
history_pos: Option<usize>,
|
history_pos: Option<usize>,
|
||||||
bottom_line: Option<String>,
|
bottom_line: Option<String>,
|
||||||
|
|
||||||
|
completion: Option<CompletionState>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CompletionState {
|
||||||
|
candidates: Vec<CompletionCandidate>,
|
||||||
|
index: usize,
|
||||||
|
original_line: String,
|
||||||
|
original_cursor: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CompletionState {
|
||||||
|
fn next(&mut self) {
|
||||||
|
self.index += 1;
|
||||||
|
if self.index >= self.candidates.len() {
|
||||||
|
self.index = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn current(&self) -> (usize, String) {
|
||||||
|
let mut line = self.original_line.clone();
|
||||||
|
let candidate = &self.candidates[self.index];
|
||||||
|
line.replace_range(candidate.range.clone(), &candidate.text);
|
||||||
|
|
||||||
|
// To figure the new cursor position do a little math:
|
||||||
|
// "he<TAB>" when the completion is "hello" will set the completion
|
||||||
|
// candidate to replace "he" with "hello", so the difference in the
|
||||||
|
// lengths of these two is how far the cursor needs to move.
|
||||||
|
let range_len = candidate.range.end - candidate.range.start;
|
||||||
|
let new_cursor = self.original_cursor + candidate.text.len() - range_len;
|
||||||
|
|
||||||
|
(new_cursor, line)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Terminal> LineEditor<T> {
|
impl<T: Terminal> LineEditor<T> {
|
||||||
@ -107,6 +140,7 @@ impl<T: Terminal> LineEditor<T> {
|
|||||||
cursor: 0,
|
cursor: 0,
|
||||||
history_pos: None,
|
history_pos: None,
|
||||||
bottom_line: None,
|
bottom_line: None,
|
||||||
|
completion: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -173,6 +207,11 @@ impl<T: Terminal> LineEditor<T> {
|
|||||||
modifiers: Modifiers::CTRL,
|
modifiers: Modifiers::CTRL,
|
||||||
}) => Some(Action::Cancel),
|
}) => Some(Action::Cancel),
|
||||||
|
|
||||||
|
InputEvent::Key(KeyEvent {
|
||||||
|
key: KeyCode::Tab,
|
||||||
|
modifiers: Modifiers::NONE,
|
||||||
|
}) => Some(Action::Complete),
|
||||||
|
|
||||||
InputEvent::Key(KeyEvent {
|
InputEvent::Key(KeyEvent {
|
||||||
key: KeyCode::Char('D'),
|
key: KeyCode::Char('D'),
|
||||||
modifiers: Modifiers::CTRL,
|
modifiers: Modifiers::CTRL,
|
||||||
@ -389,6 +428,8 @@ impl<T: Terminal> LineEditor<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn kill_text(&mut self, movement: Movement) {
|
fn kill_text(&mut self, movement: Movement) {
|
||||||
|
self.clear_completion();
|
||||||
|
|
||||||
let new_cursor = self.eval_movement(movement);
|
let new_cursor = self.eval_movement(movement);
|
||||||
|
|
||||||
let (lower, upper) = if new_cursor < self.cursor {
|
let (lower, upper) = if new_cursor < self.cursor {
|
||||||
@ -405,11 +446,16 @@ impl<T: Terminal> LineEditor<T> {
|
|||||||
self.cursor = new_cursor.min(self.line.len());
|
self.cursor = new_cursor.min(self.line.len());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn clear_completion(&mut self) {
|
||||||
|
self.completion = None;
|
||||||
|
}
|
||||||
|
|
||||||
fn read_line_impl(&mut self, host: &mut LineEditorHost) -> Fallible<Option<String>> {
|
fn read_line_impl(&mut self, host: &mut LineEditorHost) -> Fallible<Option<String>> {
|
||||||
self.line.clear();
|
self.line.clear();
|
||||||
self.cursor = 0;
|
self.cursor = 0;
|
||||||
self.history_pos = None;
|
self.history_pos = None;
|
||||||
self.bottom_line = None;
|
self.bottom_line = None;
|
||||||
|
self.clear_completion();
|
||||||
|
|
||||||
self.render(host)?;
|
self.render(host)?;
|
||||||
while let Some(event) = self.terminal.poll_input(None)? {
|
while let Some(event) = self.terminal.poll_input(None)? {
|
||||||
@ -424,8 +470,12 @@ impl<T: Terminal> LineEditor<T> {
|
|||||||
.into())
|
.into())
|
||||||
}
|
}
|
||||||
Some(Action::Kill(movement)) => self.kill_text(movement),
|
Some(Action::Kill(movement)) => self.kill_text(movement),
|
||||||
Some(Action::Move(movement)) => self.cursor = self.eval_movement(movement),
|
Some(Action::Move(movement)) => {
|
||||||
|
self.clear_completion();
|
||||||
|
self.cursor = self.eval_movement(movement);
|
||||||
|
}
|
||||||
Some(Action::InsertChar(rep, c)) => {
|
Some(Action::InsertChar(rep, c)) => {
|
||||||
|
self.clear_completion();
|
||||||
for _ in 0..rep {
|
for _ in 0..rep {
|
||||||
self.line.insert(self.cursor, c);
|
self.line.insert(self.cursor, c);
|
||||||
let mut cursor = GraphemeCursor::new(self.cursor, self.line.len(), false);
|
let mut cursor = GraphemeCursor::new(self.cursor, self.line.len(), false);
|
||||||
@ -435,6 +485,7 @@ impl<T: Terminal> LineEditor<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(Action::InsertText(rep, text)) => {
|
Some(Action::InsertText(rep, text)) => {
|
||||||
|
self.clear_completion();
|
||||||
for _ in 0..rep {
|
for _ in 0..rep {
|
||||||
self.line.insert_str(self.cursor, &text);
|
self.line.insert_str(self.cursor, &text);
|
||||||
self.cursor += text.len();
|
self.cursor += text.len();
|
||||||
@ -445,6 +496,7 @@ impl<T: Terminal> LineEditor<T> {
|
|||||||
.render(&[Change::ClearScreen(Default::default())])?;
|
.render(&[Change::ClearScreen(Default::default())])?;
|
||||||
}
|
}
|
||||||
Some(Action::HistoryPrevious) => {
|
Some(Action::HistoryPrevious) => {
|
||||||
|
self.clear_completion();
|
||||||
if let Some(cur_pos) = self.history_pos.as_ref() {
|
if let Some(cur_pos) = self.history_pos.as_ref() {
|
||||||
let prior_idx = cur_pos.saturating_sub(1);
|
let prior_idx = cur_pos.saturating_sub(1);
|
||||||
if let Some(prior) = host.history().get(prior_idx) {
|
if let Some(prior) = host.history().get(prior_idx) {
|
||||||
@ -466,6 +518,7 @@ impl<T: Terminal> LineEditor<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(Action::HistoryNext) => {
|
Some(Action::HistoryNext) => {
|
||||||
|
self.clear_completion();
|
||||||
if let Some(cur_pos) = self.history_pos.as_ref() {
|
if let Some(cur_pos) = self.history_pos.as_ref() {
|
||||||
let next_idx = cur_pos.saturating_add(1);
|
let next_idx = cur_pos.saturating_add(1);
|
||||||
if let Some(next) = host.history().get(next_idx) {
|
if let Some(next) = host.history().get(next_idx) {
|
||||||
@ -481,6 +534,29 @@ impl<T: Terminal> LineEditor<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Some(Action::Complete) => {
|
||||||
|
if self.completion.is_none() {
|
||||||
|
let candidates = host.complete(&self.line, self.cursor);
|
||||||
|
if !candidates.is_empty() {
|
||||||
|
let state = CompletionState {
|
||||||
|
candidates,
|
||||||
|
index: 0,
|
||||||
|
original_line: self.line.clone(),
|
||||||
|
original_cursor: self.cursor,
|
||||||
|
};
|
||||||
|
|
||||||
|
let (cursor, line) = state.current();
|
||||||
|
self.cursor = cursor;
|
||||||
|
self.line = line;
|
||||||
|
self.completion = Some(state);
|
||||||
|
}
|
||||||
|
} else if let Some(state) = self.completion.as_mut() {
|
||||||
|
state.next();
|
||||||
|
let (cursor, line) = state.current();
|
||||||
|
self.cursor = cursor;
|
||||||
|
self.line = line;
|
||||||
|
}
|
||||||
|
}
|
||||||
None => {}
|
None => {}
|
||||||
}
|
}
|
||||||
self.render(host)?;
|
self.render(host)?;
|
||||||
|
Loading…
Reference in New Issue
Block a user