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}