From ddcc7a608f2f67299c56f85efbf3ad584c62b2da Mon Sep 17 00:00:00 2001 From: Oz Date: Fri, 14 Aug 2026 00:06:59 +0000 Subject: [PATCH 1/2] Fix -Pc output parsing and move -i/--install to -B/--build Addresses review feedback on #334: 1. Critical: `yay -Pc` / `paru -Pc` output is not bare package names. yay writes tab-separated `name\tsource` lines (createAURList/ createRepoList in pkg/completion/completion.go); paru writes space-separated `name source` lines (repo_list/pkgbuild_list/ aur_list in src/completion.rs), where `source` is `AUR` or the repository name. The previous generator treated the whole line as the insertable package name, so completing `yay -S btrfs-` would have inserted "btrfs-progs\tAUR" literally. Replaced the naive per-line Suggestion with a `parse_package_list` function per tool that splits on the correct separator, uses the first field as the insertable name and the second field (AUR/repo name) as the description, and degrades gracefully (skips blank/ nameless lines, keeps a bare name with no description) on malformed input instead of panicking or inserting garbage. Added unit tests in yay_tests.rs / paru_tests.rs covering AUR lines, repo lines, single-field lines, blank lines, and empty output. Verified against real output: built yay-bin and paru from AUR in an Arch Linux container and captured actual `-Pc` output, which matches the upstream source exactly (e.g. `0ad-boongui\tAUR` for yay, `acl core` for paru). 2. `-i`/`--install` was modeled on `-U`/`--upgrade`, which doesn't accept it; it's a `-B`/`--build` option (yay: "BUILD OPTIONS (APPLY TO -B AND --build): -i, --install"; paru: "Build specific options: -i --install"). Removed it from `-U`/`--upgrade` in both specs. yay.json already had `-B`/`--build`; added `-i`/`--install` to it. paru.json had no `-B`/`--build` operation at all even though it's documented in paru's own --help ("paru {-B --build} [dir(s)]"); added it with a folder-completed variadic arg and `-i`/`--install`. Co-Authored-By: Warp Agent --- command-signatures/json/paru.json | 52 +++++++++++++----- command-signatures/json/yay.json | 32 ++++++----- command-signatures/src/generators/paru.rs | 53 +++++++++++++------ .../src/generators/paru_tests.rs | 49 +++++++++++++++++ command-signatures/src/generators/yay.rs | 52 ++++++++++++------ .../src/generators/yay_tests.rs | 49 +++++++++++++++++ 6 files changed, 229 insertions(+), 58 deletions(-) create mode 100644 command-signatures/src/generators/paru_tests.rs create mode 100644 command-signatures/src/generators/yay_tests.rs diff --git a/command-signatures/json/paru.json b/command-signatures/json/paru.json index 38f909c4..0e79ebaf 100644 --- a/command-signatures/json/paru.json +++ b/command-signatures/json/paru.json @@ -82,13 +82,6 @@ "args": { "name": "glob" } - }, - { - "name": [ - "-i", - "--install" - ], - "description": "Install package as well as building" } ], "isVariadic": true, @@ -179,13 +172,6 @@ "args": { "name": "glob" } - }, - { - "name": [ - "-i", - "--install" - ], - "description": "Install package as well as building" } ], "isVariadic": true, @@ -1249,6 +1235,44 @@ "description": "Upgrade the chroot" } ] + }, + { + "name": "-B", + "description": "Build a PKGBUILD in a given directory", + "options": [ + { + "name": [ + "-i", + "--install" + ], + "description": "Install package as well as building" + } + ], + "isVariadic": true, + "args": { + "name": "dir", + "template": "folders", + "isOptional": true + } + }, + { + "name": "--build", + "description": "Build a PKGBUILD in a given directory", + "options": [ + { + "name": [ + "-i", + "--install" + ], + "description": "Install package as well as building" + } + ], + "isVariadic": true, + "args": { + "name": "dir", + "template": "folders", + "isOptional": true + } } ], "options": [ diff --git a/command-signatures/json/yay.json b/command-signatures/json/yay.json index 61a21a38..9f383d3b 100644 --- a/command-signatures/json/yay.json +++ b/command-signatures/json/yay.json @@ -82,13 +82,6 @@ "args": { "name": "glob" } - }, - { - "name": [ - "-i", - "--install" - ], - "description": "Install package as well as building" } ], "isVariadic": true, @@ -179,13 +172,6 @@ "args": { "name": "glob" } - }, - { - "name": [ - "-i", - "--install" - ], - "description": "Install package as well as building" } ], "isVariadic": true, @@ -1247,6 +1233,15 @@ { "name": "-B", "description": "Build from a local PKGBUILD", + "options": [ + { + "name": [ + "-i", + "--install" + ], + "description": "Build and install a PKGBUILD in a given directory" + } + ], "args": { "name": "dir", "template": "folders", @@ -1256,6 +1251,15 @@ { "name": "--build", "description": "Build from a local PKGBUILD", + "options": [ + { + "name": [ + "-i", + "--install" + ], + "description": "Build and install a PKGBUILD in a given directory" + } + ], "args": { "name": "dir", "template": "folders", diff --git a/command-signatures/src/generators/paru.rs b/command-signatures/src/generators/paru.rs index 09603a2b..782228c4 100644 --- a/command-signatures/src/generators/paru.rs +++ b/command-signatures/src/generators/paru.rs @@ -1,9 +1,38 @@ use warp_completion_metadata::{ - CommandBuilder, CommandSignatureGenerators, Generator, GeneratorResultsCollector, Suggestion, + CommandBuilder, CommandSignatureGenerators, Generator, GeneratorResults, + GeneratorResultsCollector, Suggestion, }; use super::common::{pacman_installed_packages_generator, pacman_pkg_tar_files_in_cwd_generator}; +/// Parses the space-separated `name source` lines produced by `paru -P/--show -c/--complete`, +/// where `source` is either `AUR` or the (sync or local pkgbuild) repository the package +/// belongs to (e.g. `core`, `extra`). See `repo_list`/`pkgbuild_list`/`aur_list` in paru's +/// `src/completion.rs`, which write `pkg.name()`, `b" "`, `db.name()`, `b"\n"` and +/// `line`, `b" AUR\n"` respectively. +fn parse_package_list(output: &str) -> GeneratorResults { + output + .lines() + .filter_map(|line| { + let mut fields = line.splitn(2, ' '); + let name = fields.next()?.trim(); + if name.is_empty() { + return None; + } + Some( + match fields + .next() + .map(str::trim) + .filter(|source| !source.is_empty()) + { + Some(source) => Suggestion::with_description(name, source), + None => Suggestion::new(name), + }, + ) + }) + .collect_unordered_results() +} + pub fn generator() -> CommandSignatureGenerators { CommandSignatureGenerators::new("paru") .add_generator( @@ -11,24 +40,18 @@ pub fn generator() -> CommandSignatureGenerators { pacman_installed_packages_generator(), ) .add_generator( - // `paru -P/--show -c/--complete` prints AUR and repo package names, one per line, - // for use by shell completion scripts. This is how paru's own official fish - // completions list packages for `-S`/`sync`, unlike plain pacman, which only knows - // about the sync repositories and not the AUR. "list_all_packages", - Generator::script(CommandBuilder::single_command("paru -Pc"), |output| { - let mut targets = Vec::new(); - for package_name in output.lines() { - targets.push(Suggestion::with_description( - package_name.to_string(), - "package", - )); - } - targets.into_iter().collect_unordered_results() - }), + Generator::script( + CommandBuilder::single_command("paru -Pc"), + parse_package_list, + ), ) .add_generator( "list_all_pkg_tar_files_in_cwd", pacman_pkg_tar_files_in_cwd_generator(), ) } + +#[cfg(test)] +#[path = "paru_tests.rs"] +mod tests; diff --git a/command-signatures/src/generators/paru_tests.rs b/command-signatures/src/generators/paru_tests.rs new file mode 100644 index 00000000..a613f603 --- /dev/null +++ b/command-signatures/src/generators/paru_tests.rs @@ -0,0 +1,49 @@ +use super::parse_package_list; + +#[test] +fn test_parses_aur_and_repo_packages() { + let output = "btrfs-progs core\nyay-bin AUR\n"; + let results = parse_package_list(output); + + let suggestions: Vec<(&str, Option<&str>)> = results + .suggestions + .iter() + .map(|suggestion| { + ( + suggestion.exact_string.as_str(), + suggestion.description.as_deref(), + ) + }) + .collect(); + + assert_eq!( + suggestions, + vec![("btrfs-progs", Some("core")), ("yay-bin", Some("AUR"))] + ); +} + +#[test] +fn test_handles_single_field_line_without_panicking() { + // A line with no space-separated source shouldn't panic, and should still surface the + // package name (just without a description). + let output = "btrfs-progs\n"; + let results = parse_package_list(output); + + assert_eq!(results.suggestions.len(), 1); + assert_eq!(results.suggestions[0].exact_string, "btrfs-progs"); + assert_eq!(results.suggestions[0].description, None); +} + +#[test] +fn test_skips_blank_lines_and_lines_with_no_name() { + let output = "\n \nbtrfs-progs core\n"; + let results = parse_package_list(output); + + assert_eq!(results.suggestions.len(), 1); + assert_eq!(results.suggestions[0].exact_string, "btrfs-progs"); +} + +#[test] +fn test_empty_output() { + assert!(parse_package_list("").suggestions.is_empty()); +} diff --git a/command-signatures/src/generators/yay.rs b/command-signatures/src/generators/yay.rs index 3f695585..ddc1ab14 100644 --- a/command-signatures/src/generators/yay.rs +++ b/command-signatures/src/generators/yay.rs @@ -1,9 +1,37 @@ use warp_completion_metadata::{ - CommandBuilder, CommandSignatureGenerators, Generator, GeneratorResultsCollector, Suggestion, + CommandBuilder, CommandSignatureGenerators, Generator, GeneratorResults, + GeneratorResultsCollector, Suggestion, }; use super::common::{pacman_installed_packages_generator, pacman_pkg_tar_files_in_cwd_generator}; +/// Parses the tab-separated `name\tsource` lines produced by `yay -P/--show -c/--complete`, +/// where `source` is either `AUR` or the sync repository the package belongs to (e.g. `core`, +/// `extra`). See `createAURList`/`createRepoList` in yay's `pkg/completion/completion.go`, which +/// write `pkgName+"\tAUR\n"` and `pkg.Name()+"\t"+pkg.DB().Name()+"\n"` respectively. +fn parse_package_list(output: &str) -> GeneratorResults { + output + .lines() + .filter_map(|line| { + let mut fields = line.splitn(2, '\t'); + let name = fields.next()?.trim(); + if name.is_empty() { + return None; + } + Some( + match fields + .next() + .map(str::trim) + .filter(|source| !source.is_empty()) + { + Some(source) => Suggestion::with_description(name, source), + None => Suggestion::new(name), + }, + ) + }) + .collect_unordered_results() +} + pub fn generator() -> CommandSignatureGenerators { CommandSignatureGenerators::new("yay") .add_generator( @@ -11,24 +39,18 @@ pub fn generator() -> CommandSignatureGenerators { pacman_installed_packages_generator(), ) .add_generator( - // `yay -P/--show -c/--complete` prints AUR and repo package names, one per line, - // for use by shell completion scripts. This is how yay's own official fish/bash/zsh - // completions list packages for `-S`/`sync`, unlike plain pacman, which only knows - // about the sync repositories and not the AUR. "list_all_packages", - Generator::script(CommandBuilder::single_command("yay -Pc"), |output| { - let mut targets = Vec::new(); - for package_name in output.lines() { - targets.push(Suggestion::with_description( - package_name.to_string(), - "package", - )); - } - targets.into_iter().collect_unordered_results() - }), + Generator::script( + CommandBuilder::single_command("yay -Pc"), + parse_package_list, + ), ) .add_generator( "list_all_pkg_tar_files_in_cwd", pacman_pkg_tar_files_in_cwd_generator(), ) } + +#[cfg(test)] +#[path = "yay_tests.rs"] +mod tests; diff --git a/command-signatures/src/generators/yay_tests.rs b/command-signatures/src/generators/yay_tests.rs new file mode 100644 index 00000000..83b5c111 --- /dev/null +++ b/command-signatures/src/generators/yay_tests.rs @@ -0,0 +1,49 @@ +use super::parse_package_list; + +#[test] +fn test_parses_aur_and_repo_packages() { + let output = "btrfs-progs\tcore\nyay-bin\tAUR\n"; + let results = parse_package_list(output); + + let suggestions: Vec<(&str, Option<&str>)> = results + .suggestions + .iter() + .map(|suggestion| { + ( + suggestion.exact_string.as_str(), + suggestion.description.as_deref(), + ) + }) + .collect(); + + assert_eq!( + suggestions, + vec![("btrfs-progs", Some("core")), ("yay-bin", Some("AUR"))] + ); +} + +#[test] +fn test_handles_single_field_line_without_panicking() { + // A line with no tab-separated source shouldn't panic, and should still surface the + // package name (just without a description). + let output = "btrfs-progs\n"; + let results = parse_package_list(output); + + assert_eq!(results.suggestions.len(), 1); + assert_eq!(results.suggestions[0].exact_string, "btrfs-progs"); + assert_eq!(results.suggestions[0].description, None); +} + +#[test] +fn test_skips_blank_lines_and_lines_with_no_name() { + let output = "\n\t\nbtrfs-progs\tcore\n"; + let results = parse_package_list(output); + + assert_eq!(results.suggestions.len(), 1); + assert_eq!(results.suggestions[0].exact_string, "btrfs-progs"); +} + +#[test] +fn test_empty_output() { + assert!(parse_package_list("").suggestions.is_empty()); +} From f1f23732b83660ad722413137606290d058ec198 Mon Sep 17 00:00:00 2001 From: Oz Date: Fri, 14 Aug 2026 00:29:36 +0000 Subject: [PATCH 2/2] Inline yay/paru unit tests instead of separate _tests.rs files Matches the repo's dominant convention (git.rs, ip.rs, npm.rs, kubectl.rs, and 8 others use inline #[cfg(test)] mod tests; only 3 generators use a separate file). Moves the contents of yay_tests.rs and paru_tests.rs into inline mod tests blocks at the bottom of yay.rs/paru.rs, deletes both _tests.rs files. Same cases, same assertions, no coverage dropped. Co-Authored-By: Warp Agent --- command-signatures/src/generators/paru.rs | 53 ++++++++++++++++++- .../src/generators/paru_tests.rs | 49 ----------------- command-signatures/src/generators/yay.rs | 53 ++++++++++++++++++- .../src/generators/yay_tests.rs | 49 ----------------- 4 files changed, 102 insertions(+), 102 deletions(-) delete mode 100644 command-signatures/src/generators/paru_tests.rs delete mode 100644 command-signatures/src/generators/yay_tests.rs diff --git a/command-signatures/src/generators/paru.rs b/command-signatures/src/generators/paru.rs index 782228c4..67f7b972 100644 --- a/command-signatures/src/generators/paru.rs +++ b/command-signatures/src/generators/paru.rs @@ -53,5 +53,54 @@ pub fn generator() -> CommandSignatureGenerators { } #[cfg(test)] -#[path = "paru_tests.rs"] -mod tests; +mod tests { + use super::parse_package_list; + + #[test] + fn test_parses_aur_and_repo_packages() { + let output = "btrfs-progs core\nyay-bin AUR\n"; + let results = parse_package_list(output); + + let suggestions: Vec<(&str, Option<&str>)> = results + .suggestions + .iter() + .map(|suggestion| { + ( + suggestion.exact_string.as_str(), + suggestion.description.as_deref(), + ) + }) + .collect(); + + assert_eq!( + suggestions, + vec![("btrfs-progs", Some("core")), ("yay-bin", Some("AUR"))] + ); + } + + #[test] + fn test_handles_single_field_line_without_panicking() { + // A line with no space-separated source shouldn't panic, and should still surface the + // package name (just without a description). + let output = "btrfs-progs\n"; + let results = parse_package_list(output); + + assert_eq!(results.suggestions.len(), 1); + assert_eq!(results.suggestions[0].exact_string, "btrfs-progs"); + assert_eq!(results.suggestions[0].description, None); + } + + #[test] + fn test_skips_blank_lines_and_lines_with_no_name() { + let output = "\n \nbtrfs-progs core\n"; + let results = parse_package_list(output); + + assert_eq!(results.suggestions.len(), 1); + assert_eq!(results.suggestions[0].exact_string, "btrfs-progs"); + } + + #[test] + fn test_empty_output() { + assert!(parse_package_list("").suggestions.is_empty()); + } +} diff --git a/command-signatures/src/generators/paru_tests.rs b/command-signatures/src/generators/paru_tests.rs deleted file mode 100644 index a613f603..00000000 --- a/command-signatures/src/generators/paru_tests.rs +++ /dev/null @@ -1,49 +0,0 @@ -use super::parse_package_list; - -#[test] -fn test_parses_aur_and_repo_packages() { - let output = "btrfs-progs core\nyay-bin AUR\n"; - let results = parse_package_list(output); - - let suggestions: Vec<(&str, Option<&str>)> = results - .suggestions - .iter() - .map(|suggestion| { - ( - suggestion.exact_string.as_str(), - suggestion.description.as_deref(), - ) - }) - .collect(); - - assert_eq!( - suggestions, - vec![("btrfs-progs", Some("core")), ("yay-bin", Some("AUR"))] - ); -} - -#[test] -fn test_handles_single_field_line_without_panicking() { - // A line with no space-separated source shouldn't panic, and should still surface the - // package name (just without a description). - let output = "btrfs-progs\n"; - let results = parse_package_list(output); - - assert_eq!(results.suggestions.len(), 1); - assert_eq!(results.suggestions[0].exact_string, "btrfs-progs"); - assert_eq!(results.suggestions[0].description, None); -} - -#[test] -fn test_skips_blank_lines_and_lines_with_no_name() { - let output = "\n \nbtrfs-progs core\n"; - let results = parse_package_list(output); - - assert_eq!(results.suggestions.len(), 1); - assert_eq!(results.suggestions[0].exact_string, "btrfs-progs"); -} - -#[test] -fn test_empty_output() { - assert!(parse_package_list("").suggestions.is_empty()); -} diff --git a/command-signatures/src/generators/yay.rs b/command-signatures/src/generators/yay.rs index ddc1ab14..8c44ac8e 100644 --- a/command-signatures/src/generators/yay.rs +++ b/command-signatures/src/generators/yay.rs @@ -52,5 +52,54 @@ pub fn generator() -> CommandSignatureGenerators { } #[cfg(test)] -#[path = "yay_tests.rs"] -mod tests; +mod tests { + use super::parse_package_list; + + #[test] + fn test_parses_aur_and_repo_packages() { + let output = "btrfs-progs\tcore\nyay-bin\tAUR\n"; + let results = parse_package_list(output); + + let suggestions: Vec<(&str, Option<&str>)> = results + .suggestions + .iter() + .map(|suggestion| { + ( + suggestion.exact_string.as_str(), + suggestion.description.as_deref(), + ) + }) + .collect(); + + assert_eq!( + suggestions, + vec![("btrfs-progs", Some("core")), ("yay-bin", Some("AUR"))] + ); + } + + #[test] + fn test_handles_single_field_line_without_panicking() { + // A line with no tab-separated source shouldn't panic, and should still surface the + // package name (just without a description). + let output = "btrfs-progs\n"; + let results = parse_package_list(output); + + assert_eq!(results.suggestions.len(), 1); + assert_eq!(results.suggestions[0].exact_string, "btrfs-progs"); + assert_eq!(results.suggestions[0].description, None); + } + + #[test] + fn test_skips_blank_lines_and_lines_with_no_name() { + let output = "\n\t\nbtrfs-progs\tcore\n"; + let results = parse_package_list(output); + + assert_eq!(results.suggestions.len(), 1); + assert_eq!(results.suggestions[0].exact_string, "btrfs-progs"); + } + + #[test] + fn test_empty_output() { + assert!(parse_package_list("").suggestions.is_empty()); + } +} diff --git a/command-signatures/src/generators/yay_tests.rs b/command-signatures/src/generators/yay_tests.rs deleted file mode 100644 index 83b5c111..00000000 --- a/command-signatures/src/generators/yay_tests.rs +++ /dev/null @@ -1,49 +0,0 @@ -use super::parse_package_list; - -#[test] -fn test_parses_aur_and_repo_packages() { - let output = "btrfs-progs\tcore\nyay-bin\tAUR\n"; - let results = parse_package_list(output); - - let suggestions: Vec<(&str, Option<&str>)> = results - .suggestions - .iter() - .map(|suggestion| { - ( - suggestion.exact_string.as_str(), - suggestion.description.as_deref(), - ) - }) - .collect(); - - assert_eq!( - suggestions, - vec![("btrfs-progs", Some("core")), ("yay-bin", Some("AUR"))] - ); -} - -#[test] -fn test_handles_single_field_line_without_panicking() { - // A line with no tab-separated source shouldn't panic, and should still surface the - // package name (just without a description). - let output = "btrfs-progs\n"; - let results = parse_package_list(output); - - assert_eq!(results.suggestions.len(), 1); - assert_eq!(results.suggestions[0].exact_string, "btrfs-progs"); - assert_eq!(results.suggestions[0].description, None); -} - -#[test] -fn test_skips_blank_lines_and_lines_with_no_name() { - let output = "\n\t\nbtrfs-progs\tcore\n"; - let results = parse_package_list(output); - - assert_eq!(results.suggestions.len(), 1); - assert_eq!(results.suggestions[0].exact_string, "btrfs-progs"); -} - -#[test] -fn test_empty_output() { - assert!(parse_package_list("").suggestions.is_empty()); -}