mcp_servers/coding/tools/grep/
common.rs1use 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 Content,
11 #[serde(alias = "files_with_matches")]
13 FilesWithMatches,
14 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 if let Some(max) = self.max_results
64 && self.matches.len() >= max
65 {
66 return Ok(false); }
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, };
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 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}