Add TAB autocompletion for /project command
- Complete project names from ~/projects/ directory - Display shows project name, replacement uses ~/projects/<name> path - Projects sorted alphabetically - Added test for project completion
This commit is contained in:
@@ -5,6 +5,7 @@
|
|||||||
//! - Command completion for `/` commands at line start
|
//! - Command completion for `/` commands at line start
|
||||||
//! - File path completion for `./`, `../`, `~/`, `/` prefixes
|
//! - File path completion for `./`, `../`, `~/`, `/` prefixes
|
||||||
//! - Session ID completion for `/resume` command
|
//! - Session ID completion for `/resume` command
|
||||||
|
//! - Project name completion for `/project` command (from ~/projects/)
|
||||||
|
|
||||||
use rustyline::completion::{Completer, FilenameCompleter, Pair};
|
use rustyline::completion::{Completer, FilenameCompleter, Pair};
|
||||||
use rustyline::error::ReadlineError;
|
use rustyline::error::ReadlineError;
|
||||||
@@ -153,6 +154,33 @@ impl G3Helper {
|
|||||||
|
|
||||||
sessions
|
sessions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// List project directories from ~/projects/, sorted alphabetically.
|
||||||
|
fn list_projects(&self, prefix: &str) -> Vec<String> {
|
||||||
|
let projects_dir = match dirs::home_dir() {
|
||||||
|
Some(home) => home.join("projects"),
|
||||||
|
None => return Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if !projects_dir.is_dir() {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut projects: Vec<String> = std::fs::read_dir(&projects_dir)
|
||||||
|
.ok()
|
||||||
|
.map(|entries| {
|
||||||
|
entries
|
||||||
|
.filter_map(|entry| entry.ok())
|
||||||
|
.filter(|entry| entry.path().is_dir())
|
||||||
|
.filter_map(|entry| Some(entry.file_name().to_string_lossy().to_string()))
|
||||||
|
.filter(|name| name.starts_with(prefix))
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
projects.sort();
|
||||||
|
projects
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for G3Helper {
|
impl Default for G3Helper {
|
||||||
@@ -261,6 +289,23 @@ impl Completer for G3Helper {
|
|||||||
return Ok((word_start, matches));
|
return Ok((word_start, matches));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Case 5: Project name completion for /project command
|
||||||
|
if line_to_cursor.starts_with("/project ") {
|
||||||
|
let partial = word;
|
||||||
|
let projects = self.list_projects(partial);
|
||||||
|
let matches: Vec<Pair> = projects
|
||||||
|
.into_iter()
|
||||||
|
.map(|name| {
|
||||||
|
let full_path = format!("~/projects/{}", name);
|
||||||
|
Pair {
|
||||||
|
display: name,
|
||||||
|
replacement: full_path,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
return Ok((word_start, matches));
|
||||||
|
}
|
||||||
|
|
||||||
// No completion for regular text
|
// No completion for regular text
|
||||||
Ok((pos, vec![]))
|
Ok((pos, vec![]))
|
||||||
}
|
}
|
||||||
@@ -502,4 +547,36 @@ mod tests {
|
|||||||
let sessions = helper.list_sessions(None);
|
let sessions = helper.list_sessions(None);
|
||||||
let _ = sessions; // Just verify no panic
|
let _ = sessions; // Just verify no panic
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_project_completion_lists_projects() {
|
||||||
|
let helper = G3Helper::new();
|
||||||
|
let history = rustyline::history::DefaultHistory::new();
|
||||||
|
let ctx = Context::new(&history);
|
||||||
|
|
||||||
|
let line = "/project ";
|
||||||
|
let pos = line.len();
|
||||||
|
let (start, completions) = helper.complete(line, pos, &ctx).unwrap();
|
||||||
|
let _ = start;
|
||||||
|
|
||||||
|
// If ~/projects exists and has directories, we should get completions
|
||||||
|
if let Some(home) = dirs::home_dir() {
|
||||||
|
let projects_dir = home.join("projects");
|
||||||
|
if projects_dir.is_dir() {
|
||||||
|
// Verify completions have the right format (display is name, replacement is ~/projects/name)
|
||||||
|
for completion in &completions {
|
||||||
|
assert!(completion.replacement.starts_with("~/projects/"),
|
||||||
|
"Replacement should start with ~/projects/, got: {}", completion.replacement);
|
||||||
|
assert!(!completion.display.contains('/'),
|
||||||
|
"Display should be just the project name, got: {}", completion.display);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test with a prefix that won't match anything
|
||||||
|
let line = "/project zzz_nonexistent_prefix_";
|
||||||
|
let pos = line.len();
|
||||||
|
let (_, completions) = helper.complete(line, pos, &ctx).unwrap();
|
||||||
|
assert_eq!(completions.len(), 0, "Non-matching prefix should return empty");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user