Skip to content

Commit 447c3ff

Browse files
authored
feat(cli): Add file/folder name auto-completion in q chat (#930)
* Add file/folder name auto-completion in q chat * feat(cli): Add PathCompleter for file path suggestions Implement PathCompleter struct to provide filesystem path completion functionality in the CLI, enhancing the user experience with intelligent path suggestions during input. 🤖 Assisted by [Amazon Q Developer](https://aws.amazon.com/q/developer) * use file-path completion as fallback
1 parent 84f8cca commit 447c3ff

File tree

1 file changed

+113
-15
lines changed

1 file changed

+113
-15
lines changed

crates/q_cli/src/cli/chat/prompt.rs

Lines changed: 113 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use crossterm::style::Stylize;
44
use eyre::Result;
55
use rustyline::completion::{
66
Completer,
7+
FilenameCompleter,
78
extract_word,
89
};
910
use rustyline::error::ReadlineError;
@@ -61,11 +62,63 @@ pub fn generate_prompt(current_profile: Option<&str>) -> String {
6162
"> ".to_string()
6263
}
6364

64-
pub struct ChatCompleter {}
65+
/// Complete commands that start with a slash
66+
fn complete_command(word: &str, start: usize) -> (usize, Vec<String>) {
67+
(
68+
start,
69+
COMMANDS
70+
.iter()
71+
.filter(|p| p.starts_with(word))
72+
.map(|s| (*s).to_owned())
73+
.collect(),
74+
)
75+
}
76+
77+
/// A wrapper around FilenameCompleter that provides enhanced path detection
78+
/// and completion capabilities for the chat interface.
79+
pub struct PathCompleter {
80+
/// The underlying filename completer from rustyline
81+
filename_completer: FilenameCompleter,
82+
}
83+
84+
impl PathCompleter {
85+
/// Creates a new PathCompleter instance
86+
pub fn new() -> Self {
87+
Self {
88+
filename_completer: FilenameCompleter::new(),
89+
}
90+
}
91+
92+
/// Attempts to complete a file path at the given position in the line
93+
pub fn complete_path(
94+
&self,
95+
line: &str,
96+
pos: usize,
97+
ctx: &Context<'_>,
98+
) -> Result<(usize, Vec<String>), ReadlineError> {
99+
// Use the filename completer to get path completions
100+
match self.filename_completer.complete(line, pos, ctx) {
101+
Ok((pos, completions)) => {
102+
// Convert the filename completer's pairs to strings
103+
let file_completions: Vec<String> = completions.iter().map(|pair| pair.replacement.clone()).collect();
104+
105+
// Return the completions if we have any
106+
Ok((pos, file_completions))
107+
},
108+
Err(err) => Err(err),
109+
}
110+
}
111+
}
112+
113+
pub struct ChatCompleter {
114+
path_completer: PathCompleter,
115+
}
65116

66117
impl ChatCompleter {
67118
fn new() -> Self {
68-
Self {}
119+
Self {
120+
path_completer: PathCompleter::new(),
121+
}
69122
}
70123
}
71124

@@ -79,18 +132,21 @@ impl Completer for ChatCompleter {
79132
_ctx: &Context<'_>,
80133
) -> Result<(usize, Vec<Self::Candidate>), ReadlineError> {
81134
let (start, word) = extract_word(line, pos, None, |c| c.is_space());
82-
Ok((
83-
start,
84-
if word.starts_with('/') {
85-
COMMANDS
86-
.iter()
87-
.filter(|p| p.starts_with(word))
88-
.map(|s| (*s).to_owned())
89-
.collect()
90-
} else {
91-
Vec::new()
92-
},
93-
))
135+
136+
// Handle command completion
137+
if word.starts_with('/') {
138+
return Ok(complete_command(word, start));
139+
}
140+
141+
// Handle file path completion as fallback
142+
if let Ok((pos, completions)) = self.path_completer.complete_path(line, pos, _ctx) {
143+
if !completions.is_empty() {
144+
return Ok((pos, completions));
145+
}
146+
}
147+
148+
// Default: no completions
149+
Ok((start, Vec::new()))
94150
}
95151
}
96152

@@ -159,8 +215,50 @@ mod tests {
159215

160216
#[test]
161217
fn test_generate_prompt() {
218+
// Test default prompt (no profile)
162219
assert_eq!(generate_prompt(None), "> ");
220+
// Test default profile (should be same as no profile)
163221
assert_eq!(generate_prompt(Some("default")), "> ");
164-
assert!(generate_prompt(Some("test-profile")).contains("test-profile"));
222+
// Test custom profile
223+
assert_eq!(generate_prompt(Some("test-profile")), "[test-profile] > ");
224+
// Test another custom profile
225+
assert_eq!(generate_prompt(Some("dev")), "[dev] > ");
226+
}
227+
228+
#[test]
229+
fn test_chat_completer_command_completion() {
230+
let completer = ChatCompleter::new();
231+
let line = "/h";
232+
let pos = 2; // Position at the end of "/h"
233+
234+
// Create a mock context with empty history
235+
let empty_history = DefaultHistory::new();
236+
let ctx = Context::new(&empty_history);
237+
238+
// Get completions
239+
let (start, completions) = completer.complete(line, pos, &ctx).unwrap();
240+
241+
// Verify start position
242+
assert_eq!(start, 0);
243+
244+
// Verify completions contain expected commands
245+
assert!(completions.contains(&"/help".to_string()));
246+
}
247+
248+
#[test]
249+
fn test_chat_completer_no_completion() {
250+
let completer = ChatCompleter::new();
251+
let line = "Hello, how are you?";
252+
let pos = line.len();
253+
254+
// Create a mock context with empty history
255+
let empty_history = DefaultHistory::new();
256+
let ctx = Context::new(&empty_history);
257+
258+
// Get completions
259+
let (_, completions) = completer.complete(line, pos, &ctx).unwrap();
260+
261+
// Verify no completions are returned for regular text
262+
assert!(completions.is_empty());
165263
}
166264
}

0 commit comments

Comments
 (0)