From 71e76b34e6401c70627484760494e657813b4075 Mon Sep 17 00:00:00 2001 From: EmojiPati <49492351+EmojiPati@users.noreply.github.com> Date: Mon, 20 Apr 2026 07:23:31 +0300 Subject: [PATCH] fix(completer): wrap file preview in `sh -c` for fish compatibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fzf dispatches `--preview` commands through `$SHELL`, so when the user's login shell is fish the `@`-completion preview fails with "fish: Missing end to balance this if statement" — fish cannot parse the POSIX `if [ … ]; then … fi` body. Wrapping the script in `sh -c` makes the preview render under any non-POSIX login shell. --- .../src/completer/input_completer.rs | 57 +++++++++++++++---- 1 file changed, 45 insertions(+), 12 deletions(-) diff --git a/crates/forge_main/src/completer/input_completer.rs b/crates/forge_main/src/completer/input_completer.rs index 9f81a661dc..35657ee0e9 100644 --- a/crates/forge_main/src/completer/input_completer.rs +++ b/crates/forge_main/src/completer/input_completer.rs @@ -40,18 +40,7 @@ impl Completer for InputCompleter { .map(|file| file.path) .collect(); - // Preview command: show directory listing for dirs, file contents for files. - // {2} references the path column (items are formatted as "{idx}\t{path}"). - // Use bat for syntax-highlighted file previews when available, falling back - // to cat. Mirrors the shell plugin's _FORGE_CAT_CMD and completion.zsh preview. - let cat_cmd = if which_bat() { - "bat --color=always --style=numbers,changes --line-range=:500" - } else { - "cat" - }; - let preview_cmd = format!( - "if [ -d {{2}} ]; then ls -la --color=always {{2}} 2>/dev/null || ls -la {{2}}; else {cat_cmd} {{2}}; fi" - ); + let preview_cmd = build_preview_cmd(cat_cmd_for_preview()); let mut builder = ForgeWidget::select("File", files) .with_preview(preview_cmd) @@ -87,3 +76,47 @@ fn which_bat() -> bool { .map(|o| o.status.success()) .unwrap_or(false) } + +/// Prefers `bat` for syntax-highlighted output when it is on `$PATH`, falling +/// back to plain `cat`. Mirrors the shell plugin's `_FORGE_CAT_CMD`. +fn cat_cmd_for_preview() -> &'static str { + if which_bat() { + "bat --color=always --style=numbers,changes --line-range=:500" + } else { + "cat" + } +} + +/// Builds the fzf `--preview` command as `sh -c '…' _ {2}`. +/// +/// Wrapping in `sh -c` is required because fzf dispatches previews through +/// `$SHELL`, and shells like fish cannot parse the POSIX `if/then/fi` body. +/// `{2}` is fzf's substitution for the tab-separated path column. +fn build_preview_cmd(cat_cmd: &str) -> String { + format!( + r#"sh -c 'if [ -d "$1" ]; then ls -la --color=always "$1" 2>/dev/null || ls -la "$1"; else {cat_cmd} "$1"; fi' _ {{2}}"# + ) +} + +#[cfg(test)] +mod tests { + use pretty_assertions::assert_eq; + + use super::*; + + #[test] + fn test_build_preview_cmd_with_cat() { + let fixture = "cat"; + let actual = build_preview_cmd(fixture); + let expected = r#"sh -c 'if [ -d "$1" ]; then ls -la --color=always "$1" 2>/dev/null || ls -la "$1"; else cat "$1"; fi' _ {2}"#; + assert_eq!(actual, expected); + } + + #[test] + fn test_build_preview_cmd_with_bat() { + let fixture = "bat --color=always --style=numbers,changes --line-range=:500"; + let actual = build_preview_cmd(fixture); + let expected = r#"sh -c 'if [ -d "$1" ]; then ls -la --color=always "$1" 2>/dev/null || ls -la "$1"; else bat --color=always --style=numbers,changes --line-range=:500 "$1"; fi' _ {2}"#; + assert_eq!(actual, expected); + } +}