Skip to main content

leta_fs/
text.rs

1use std::path::Path;
2
3use fastrace::trace;
4use thiserror::Error;
5
6#[derive(Error, Debug)]
7pub enum TextError {
8    #[error("IO error: {0}")]
9    Io(#[from] std::io::Error),
10    #[error("UTF-8 decoding error: {0}")]
11    Utf8(#[from] std::string::FromUtf8Error),
12}
13
14pub fn get_language_id(path: &Path) -> &'static str {
15    let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
16    let filename = path.file_name().and_then(|f| f.to_str()).unwrap_or("");
17
18    match ext {
19        "py" | "pyi" => "python",
20        "rs" => "rust",
21        "ts" => "typescript",
22        "tsx" => "typescriptreact",
23        "js" => "javascript",
24        "jsx" => "javascriptreact",
25        "go" => "go",
26        "c" | "h" => "c",
27        "cpp" | "hpp" | "cc" | "cxx" | "hxx" => "cpp",
28        "java" => "java",
29        "rb" | "rake" => "ruby",
30        "php" | "phtml" => "php",
31        "ex" | "exs" => "elixir",
32        "hs" => "haskell",
33        "ml" | "mli" => "ocaml",
34        "lua" => "lua",
35        "zig" => "zig",
36        "yaml" | "yml" => "yaml",
37        "json" => "json",
38        "html" | "htm" => "html",
39        "css" => "css",
40        "scss" => "scss",
41        "less" => "less",
42        "md" | "markdown" => "markdown",
43        "toml" => "toml",
44        "xml" => "xml",
45        "sh" | "bash" => "shellscript",
46        "sql" => "sql",
47        "dummy-doesnt-exist" => "dummy-doesnt-exist",
48        _ => match filename {
49            "Gemfile" | "Rakefile" => "ruby",
50            "Makefile" | "makefile" | "GNUmakefile" => "makefile",
51            "Dockerfile" => "dockerfile",
52            _ => "plaintext",
53        },
54    }
55}
56
57pub fn read_file_content(path: &Path) -> Result<String, TextError> {
58    let bytes = std::fs::read(path)?;
59    let content = String::from_utf8(bytes)?;
60    Ok(content)
61}
62
63pub fn file_mtime(path: &Path) -> String {
64    match std::fs::metadata(path) {
65        Ok(meta) => match meta.modified() {
66            Ok(mtime) => match mtime.duration_since(std::time::UNIX_EPOCH) {
67                Ok(duration) => format!("{}.{}", duration.as_secs(), duration.subsec_nanos()),
68                Err(_) => String::new(),
69            },
70            Err(_) => String::new(),
71        },
72        Err(_) => String::new(),
73    }
74}
75
76#[trace]
77pub fn get_lines_around(
78    content: &str,
79    center_line: usize,
80    context: usize,
81) -> (Vec<String>, usize, usize) {
82    let lines: Vec<&str> = content.lines().collect();
83    let total = lines.len();
84
85    if total == 0 {
86        return (vec![], 0, 0);
87    }
88
89    let center = center_line.min(total.saturating_sub(1));
90    let start = center.saturating_sub(context);
91    let end = (center + context).min(total.saturating_sub(1));
92
93    let extracted: Vec<String> = lines[start..=end].iter().map(|s| s.to_string()).collect();
94    (extracted, start, end)
95}
96
97#[trace]
98pub fn count_lines(content: &str) -> usize {
99    if content.is_empty() {
100        0
101    } else {
102        content.lines().count()
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    #[test]
111    fn test_language_detection() {
112        assert_eq!(get_language_id(Path::new("test.py")), "python");
113        assert_eq!(get_language_id(Path::new("test.rs")), "rust");
114        assert_eq!(get_language_id(Path::new("test.go")), "go");
115        assert_eq!(get_language_id(Path::new("test.ts")), "typescript");
116        assert_eq!(get_language_id(Path::new("Gemfile")), "ruby");
117    }
118
119    #[test]
120    fn test_get_lines_around() {
121        let content = "line0\nline1\nline2\nline3\nline4";
122        let (lines, start, end) = get_lines_around(content, 2, 1);
123        assert_eq!(lines, vec!["line1", "line2", "line3"]);
124        assert_eq!(start, 1);
125        assert_eq!(end, 3);
126    }
127}