Sweep every first-party crate source (1956 .rs files) to the project comment guidelines: delete redundant restatements, decorative banners, change narration, and end-of-line comments; keep and tighten the crucial ones (invariants, bug rationale, SAFETY blocks, ported-source attribution). No functional code changed. Every edit is proven comment-only against the prior tree by a comment-stripping lexer (string/char/raw-string aware) plus a separate doctest-fence check. Where removing a comment made rustfmt or clippy want to re-lay-out adjacent code, the minimal triggering comment is restored so code tokens stay byte-identical. Gates green: cargo fmt --all --check (0 diffs), cargo check and cargo clippy --workspace --all-targets (0 warnings). Adds scripts/check_codegen_comment_guidelines.py — the enforcement gate for these guidelines (flags banners, end-of-line comments, change narration, and commented-out code).
400 lines
13 KiB
Rust
400 lines
13 KiB
Rust
use std::sync::atomic::{AtomicBool, Ordering};
|
|
use std::sync::{Arc, OnceLock};
|
|
use std::time::{Duration, Instant};
|
|
|
|
use arc_swap::ArcSwap;
|
|
|
|
use super::shell_token::{CurrentToken, build_insert_token, parse_current_token};
|
|
use super::{RankedSuggestion, SuggestContext, SuggestionSource, splice_token_into_line};
|
|
|
|
const CACHE_TTL: Duration = Duration::from_secs(60);
|
|
const MAX_RESULTS: usize = 10;
|
|
|
|
pub(crate) struct PathProvider;
|
|
|
|
impl PathProvider {
|
|
pub async fn suggest(&self, ctx: &SuggestContext) -> Vec<RankedSuggestion> {
|
|
// shell_token quoting is POSIX-only: cmd/pwsh would misparse the
|
|
// escaped line, so Windows serves no deterministic completions.
|
|
if cfg!(windows) {
|
|
return Vec::new();
|
|
}
|
|
let tok = match extract_command_token(ctx.prefix()) {
|
|
Some(t) => t,
|
|
None => return Vec::new(),
|
|
};
|
|
let token_range = (tok.start, ctx.prefix().len());
|
|
|
|
let cache = get_or_refresh_path_cache().await;
|
|
let mut results = filter_executables(&tok, token_range, &cache.executables);
|
|
splice_token_into_line(&mut results, &ctx.text, token_range);
|
|
results
|
|
}
|
|
}
|
|
|
|
/// The command token being typed, via the canonical tokenizer: quotes hide
|
|
/// separators (`echo "a | gr` is quoted data, not a command position), the
|
|
/// cursor must sit in the segment's first word, and — mirroring the file
|
|
/// provider — flag-looking tokens never complete.
|
|
fn extract_command_token(prefix: &str) -> Option<CurrentToken> {
|
|
let tok = parse_current_token(prefix);
|
|
if tok.tokens_before != 0 || tok.after_redirect || tok.value.is_empty() {
|
|
return None;
|
|
}
|
|
if tok.value.starts_with('-') {
|
|
return None;
|
|
}
|
|
Some(tok)
|
|
}
|
|
|
|
fn filter_executables(
|
|
tok: &CurrentToken,
|
|
token_range: (usize, usize),
|
|
executables: &[String],
|
|
) -> Vec<RankedSuggestion> {
|
|
let prefix = tok.value.as_str();
|
|
// Binary search to the first entry >= prefix, then take while starts_with.
|
|
let start = executables.partition_point(|e| e.as_str() < prefix);
|
|
|
|
let mut results: Vec<RankedSuggestion> = Vec::new();
|
|
let mut truncated = false;
|
|
for exe in executables[start..]
|
|
.iter()
|
|
.take_while(|exe| exe.starts_with(prefix))
|
|
{
|
|
if results.len() == MAX_RESULTS {
|
|
// An uncapped match remains: the set is not exhaustive.
|
|
truncated = true;
|
|
break;
|
|
}
|
|
results.push(RankedSuggestion {
|
|
display: exe.clone(),
|
|
// Re-quoted like filenames: an executable named `zz;echo PWNED`
|
|
// must insert as ONE word, never a second command.
|
|
insert_text: build_insert_token(tok, "", exe, false),
|
|
description: String::new(),
|
|
source: SuggestionSource::Path,
|
|
priority: 0,
|
|
is_ghost_candidate: false,
|
|
replace_range: Some(token_range),
|
|
token_text: None,
|
|
truncated: false,
|
|
});
|
|
}
|
|
if truncated {
|
|
results.iter_mut().for_each(|s| s.truncated = true);
|
|
}
|
|
results
|
|
}
|
|
|
|
struct PathCacheInner {
|
|
executables: Vec<String>,
|
|
updated_at: Instant,
|
|
path_env: String,
|
|
}
|
|
|
|
static PATH_CACHE: OnceLock<ArcSwap<PathCacheInner>> = OnceLock::new();
|
|
static PATH_REFRESHING: AtomicBool = AtomicBool::new(false);
|
|
|
|
async fn get_or_refresh_path_cache() -> Arc<PathCacheInner> {
|
|
let swap = PATH_CACHE.get_or_init(|| {
|
|
ArcSwap::from_pointee(PathCacheInner {
|
|
executables: Vec::new(),
|
|
updated_at: Instant::now() - CACHE_TTL - Duration::from_secs(1),
|
|
path_env: String::new(),
|
|
})
|
|
});
|
|
|
|
let current = swap.load_full();
|
|
let current_path = std::env::var("PATH").unwrap_or_default();
|
|
|
|
if current.updated_at.elapsed() < CACHE_TTL && current.path_env == current_path {
|
|
return current;
|
|
}
|
|
|
|
if PATH_REFRESHING
|
|
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Relaxed)
|
|
.is_err()
|
|
{
|
|
return current;
|
|
}
|
|
|
|
let result = match tokio::task::spawn_blocking(scan_path_dirs).await {
|
|
Ok(executables) => {
|
|
let new = Arc::new(PathCacheInner {
|
|
executables,
|
|
updated_at: Instant::now(),
|
|
path_env: current_path,
|
|
});
|
|
swap.store(Arc::clone(&new));
|
|
new
|
|
}
|
|
Err(_) => current,
|
|
};
|
|
|
|
PATH_REFRESHING.store(false, Ordering::Release);
|
|
result
|
|
}
|
|
|
|
fn scan_path_dirs() -> Vec<String> {
|
|
let path_var = std::env::var("PATH").unwrap_or_default();
|
|
scan_path_from(&path_var)
|
|
}
|
|
|
|
fn scan_path_from(path_var: &str) -> Vec<String> {
|
|
let mut executables = Vec::new();
|
|
|
|
for dir in std::env::split_paths(path_var) {
|
|
let entries = match std::fs::read_dir(&dir) {
|
|
Ok(e) => e,
|
|
Err(_) => continue,
|
|
};
|
|
|
|
for entry in entries.filter_map(Result::ok) {
|
|
let meta = match entry.metadata() {
|
|
Ok(m) => m,
|
|
Err(_) => continue,
|
|
};
|
|
|
|
if !meta.is_file() {
|
|
continue;
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
{
|
|
use std::os::unix::fs::PermissionsExt;
|
|
if meta.permissions().mode() & 0o111 == 0 {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if let Ok(name) = entry.file_name().into_string() {
|
|
executables.push(name);
|
|
}
|
|
}
|
|
}
|
|
|
|
executables.sort_unstable();
|
|
executables.dedup();
|
|
executables
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn cmd(prefix: &str) -> Option<(usize, String)> {
|
|
extract_command_token(prefix).map(|t| (t.start, t.value))
|
|
}
|
|
|
|
#[test]
|
|
fn prefix_at_start_of_line() {
|
|
assert_eq!(cmd("git"), Some((0, "git".into())));
|
|
assert_eq!(cmd("gi"), Some((0, "gi".into())));
|
|
}
|
|
|
|
#[test]
|
|
fn no_prefix_at_argument_position() {
|
|
assert_eq!(cmd("git com"), None);
|
|
assert_eq!(cmd("ls | grep foo"), None);
|
|
}
|
|
|
|
#[test]
|
|
fn prefix_after_separators() {
|
|
assert_eq!(cmd("ls | gr"), Some((5, "gr".into())));
|
|
assert_eq!(cmd("make && gi"), Some((8, "gi".into())));
|
|
assert_eq!(cmd("cd /tmp; ls"), Some((9, "ls".into())));
|
|
assert_eq!(cmd("false || tr"), Some((9, "tr".into())));
|
|
assert_eq!(cmd("sleep 10 & ls"), Some((11, "ls".into())));
|
|
}
|
|
|
|
#[test]
|
|
fn none_when_empty_after_separator() {
|
|
assert_eq!(cmd("ls | "), None);
|
|
assert_eq!(cmd("sleep 10 & "), None);
|
|
assert_eq!(cmd("&&"), None);
|
|
}
|
|
|
|
#[test]
|
|
fn none_for_empty_or_whitespace_input() {
|
|
assert_eq!(cmd(""), None);
|
|
assert_eq!(cmd(" "), None);
|
|
}
|
|
|
|
/// A separator inside quotes is data, not a command position — a naive
|
|
/// segment scan would offer executables inside quoted strings.
|
|
#[test]
|
|
fn none_inside_quoted_data() {
|
|
assert_eq!(cmd("echo \"x | gr"), None);
|
|
}
|
|
|
|
/// Flag-looking and redirect-target tokens never command-complete.
|
|
#[test]
|
|
fn none_for_flags_and_redirect_targets() {
|
|
assert_eq!(cmd("-gr"), None);
|
|
assert_eq!(cmd("> lo"), None);
|
|
}
|
|
|
|
fn tok(prefix: &str) -> CurrentToken {
|
|
parse_current_token(prefix)
|
|
}
|
|
|
|
#[test]
|
|
fn filter_matches_prefix() {
|
|
let exes = vec![
|
|
"gcc".into(),
|
|
"git".into(),
|
|
"grep".into(),
|
|
"less".into(),
|
|
"ls".into(),
|
|
];
|
|
let results = filter_executables(&tok("gr"), (0, 2), &exes);
|
|
assert_eq!(results.len(), 1);
|
|
assert_eq!(results[0].insert_text, "grep");
|
|
}
|
|
|
|
#[test]
|
|
fn filter_multiple_matches() {
|
|
let exes = vec!["git".into(), "grep".into(), "gzip".into()];
|
|
let results = filter_executables(&tok("g"), (0, 1), &exes);
|
|
assert_eq!(results.len(), 3);
|
|
assert!(results.iter().all(|s| !s.truncated));
|
|
}
|
|
|
|
#[test]
|
|
fn filter_no_match() {
|
|
let exes = vec!["git".into(), "grep".into()];
|
|
assert!(filter_executables(&tok("docker"), (0, 6), &exes).is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn filter_path_suggestions_are_not_ghost() {
|
|
let exes = vec!["git".into()];
|
|
let results = filter_executables(&tok("g"), (0, 1), &exes);
|
|
assert!(!results[0].is_ghost_candidate);
|
|
assert_eq!(results[0].source, SuggestionSource::Path);
|
|
assert_eq!(results[0].priority, 0);
|
|
}
|
|
|
|
/// Capped sets mark every row truncated so the pager keeps
|
|
/// dropdown-only semantics (an unshown match could disprove an LCP).
|
|
#[test]
|
|
fn filter_caps_at_max_and_marks_truncated() {
|
|
let exes: Vec<String> = (0..20).map(|i| format!("test_{i:03}")).collect();
|
|
let results = filter_executables(&tok("test"), (0, 4), &exes);
|
|
assert_eq!(results.len(), MAX_RESULTS);
|
|
assert!(results.iter().all(|s| s.truncated));
|
|
|
|
let exes: Vec<String> = (0..MAX_RESULTS).map(|i| format!("test_{i:03}")).collect();
|
|
let results = filter_executables(&tok("test"), (0, 4), &exes);
|
|
assert_eq!(results.len(), MAX_RESULTS);
|
|
assert!(results.iter().all(|s| !s.truncated));
|
|
}
|
|
|
|
/// The segment-after-pipe token range: accepting `grep` for `ls | gr`
|
|
/// must target only the `gr` token, never the whole line.
|
|
#[test]
|
|
fn filter_stamps_segment_token_range() {
|
|
let t = extract_command_token("ls | gr").unwrap();
|
|
let exes = vec!["grep".into()];
|
|
let results = filter_executables(&t, (t.start, 7), &exes);
|
|
assert_eq!(results[0].replace_range, Some((5, 7)));
|
|
assert_eq!(results[0].insert_text, "grep");
|
|
}
|
|
|
|
/// Metacharacter executable names insert as ONE word — accepting
|
|
/// `zz;echo PWNED` must never put a second command on the line.
|
|
#[test]
|
|
fn filter_escapes_metacharacter_executable_names() {
|
|
let exes = vec!["zz;echo PWNED".into()];
|
|
let results = filter_executables(&tok("zz"), (0, 2), &exes);
|
|
assert_eq!(results[0].display, "zz;echo PWNED");
|
|
assert_eq!(results[0].insert_text, "zz\\;echo\\ PWNED");
|
|
}
|
|
|
|
/// A quoted command prefix completes inside its quote style.
|
|
#[test]
|
|
fn filter_requotes_quoted_command_prefix() {
|
|
let exes = vec!["grep".into()];
|
|
let results = filter_executables(&tok("\"gr"), (0, 3), &exes);
|
|
assert_eq!(results[0].insert_text, "\"grep\"");
|
|
}
|
|
|
|
#[test]
|
|
fn scan_nonexistent_dir() {
|
|
assert!(scan_path_from("/nonexistent/path/that/doesnt/exist").is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn scan_creates_sorted_deduped_list() {
|
|
use std::fs;
|
|
use tempfile::TempDir;
|
|
|
|
let dir = TempDir::new().unwrap();
|
|
let bin = dir.path().join("bin");
|
|
fs::create_dir(&bin).unwrap();
|
|
|
|
for name in &["zzz_cmd", "aaa_cmd", "mmm_cmd"] {
|
|
let path = bin.join(name);
|
|
fs::write(&path, "").unwrap();
|
|
#[cfg(unix)]
|
|
{
|
|
use std::os::unix::fs::PermissionsExt;
|
|
fs::set_permissions(&path, fs::Permissions::from_mode(0o755)).unwrap();
|
|
}
|
|
}
|
|
|
|
let result = scan_path_from(bin.to_str().unwrap());
|
|
assert_eq!(result, vec!["aaa_cmd", "mmm_cmd", "zzz_cmd"]);
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
#[test]
|
|
fn scan_skips_nonexecutable_files() {
|
|
use std::fs;
|
|
use std::os::unix::fs::PermissionsExt;
|
|
use tempfile::TempDir;
|
|
|
|
let dir = TempDir::new().unwrap();
|
|
let bin = dir.path().join("bin");
|
|
fs::create_dir(&bin).unwrap();
|
|
|
|
let exec_path = bin.join("my_exec");
|
|
fs::write(&exec_path, "").unwrap();
|
|
fs::set_permissions(&exec_path, fs::Permissions::from_mode(0o755)).unwrap();
|
|
|
|
let data_path = bin.join("my_data");
|
|
fs::write(&data_path, "").unwrap();
|
|
fs::set_permissions(&data_path, fs::Permissions::from_mode(0o644)).unwrap();
|
|
|
|
let result = scan_path_from(bin.to_str().unwrap());
|
|
assert_eq!(result, vec!["my_exec"]);
|
|
}
|
|
|
|
#[test]
|
|
fn scan_deduplicates_across_dirs() {
|
|
use std::fs;
|
|
use tempfile::TempDir;
|
|
|
|
let dir = TempDir::new().unwrap();
|
|
let bin1 = dir.path().join("bin1");
|
|
let bin2 = dir.path().join("bin2");
|
|
fs::create_dir(&bin1).unwrap();
|
|
fs::create_dir(&bin2).unwrap();
|
|
|
|
for bin in [&bin1, &bin2] {
|
|
let path = bin.join("shared_cmd");
|
|
fs::write(&path, "").unwrap();
|
|
#[cfg(unix)]
|
|
{
|
|
use std::os::unix::fs::PermissionsExt;
|
|
fs::set_permissions(&path, fs::Permissions::from_mode(0o755)).unwrap();
|
|
}
|
|
}
|
|
|
|
let path_var = format!("{}:{}", bin1.to_str().unwrap(), bin2.to_str().unwrap());
|
|
let result = scan_path_from(&path_var);
|
|
assert_eq!(result, vec!["shared_cmd"]);
|
|
}
|
|
}
|