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 {
|
||||
&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<()> {
|
||||
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 host = Host::default();
|
||||
|
@ -22,4 +22,5 @@ pub enum Action {
|
||||
Kill(Movement),
|
||||
HistoryPrevious,
|
||||
HistoryNext,
|
||||
Complete,
|
||||
}
|
||||
|
@ -63,6 +63,26 @@ pub trait LineEditorHost {
|
||||
|
||||
/// Returns the history implementation
|
||||
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.
|
||||
|
@ -76,6 +76,39 @@ pub struct LineEditor<T: Terminal> {
|
||||
|
||||
history_pos: Option<usize>,
|
||||
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> {
|
||||
@ -107,6 +140,7 @@ impl<T: Terminal> LineEditor<T> {
|
||||
cursor: 0,
|
||||
history_pos: None,
|
||||
bottom_line: None,
|
||||
completion: None,
|
||||
}
|
||||
}
|
||||
|
||||
@ -173,6 +207,11 @@ impl<T: Terminal> LineEditor<T> {
|
||||
modifiers: Modifiers::CTRL,
|
||||
}) => Some(Action::Cancel),
|
||||
|
||||
InputEvent::Key(KeyEvent {
|
||||
key: KeyCode::Tab,
|
||||
modifiers: Modifiers::NONE,
|
||||
}) => Some(Action::Complete),
|
||||
|
||||
InputEvent::Key(KeyEvent {
|
||||
key: KeyCode::Char('D'),
|
||||
modifiers: Modifiers::CTRL,
|
||||
@ -389,6 +428,8 @@ impl<T: Terminal> LineEditor<T> {
|
||||
}
|
||||
|
||||
fn kill_text(&mut self, movement: Movement) {
|
||||
self.clear_completion();
|
||||
|
||||
let new_cursor = self.eval_movement(movement);
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
fn clear_completion(&mut self) {
|
||||
self.completion = None;
|
||||
}
|
||||
|
||||
fn read_line_impl(&mut self, host: &mut LineEditorHost) -> Fallible<Option<String>> {
|
||||
self.line.clear();
|
||||
self.cursor = 0;
|
||||
self.history_pos = None;
|
||||
self.bottom_line = None;
|
||||
self.clear_completion();
|
||||
|
||||
self.render(host)?;
|
||||
while let Some(event) = self.terminal.poll_input(None)? {
|
||||
@ -424,8 +470,12 @@ impl<T: Terminal> LineEditor<T> {
|
||||
.into())
|
||||
}
|
||||
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)) => {
|
||||
self.clear_completion();
|
||||
for _ in 0..rep {
|
||||
self.line.insert(self.cursor, c);
|
||||
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)) => {
|
||||
self.clear_completion();
|
||||
for _ in 0..rep {
|
||||
self.line.insert_str(self.cursor, &text);
|
||||
self.cursor += text.len();
|
||||
@ -445,6 +496,7 @@ impl<T: Terminal> LineEditor<T> {
|
||||
.render(&[Change::ClearScreen(Default::default())])?;
|
||||
}
|
||||
Some(Action::HistoryPrevious) => {
|
||||
self.clear_completion();
|
||||
if let Some(cur_pos) = self.history_pos.as_ref() {
|
||||
let prior_idx = cur_pos.saturating_sub(1);
|
||||
if let Some(prior) = host.history().get(prior_idx) {
|
||||
@ -466,6 +518,7 @@ impl<T: Terminal> LineEditor<T> {
|
||||
}
|
||||
}
|
||||
Some(Action::HistoryNext) => {
|
||||
self.clear_completion();
|
||||
if let Some(cur_pos) = self.history_pos.as_ref() {
|
||||
let next_idx = cur_pos.saturating_add(1);
|
||||
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 => {}
|
||||
}
|
||||
self.render(host)?;
|
||||
|
Loading…
Reference in New Issue
Block a user