1use serde::{Deserialize, Serialize};
4
5use crate::{client::Opencode, error::OpencodeError};
6
7#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
13pub struct Position {
14 pub character: i64,
16 pub line: i64,
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
22pub struct Range {
23 pub end: Position,
25 pub start: Position,
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
31pub struct SymbolLocation {
32 pub range: Range,
34 pub uri: String,
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
42pub struct SymbolInfo {
43 pub kind: i64,
45 pub location: SymbolLocation,
47 pub name: String,
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
53pub struct TextMatch {
54 pub text: String,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
60pub struct Submatch {
61 pub end: i64,
63 #[serde(rename = "match")]
65 pub match_info: TextMatch,
66 pub start: i64,
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
72pub struct Lines {
73 pub text: String,
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
79pub struct PathInfo {
80 pub text: String,
82}
83
84#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
86pub struct FindTextResponseItem {
87 pub absolute_offset: i64,
89 pub line_number: i64,
91 pub lines: Lines,
93 pub path: PathInfo,
95 pub submatches: Vec<Submatch>,
97}
98
99pub type FindFilesResponse = Vec<String>;
101
102pub type FindSymbolsResponse = Vec<SymbolInfo>;
104
105pub type FindTextResponse = Vec<FindTextResponseItem>;
107
108#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
114pub struct FindFilesParams {
115 pub query: String,
117}
118
119#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
121pub struct FindSymbolsParams {
122 pub query: String,
124}
125
126#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
128pub struct FindTextParams {
129 pub pattern: String,
131}
132
133pub struct FindResource<'a> {
139 client: &'a Opencode,
140}
141
142impl<'a> FindResource<'a> {
143 pub(crate) const fn new(client: &'a Opencode) -> Self {
144 Self { client }
145 }
146
147 pub async fn files(
151 &self,
152 params: &FindFilesParams,
153 ) -> Result<FindFilesResponse, OpencodeError> {
154 self.client.get_with_query("/find/file", Some(params), None).await
155 }
156
157 pub async fn symbols(
161 &self,
162 params: &FindSymbolsParams,
163 ) -> Result<FindSymbolsResponse, OpencodeError> {
164 self.client.get_with_query("/find/symbol", Some(params), None).await
165 }
166
167 pub async fn text(&self, params: &FindTextParams) -> Result<FindTextResponse, OpencodeError> {
171 self.client.get_with_query("/find", Some(params), None).await
172 }
173}
174
175#[cfg(test)]
180mod tests {
181 use serde_json;
182
183 use super::*;
184
185 #[test]
186 fn symbol_info_round_trip() {
187 let symbol = SymbolInfo {
188 kind: 12,
189 location: SymbolLocation {
190 range: Range {
191 end: Position { character: 20, line: 10 },
192 start: Position { character: 5, line: 10 },
193 },
194 uri: "file:///src/main.rs".to_owned(),
195 },
196 name: "my_function".to_owned(),
197 };
198
199 let json = serde_json::to_string(&symbol).unwrap();
200 let deserialized: SymbolInfo = serde_json::from_str(&json).unwrap();
201 assert_eq!(symbol, deserialized);
202
203 let value: serde_json::Value = serde_json::from_str(&json).unwrap();
205 assert_eq!(value["kind"], 12);
206 assert_eq!(value["name"], "my_function");
207 assert_eq!(value["location"]["uri"], "file:///src/main.rs");
208 assert_eq!(value["location"]["range"]["start"]["line"], 10);
209 assert_eq!(value["location"]["range"]["start"]["character"], 5);
210 assert_eq!(value["location"]["range"]["end"]["character"], 20);
211 }
212
213 #[test]
214 fn find_text_response_item_round_trip() {
215 let item = FindTextResponseItem {
216 absolute_offset: 1024,
217 line_number: 42,
218 lines: Lines { text: " let x = 42;".to_owned() },
219 path: PathInfo { text: "src/main.rs".to_owned() },
220 submatches: vec![Submatch {
221 end: 15,
222 match_info: TextMatch { text: "42".to_owned() },
223 start: 13,
224 }],
225 };
226
227 let json = serde_json::to_string(&item).unwrap();
228 let deserialized: FindTextResponseItem = serde_json::from_str(&json).unwrap();
229 assert_eq!(item, deserialized);
230
231 let value: serde_json::Value = serde_json::from_str(&json).unwrap();
233 assert_eq!(value["absolute_offset"], 1024);
234 assert_eq!(value["line_number"], 42);
235 assert_eq!(value["lines"]["text"], " let x = 42;");
236 assert_eq!(value["path"]["text"], "src/main.rs");
237 assert_eq!(value["submatches"][0]["match"]["text"], "42");
238 assert_eq!(value["submatches"][0]["start"], 13);
239 assert_eq!(value["submatches"][0]["end"], 15);
240 }
241
242 #[test]
243 fn find_text_response_item_deserialize_match_field() {
244 let json = r#"{
246 "absolute_offset": 0,
247 "line_number": 1,
248 "lines": { "text": "hello world" },
249 "path": { "text": "test.txt" },
250 "submatches": [{
251 "end": 5,
252 "match": { "text": "hello" },
253 "start": 0
254 }]
255 }"#;
256
257 let item: FindTextResponseItem = serde_json::from_str(json).unwrap();
258 assert_eq!(item.submatches[0].match_info.text, "hello");
259 }
260
261 #[test]
262 fn find_files_params_serialize() {
263 let params = FindFilesParams { query: "main.rs".to_owned() };
264 let json = serde_json::to_string(¶ms).unwrap();
265 let value: serde_json::Value = serde_json::from_str(&json).unwrap();
266 assert_eq!(value["query"], "main.rs");
267 }
268
269 #[test]
270 fn find_symbols_params_serialize() {
271 let params = FindSymbolsParams { query: "MyStruct".to_owned() };
272 let json = serde_json::to_string(¶ms).unwrap();
273 let value: serde_json::Value = serde_json::from_str(&json).unwrap();
274 assert_eq!(value["query"], "MyStruct");
275 }
276
277 #[test]
278 fn find_text_params_serialize() {
279 let params = FindTextParams { pattern: "TODO".to_owned() };
280 let json = serde_json::to_string(¶ms).unwrap();
281 let value: serde_json::Value = serde_json::from_str(&json).unwrap();
282 assert_eq!(value["pattern"], "TODO");
283 }
284
285 #[test]
286 fn find_symbols_response_round_trip() {
287 let response: FindSymbolsResponse = vec![SymbolInfo {
288 kind: 5,
289 location: SymbolLocation {
290 range: Range {
291 end: Position { character: 10, line: 0 },
292 start: Position { character: 0, line: 0 },
293 },
294 uri: "file:///lib.rs".to_owned(),
295 },
296 name: "Foo".to_owned(),
297 }];
298
299 let json = serde_json::to_string(&response).unwrap();
300 let deserialized: FindSymbolsResponse = serde_json::from_str(&json).unwrap();
301 assert_eq!(response, deserialized);
302 }
303
304 #[test]
305 fn find_files_response_round_trip() {
306 let response: FindFilesResponse = vec!["src/main.rs".to_owned(), "src/lib.rs".to_owned()];
307
308 let json = serde_json::to_string(&response).unwrap();
309 let deserialized: FindFilesResponse = serde_json::from_str(&json).unwrap();
310 assert_eq!(response, deserialized);
311 }
312
313 #[test]
316 fn find_text_response_item_empty_submatches() {
317 let item = FindTextResponseItem {
318 absolute_offset: 0,
319 line_number: 1,
320 lines: Lines { text: "no matches here".to_owned() },
321 path: PathInfo { text: "test.txt".to_owned() },
322 submatches: vec![],
323 };
324 let json = serde_json::to_string(&item).unwrap();
325 let deserialized: FindTextResponseItem = serde_json::from_str(&json).unwrap();
326 assert_eq!(item, deserialized);
327 assert!(deserialized.submatches.is_empty());
328 }
329
330 #[test]
331 fn find_text_response_empty_vec() {
332 let response: FindTextResponse = vec![];
333 let json = serde_json::to_string(&response).unwrap();
334 assert_eq!(json, "[]");
335 let deserialized: FindTextResponse = serde_json::from_str(&json).unwrap();
336 assert_eq!(response, deserialized);
337 }
338
339 #[test]
340 fn find_files_response_empty() {
341 let response: FindFilesResponse = vec![];
342 let json = serde_json::to_string(&response).unwrap();
343 assert_eq!(json, "[]");
344 let deserialized: FindFilesResponse = serde_json::from_str(&json).unwrap();
345 assert_eq!(response, deserialized);
346 }
347
348 #[test]
349 fn find_symbols_response_empty() {
350 let response: FindSymbolsResponse = vec![];
351 let json = serde_json::to_string(&response).unwrap();
352 assert_eq!(json, "[]");
353 let deserialized: FindSymbolsResponse = serde_json::from_str(&json).unwrap();
354 assert_eq!(response, deserialized);
355 }
356}