elio 1.4.0

Snappy, batteries-included terminal file manager with rich previews, inline images, bulk actions, and trash support.
Documentation
use super::backends::{custom, plain, syntect};
use crate::file_info::{CodeBackend, PreviewSpec};
use ratatui::text::Line;

pub(crate) fn render_code_preview<F>(
    spec: PreviewSpec,
    text: &str,
    line_numbers: bool,
    line_limit: usize,
    canceled: &F,
) -> Vec<Line<'static>>
where
    F: Fn() -> bool,
{
    match spec.code_backend {
        CodeBackend::Plain => {
            plain::render_plain_code_preview(text, line_numbers, line_limit, canceled)
        }
        CodeBackend::Custom(kind) => {
            custom::render_custom_code_preview(kind, text, line_numbers, line_limit, canceled)
        }
        CodeBackend::Syntect => {
            render_syntect_with_fallback(spec, text, line_numbers, line_limit, canceled)
        }
    }
}

fn render_syntect_with_fallback<F>(
    spec: PreviewSpec,
    text: &str,
    line_numbers: bool,
    line_limit: usize,
    canceled: &F,
) -> Vec<Line<'static>>
where
    F: Fn() -> bool,
{
    let Some(code_syntax) = spec.code_syntax else {
        return plain::render_plain_code_preview(text, line_numbers, line_limit, canceled);
    };

    if !syntect::is_enabled(code_syntax) {
        return plain::render_plain_code_preview(text, line_numbers, line_limit, canceled);
    }

    syntect::render_syntect_code_preview(code_syntax, text, line_numbers, line_limit, canceled)
        .unwrap_or_else(|_| {
            plain::render_plain_code_preview(text, line_numbers, line_limit, canceled)
        })
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::file_info::{CodeBackend, CustomCodeKind, PreviewKind, PreviewSpec};
    use ratatui::text::Line;
    use std::cell::Cell;

    fn line_text(line: &Line<'_>) -> String {
        line.spans
            .iter()
            .map(|span| span.content.as_ref())
            .collect::<String>()
    }

    fn line_texts(lines: &[Line<'_>]) -> Vec<String> {
        lines.iter().map(line_text).collect()
    }

    #[test]
    fn enabled_javascript_preview_specs_use_syntect() {
        let preview = render_code_preview(
            PreviewSpec::code("javascript", CodeBackend::Syntect, None),
            "const value = 1;\n",
            true,
            20,
            &|| false,
        );
        let expected = syntect::render_syntect_code_preview(
            "javascript",
            "const value = 1;\n",
            true,
            20,
            &|| false,
        )
        .expect("javascript should render through syntect");

        assert_eq!(preview, expected);
    }

    #[test]
    fn enabled_typescript_family_uses_syntect_aliases() {
        let preview = render_code_preview(
            PreviewSpec::code("tsx", CodeBackend::Syntect, None),
            "export function App() { return <div>Hello</div>; }\n",
            true,
            20,
            &|| false,
        );
        let expected = syntect::render_syntect_code_preview(
            "tsx",
            "export function App() { return <div>Hello</div>; }\n",
            true,
            20,
            &|| false,
        )
        .expect("tsx should render through syntect");

        assert_eq!(preview, expected);
    }

    #[test]
    fn enabled_generic_source_preview_specs_use_syntect() {
        for (code_syntax, text) in [
            ("rust", "fn main() {}\n"),
            ("go", "package main\nfunc main() {}\n"),
            ("c", "#include <stdio.h>\nint main(void) { return 0; }\n"),
            (
                "cpp",
                "#include <iostream>\nint main() { std::cout << \"hi\"; }\n",
            ),
            (
                "cs",
                "public class Greeter { public string Greet(string name) => name; }\n",
            ),
            (
                "java",
                "class Main {\n    public static void main(String[] args) {}\n}\n",
            ),
            (
                "dart",
                "class Greeter { String greet(String name) => name; }\n",
            ),
            (
                "zig",
                "const std = @import(\"std\");\npub fn main() void {}\n",
            ),
            ("php", "<?php echo \"hi\";\n"),
            (
                "swift",
                "struct Greeter { func greet(name: String) -> String { name } }\n",
            ),
            (
                "kotlin",
                "class Greeter { fun greet(name: String): String = name }\n",
            ),
            (
                "elixir",
                "defmodule Greeter do\n  def greet(name), do: \"hi #{name}\"\nend\n",
            ),
            (
                "powershell",
                "function Invoke-Greeting([string]$Name) { Write-Host \"Hello $Name\" }\n",
            ),
            ("python", "class Greeter:\n    pass\n"),
            ("ruby", "class Greeter\nend\n"),
            ("lua", "local name = \"elio\"\n"),
            ("make", "build:\n\tcc main.c\n"),
            ("bash", "export PATH=\"$HOME/bin:$PATH\"\n"),
            ("html", "<div class=\"app\">elio</div>\n"),
            ("xml", "<layout id=\"main\" />\n"),
            ("css", ".app { color: #fff; }\n"),
            ("scss", "$fg: #fff;\n.button { color: $fg; }\n"),
            ("sass", "$fg: #fff\n.button\n  color: $fg\n"),
            ("less", "@fg: #fff;\n.button { color: @fg; }\n"),
            (
                "nix",
                "{ description = \"elio\"; outputs = { self }: { packages.default = self; }; }\n",
            ),
            (
                "cmake",
                "cmake_minimum_required(VERSION 3.28)\nproject(elio)\n",
            ),
            (
                "qml",
                "import QtQuick\nItem {\n  property bool active: true\n  onActiveChanged: console.log(\"changed\")\n}\n",
            ),
        ] {
            let preview = render_code_preview(
                PreviewSpec::code(code_syntax, CodeBackend::Syntect, None),
                text,
                true,
                20,
                &|| false,
            );
            let expected =
                syntect::render_syntect_code_preview(code_syntax, text, true, 20, &|| false)
                    .expect("{code_syntax} should render through syntect");

            assert_eq!(preview, expected, "expected {code_syntax} to use syntect");
        }
    }

    #[test]
    fn syntect_renderer_returns_error_for_unknown_syntax() {
        assert!(
            syntect::render_syntect_code_preview(
                "totally-unknown-syntax",
                "hello\n",
                true,
                20,
                &|| false,
            )
            .is_err()
        );
    }

    #[test]
    fn unsupported_syntect_specs_still_fall_back_to_plain_rendering() {
        let preview = render_code_preview(
            PreviewSpec::code("totally-unknown-syntax", CodeBackend::Syntect, None),
            "still plain\n",
            true,
            20,
            &|| false,
        );
        let expected = plain::render_plain_code_preview("still plain\n", true, 20, &|| false);

        assert_eq!(preview, expected);
    }

    #[test]
    fn custom_preview_specs_use_custom_backend() {
        let preview = render_code_preview(
            PreviewSpec::code("jsonc", CodeBackend::Custom(CustomCodeKind::Jsonc), None),
            "{\n  // comment\n}\n",
            true,
            20,
            &|| false,
        );
        let expected = crate::preview::code::custom::render_custom_code_preview(
            CustomCodeKind::Jsonc,
            "{\n  // comment\n}\n",
            true,
            20,
            &|| false,
        );

        assert_eq!(preview, expected);
    }

    #[test]
    fn unknown_syntect_preview_specs_fall_back_to_plain_text() {
        let text = "first()\nsecond()\n";
        let preview = render_code_preview(
            PreviewSpec::code("totally-unknown-syntax", CodeBackend::Syntect, None),
            text,
            true,
            20,
            &|| false,
        );
        let expected = plain::render_plain_code_preview(text, true, 20, &|| false);

        assert_eq!(preview, expected);
        assert_eq!(
            line_texts(&preview),
            vec!["  1 first()".to_string(), "  2 second()".to_string()]
        );
    }

    #[test]
    fn missing_syntect_code_syntax_falls_back_to_plain_text() {
        let text = "plain text\nstill visible\n";
        let preview = render_code_preview(
            PreviewSpec {
                kind: PreviewKind::Source,
                language_hint: Some("unknown"),
                code_syntax: None,
                code_backend: CodeBackend::Syntect,
                structured_format: None,
                document_format: None,
            },
            text,
            true,
            20,
            &|| false,
        );
        let expected = plain::render_plain_code_preview(text, true, 20, &|| false);

        assert_eq!(preview, expected);
    }

    #[test]
    fn golden_code_previews_keep_expected_text_layout_across_backends() {
        let syntect_preview = render_code_preview(
            PreviewSpec::code("rust", CodeBackend::Syntect, None),
            "fn main() {\n    println!(\"hi\");\n}\n",
            true,
            20,
            &|| false,
        );
        assert_eq!(
            line_texts(&syntect_preview),
            vec![
                "  1 fn main() {".to_string(),
                "  2     println!(\"hi\");".to_string(),
                "  3 }".to_string(),
            ]
        );

        let custom_preview = render_code_preview(
            PreviewSpec::code("jsonc", CodeBackend::Custom(CustomCodeKind::Jsonc), None),
            "{\n  // keep me\n  \"name\": \"elio\"\n}\n",
            true,
            20,
            &|| false,
        );
        assert_eq!(
            line_texts(&custom_preview),
            vec![
                "  1 {".to_string(),
                "  2   // keep me".to_string(),
                "  3   \"name\": \"elio\"".to_string(),
                "  4 }".to_string(),
            ]
        );
    }

    #[test]
    fn renderer_respects_line_limit_before_backend_dispatch() {
        let syntect_preview = render_code_preview(
            PreviewSpec::code("rust", CodeBackend::Syntect, None),
            "fn one() {}\nfn two() {}\nfn three() {}\n",
            true,
            2,
            &|| false,
        );
        assert_eq!(
            line_texts(&syntect_preview),
            vec!["  1 fn one() {}".to_string(), "  2 fn two() {}".to_string()]
        );

        let custom_preview = render_code_preview(
            PreviewSpec::code("jsonc", CodeBackend::Custom(CustomCodeKind::Jsonc), None),
            "{\n  // first\n  // second\n}\n",
            true,
            2,
            &|| false,
        );
        assert_eq!(
            line_texts(&custom_preview),
            vec!["  1 {".to_string(), "  2   // first".to_string()]
        );
    }

    #[test]
    fn renderer_stops_on_cancellation_without_empty_placeholder() {
        let calls = Cell::new(0usize);
        let preview = render_code_preview(
            PreviewSpec::code("rust", CodeBackend::Syntect, None),
            "fn first() {}\nfn second() {}\nfn third() {}\n",
            true,
            20,
            &|| {
                let current = calls.get();
                calls.set(current + 1);
                current >= 1
            },
        );

        assert_eq!(line_texts(&preview), vec!["  1 fn first() {}".to_string()]);

        let canceled_immediately = render_code_preview(
            PreviewSpec::code("rust", CodeBackend::Syntect, None),
            "fn hidden() {}\n",
            true,
            20,
            &|| true,
        );
        assert!(canceled_immediately.is_empty());
    }
}