1pub mod rg;
2
3use anyhow::Result;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum Format {
7 RgJson,
8}
9
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct GroupedItem {
12 pub filepath: String,
13 pub text: String,
14 pub match_count: usize,
15}
16
17pub fn detect_format(input: &str) -> Option<Format> {
18 let first_line = input.lines().find(|line| !line.trim().is_empty())?;
19 let value = serde_json::from_str::<serde_json::Value>(first_line).ok()?;
20 match value.get("type").and_then(|value| value.as_str()) {
21 Some(_) => Some(Format::RgJson),
22 None => None,
23 }
24}
25
26pub fn filename_for(filepath: &str) -> &str {
27 std::path::Path::new(filepath)
28 .file_name()
29 .and_then(|name| name.to_str())
30 .unwrap_or(filepath)
31}
32
33pub fn render_title(
34 title: Option<&str>,
35 title_template: Option<&str>,
36 item: &GroupedItem,
37) -> Result<String> {
38 let template = title_template.unwrap_or("{{match_count}}:{{filepath}}");
39
40 if title_template.is_some() {
41 let rendered = template
42 .replace("{{filepath}}", &item.filepath)
43 .replace("{{filename}}", filename_for(&item.filepath))
44 .replace("{{match_count}}", &item.match_count.to_string());
45 return Ok(rendered);
46 }
47
48 if let Some(title) = title {
49 return Ok(title.to_string());
50 }
51
52 Ok(template
53 .replace("{{filepath}}", &item.filepath)
54 .replace("{{filename}}", filename_for(&item.filepath))
55 .replace("{{match_count}}", &item.match_count.to_string()))
56}
57
58#[cfg(test)]
59mod tests {
60 use super::{detect_format, render_title, Format, GroupedItem};
61
62 fn grouped_item() -> GroupedItem {
63 GroupedItem {
64 filepath: "lib/a.rb".to_string(),
65 text: "1: foo".to_string(),
66 match_count: 2,
67 }
68 }
69
70 #[test]
71 fn test_detect_format_rg_json() {
72 let input = r#"{"type":"begin","data":{"path":{"text":"lib/a.rb"}}}"#;
73 assert_eq!(detect_format(input), Some(Format::RgJson));
74 }
75
76 #[test]
77 fn test_detect_format_unknown() {
78 assert_eq!(detect_format("not json"), None);
79 }
80
81 #[test]
82 fn test_render_title_template_filepath() {
83 assert_eq!(
84 render_title(None, Some("{{filepath}}"), &grouped_item()).unwrap(),
85 "lib/a.rb"
86 );
87 }
88
89 #[test]
90 fn test_render_title_template_filename() {
91 assert_eq!(
92 render_title(None, Some("{{filename}}"), &grouped_item()).unwrap(),
93 "a.rb"
94 );
95 }
96
97 #[test]
98 fn test_render_title_template_match_count() {
99 assert_eq!(
100 render_title(None, Some("matches: {{match_count}}"), &grouped_item()).unwrap(),
101 "matches: 2"
102 );
103 }
104
105 #[test]
106 fn test_render_title_prefers_template_over_title() {
107 assert_eq!(
108 render_title(Some("fallback"), Some("{{filename}}"), &grouped_item()).unwrap(),
109 "a.rb"
110 );
111 }
112
113 #[test]
114 fn test_render_title_uses_literal_title() {
115 assert_eq!(
116 render_title(Some("explicit title"), None, &grouped_item()).unwrap(),
117 "explicit title"
118 );
119 }
120
121 #[test]
122 fn test_render_title_defaults_to_match_count_and_filepath() {
123 assert_eq!(
124 render_title(None, None, &grouped_item()).unwrap(),
125 "2:lib/a.rb"
126 );
127 }
128}