W40: Cmdline completion hint popup
When Tab in the cmdline completes to more than one candidate, show a popup above the cmdline listing the candidates. Standard shell completion behaviour (fish, zsh, bash). Helps users pick the right file when many match. - Cmdline::completions: Vec<String> field stores the candidates - Cmdline::clear_completions() for cleanup on commit/cancel - Cmdline::has_completions() returns true if >1 candidates - Cmdline::render shows the popup when has_completions() is true - Esc/Enter in handle_key clears completions Added 1 unit test verifying completions are populated and clear_completions works. Tests: 1402 pass (was 1401), zero warnings.
This commit is contained in:
@@ -200,6 +200,21 @@ if matches.is_empty() {
|
||||
self.base_dir = Some(path);
|
||||
}
|
||||
|
||||
/// Clear the completion hint list. Call this when the cmdline
|
||||
/// is committed or cancelled.
|
||||
pub fn clear_completions(&mut self) {
|
||||
self.completions.clear();
|
||||
}
|
||||
|
||||
/// `true` when the last Tab completion produced multiple
|
||||
/// candidates (so the renderer should show a hint popup).
|
||||
#[must_use]
|
||||
pub fn has_completions(&self) -> bool {
|
||||
self.completions.len() > 1
|
||||
}
|
||||
|
||||
/// Insert `text` at the cursor. Activates the cmdline if it
|
||||
/// is not already active.
|
||||
pub fn insert_text(&mut self, text: &str) {
|
||||
if !self.active {
|
||||
self.activate();
|
||||
@@ -268,6 +283,7 @@ if matches.is_empty() {
|
||||
}
|
||||
match key {
|
||||
Key::ESCAPE => {
|
||||
self.clear_completions();
|
||||
self.deactivate();
|
||||
CmdlineResult::Cancelled
|
||||
}
|
||||
@@ -277,6 +293,7 @@ if matches.is_empty() {
|
||||
return CmdlineResult::Cancelled;
|
||||
}
|
||||
self.push_history(&cmd);
|
||||
self.clear_completions();
|
||||
self.deactivate();
|
||||
CmdlineResult::Execute(cmd)
|
||||
}
|
||||
@@ -409,6 +426,52 @@ if matches.is_empty() {
|
||||
let line = Line::from(prompt_span);
|
||||
frame.render_widget(Paragraph::new(line), area);
|
||||
}
|
||||
|
||||
// Completion hint popup: shown above the cmdline when
|
||||
// Tab produced multiple candidates.
|
||||
if self.has_completions() && area.y >= self.completions.len() as u16 {
|
||||
let hint_h = self.completions.len() as u16 + 2;
|
||||
let hint_w = (self
|
||||
.completions
|
||||
.iter()
|
||||
.map(|s| s.chars().count())
|
||||
.max()
|
||||
.unwrap_or(0) as u16)
|
||||
+ 4;
|
||||
let hint_area = Rect {
|
||||
x: area.x,
|
||||
y: area.y.saturating_sub(hint_h),
|
||||
width: hint_w.min(area.width),
|
||||
height: hint_h,
|
||||
};
|
||||
let block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(theme.accent))
|
||||
.title(Span::styled(
|
||||
" Completions ",
|
||||
Style::default()
|
||||
.fg(theme.cursor_fg)
|
||||
.bg(theme.accent)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
));
|
||||
let inner = block.inner(hint_area);
|
||||
frame.render_widget(block, hint_area);
|
||||
let mut y = inner.y;
|
||||
for cand in &self.completions {
|
||||
if y >= inner.y + inner.height {
|
||||
break;
|
||||
}
|
||||
let line = Line::from(Span::styled(
|
||||
format!(" {}", cand),
|
||||
Style::default().fg(theme.foreground),
|
||||
));
|
||||
frame.render_widget(
|
||||
Paragraph::new(line),
|
||||
Rect::new(inner.x, y, inner.width, 1),
|
||||
);
|
||||
y += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -536,4 +599,24 @@ mod tests {
|
||||
assert_eq!(c.input.value(), "foo_");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn complete_populates_completions_for_popup() {
|
||||
let dir = std::env::temp_dir().join("tlc-clp-cmdline-completions");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
std::fs::create_dir_all(dir.join("alpha")).unwrap();
|
||||
std::fs::create_dir_all(dir.join("alpine")).unwrap();
|
||||
let mut c = Cmdline::new();
|
||||
c.activate();
|
||||
c.insert_text("al");
|
||||
c.complete(Some(&dir));
|
||||
// Two candidates should have populated completions.
|
||||
assert!(c.has_completions());
|
||||
assert!(c.completions.contains(&"alpha".to_string()));
|
||||
assert!(c.completions.contains(&"alpine".to_string()));
|
||||
c.clear_completions();
|
||||
assert!(!c.has_completions());
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user