Skip to main content

mcp_servers/coding/tools/grep/
common.rs

1use grep::searcher::{Sink, SinkMatch};
2use schemars::JsonSchema;
3use serde::{Deserialize, Serialize};
4use std::path::Path;
5
6#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
7#[serde(rename_all = "camelCase")]
8pub enum OutputMode {
9    /// Return matching lines with file paths and line numbers
10    Content,
11    /// Return only file paths that contain matches
12    #[serde(alias = "files_with_matches")]
13    FilesWithMatches,
14    /// Return match counts per file
15    Count,
16}
17
18#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
19#[serde(rename_all = "camelCase")]
20pub struct MatchData {
21    pub file: String,
22    pub line_number: Option<usize>,
23    pub line: String,
24    pub before_context: Option<Vec<String>>,
25    pub after_context: Option<Vec<String>>,
26}
27
28pub struct MatchCollectorSink {
29    file_path: std::path::PathBuf,
30    line_numbers: bool,
31    max_results: Option<usize>,
32    pub matches: Vec<MatchData>,
33    context_before: Vec<String>,
34}
35
36impl MatchCollectorSink {
37    pub fn new(file_path: &Path, line_numbers: bool) -> Self {
38        Self {
39            file_path: file_path.to_path_buf(),
40            line_numbers,
41            max_results: None,
42            matches: Vec::new(),
43            context_before: Vec::new(),
44        }
45    }
46
47    pub fn with_max_results(file_path: &Path, line_numbers: bool, max_results: Option<usize>) -> Self {
48        Self {
49            file_path: file_path.to_path_buf(),
50            line_numbers,
51            max_results,
52            matches: Vec::new(),
53            context_before: Vec::new(),
54        }
55    }
56}
57
58impl Sink for MatchCollectorSink {
59    type Error = std::io::Error;
60
61    fn matched(&mut self, _searcher: &grep::searcher::Searcher, mat: &SinkMatch<'_>) -> Result<bool, Self::Error> {
62        // Check if we've hit the max results limit
63        if let Some(max) = self.max_results
64            && self.matches.len() >= max
65        {
66            return Ok(false); // Stop searching
67        }
68
69        let line_str = std::str::from_utf8(mat.bytes()).unwrap_or("<invalid utf8>");
70
71        let before_context = if self.context_before.is_empty() { None } else { Some(self.context_before.clone()) };
72
73        let match_data = MatchData {
74            file: self.file_path.display().to_string(),
75            line_number: if self.line_numbers { mat.line_number().and_then(|n| usize::try_from(n).ok()) } else { None },
76            line: line_str.to_string(),
77            before_context,
78            after_context: None, // Will be filled by context method
79        };
80
81        self.matches.push(match_data);
82        self.context_before.clear();
83        Ok(true)
84    }
85
86    fn context(
87        &mut self,
88        _searcher: &grep::searcher::Searcher,
89        ctx: &grep::searcher::SinkContext<'_>,
90    ) -> Result<bool, Self::Error> {
91        let line_str = std::str::from_utf8(ctx.bytes()).unwrap_or("<invalid utf8>");
92
93        match ctx.kind() {
94            grep::searcher::SinkContextKind::Before => {
95                self.context_before.push(line_str.to_string());
96            }
97            grep::searcher::SinkContextKind::After => {
98                // Add after context to the last match
99                if let Some(last_match) = self.matches.last_mut() {
100                    if last_match.after_context.is_none() {
101                        last_match.after_context = Some(Vec::new());
102                    }
103                    if let Some(ref mut after) = last_match.after_context {
104                        after.push(line_str.to_string());
105                    }
106                }
107            }
108            grep::searcher::SinkContextKind::Other => {}
109        }
110
111        Ok(true)
112    }
113}
114
115pub struct HasMatchSink {
116    pub has_match: bool,
117}
118
119impl HasMatchSink {
120    pub fn new() -> Self {
121        Self { has_match: false }
122    }
123}
124
125impl Default for HasMatchSink {
126    fn default() -> Self {
127        Self::new()
128    }
129}
130
131impl Sink for HasMatchSink {
132    type Error = std::io::Error;
133
134    fn matched(&mut self, _searcher: &grep::searcher::Searcher, _mat: &SinkMatch<'_>) -> Result<bool, Self::Error> {
135        self.has_match = true;
136        Ok(false)
137    }
138}
139
140pub struct CountSink {
141    pub count: usize,
142}
143
144impl CountSink {
145    pub fn new() -> Self {
146        Self { count: 0 }
147    }
148}
149
150impl Default for CountSink {
151    fn default() -> Self {
152        Self::new()
153    }
154}
155
156impl Sink for CountSink {
157    type Error = std::io::Error;
158
159    fn matched(&mut self, _searcher: &grep::searcher::Searcher, _mat: &SinkMatch<'_>) -> Result<bool, Self::Error> {
160        self.count += 1;
161        Ok(true)
162    }
163}