1
1
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:
Wez Furlong 2019-05-27 17:39:10 -07:00
parent 330c8e8c1f
commit 5976e8d229
4 changed files with 161 additions and 2 deletions

View File

@ -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();

View File

@ -22,4 +22,5 @@ pub enum Action {
Kill(Movement), Kill(Movement),
HistoryPrevious, HistoryPrevious,
HistoryNext, HistoryNext,
Complete,
} }

View File

@ -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.

View File

@ -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)?;