From dece1210c7096e6267937efae93148d711088cdb Mon Sep 17 00:00:00 2001 From: vasilito Date: Tue, 7 Jul 2026 05:59:05 +0300 Subject: [PATCH] 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 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. --- .../tui/tlc/source/src/filemanager/cmdline.rs | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/local/recipes/tui/tlc/source/src/filemanager/cmdline.rs b/local/recipes/tui/tlc/source/src/filemanager/cmdline.rs index 8e34f15474..d3c850ec03 100644 --- a/local/recipes/tui/tlc/source/src/filemanager/cmdline.rs +++ b/local/recipes/tui/tlc/source/src/filemanager/cmdline.rs @@ -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); + } }