use serde::{Deserialize, Serialize};
use crate::{client::Opencode, error::OpencodeError};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Position {
pub character: i64,
pub line: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Range {
pub end: Position,
pub start: Position,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SymbolLocation {
pub range: Range,
pub uri: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SymbolInfo {
pub kind: i64,
pub location: SymbolLocation,
pub name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TextMatch {
pub text: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Submatch {
pub end: i64,
#[serde(rename = "match")]
pub match_info: TextMatch,
pub start: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Lines {
pub text: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct PathInfo {
pub text: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct FindTextResponseItem {
pub absolute_offset: i64,
pub line_number: i64,
pub lines: Lines,
pub path: PathInfo,
pub submatches: Vec<Submatch>,
}
pub type FindFilesResponse = Vec<String>;
pub type FindSymbolsResponse = Vec<SymbolInfo>;
pub type FindTextResponse = Vec<FindTextResponseItem>;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct FindFilesParams {
pub query: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct FindSymbolsParams {
pub query: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct FindTextParams {
pub pattern: String,
}
pub struct FindResource<'a> {
client: &'a Opencode,
}
impl<'a> FindResource<'a> {
pub(crate) const fn new(client: &'a Opencode) -> Self {
Self { client }
}
pub async fn files(
&self,
params: &FindFilesParams,
) -> Result<FindFilesResponse, OpencodeError> {
self.client.get_with_query("/find/file", Some(params), None).await
}
pub async fn symbols(
&self,
params: &FindSymbolsParams,
) -> Result<FindSymbolsResponse, OpencodeError> {
self.client.get_with_query("/find/symbol", Some(params), None).await
}
pub async fn text(&self, params: &FindTextParams) -> Result<FindTextResponse, OpencodeError> {
self.client.get_with_query("/find", Some(params), None).await
}
}
#[cfg(test)]
mod tests {
use serde_json;
use super::*;
#[test]
fn symbol_info_round_trip() {
let symbol = SymbolInfo {
kind: 12,
location: SymbolLocation {
range: Range {
end: Position { character: 20, line: 10 },
start: Position { character: 5, line: 10 },
},
uri: "file:///src/main.rs".to_owned(),
},
name: "my_function".to_owned(),
};
let json = serde_json::to_string(&symbol).unwrap();
let deserialized: SymbolInfo = serde_json::from_str(&json).unwrap();
assert_eq!(symbol, deserialized);
let value: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(value["kind"], 12);
assert_eq!(value["name"], "my_function");
assert_eq!(value["location"]["uri"], "file:///src/main.rs");
assert_eq!(value["location"]["range"]["start"]["line"], 10);
assert_eq!(value["location"]["range"]["start"]["character"], 5);
assert_eq!(value["location"]["range"]["end"]["character"], 20);
}
#[test]
fn find_text_response_item_round_trip() {
let item = FindTextResponseItem {
absolute_offset: 1024,
line_number: 42,
lines: Lines { text: " let x = 42;".to_owned() },
path: PathInfo { text: "src/main.rs".to_owned() },
submatches: vec![Submatch {
end: 15,
match_info: TextMatch { text: "42".to_owned() },
start: 13,
}],
};
let json = serde_json::to_string(&item).unwrap();
let deserialized: FindTextResponseItem = serde_json::from_str(&json).unwrap();
assert_eq!(item, deserialized);
let value: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(value["absolute_offset"], 1024);
assert_eq!(value["line_number"], 42);
assert_eq!(value["lines"]["text"], " let x = 42;");
assert_eq!(value["path"]["text"], "src/main.rs");
assert_eq!(value["submatches"][0]["match"]["text"], "42");
assert_eq!(value["submatches"][0]["start"], 13);
assert_eq!(value["submatches"][0]["end"], 15);
}
#[test]
fn find_text_response_item_deserialize_match_field() {
let json = r#"{
"absolute_offset": 0,
"line_number": 1,
"lines": { "text": "hello world" },
"path": { "text": "test.txt" },
"submatches": [{
"end": 5,
"match": { "text": "hello" },
"start": 0
}]
}"#;
let item: FindTextResponseItem = serde_json::from_str(json).unwrap();
assert_eq!(item.submatches[0].match_info.text, "hello");
}
#[test]
fn find_files_params_serialize() {
let params = FindFilesParams { query: "main.rs".to_owned() };
let json = serde_json::to_string(¶ms).unwrap();
let value: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(value["query"], "main.rs");
}
#[test]
fn find_symbols_params_serialize() {
let params = FindSymbolsParams { query: "MyStruct".to_owned() };
let json = serde_json::to_string(¶ms).unwrap();
let value: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(value["query"], "MyStruct");
}
#[test]
fn find_text_params_serialize() {
let params = FindTextParams { pattern: "TODO".to_owned() };
let json = serde_json::to_string(¶ms).unwrap();
let value: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(value["pattern"], "TODO");
}
#[test]
fn find_symbols_response_round_trip() {
let response: FindSymbolsResponse = vec![SymbolInfo {
kind: 5,
location: SymbolLocation {
range: Range {
end: Position { character: 10, line: 0 },
start: Position { character: 0, line: 0 },
},
uri: "file:///lib.rs".to_owned(),
},
name: "Foo".to_owned(),
}];
let json = serde_json::to_string(&response).unwrap();
let deserialized: FindSymbolsResponse = serde_json::from_str(&json).unwrap();
assert_eq!(response, deserialized);
}
#[test]
fn find_files_response_round_trip() {
let response: FindFilesResponse = vec!["src/main.rs".to_owned(), "src/lib.rs".to_owned()];
let json = serde_json::to_string(&response).unwrap();
let deserialized: FindFilesResponse = serde_json::from_str(&json).unwrap();
assert_eq!(response, deserialized);
}
#[test]
fn find_text_response_item_empty_submatches() {
let item = FindTextResponseItem {
absolute_offset: 0,
line_number: 1,
lines: Lines { text: "no matches here".to_owned() },
path: PathInfo { text: "test.txt".to_owned() },
submatches: vec![],
};
let json = serde_json::to_string(&item).unwrap();
let deserialized: FindTextResponseItem = serde_json::from_str(&json).unwrap();
assert_eq!(item, deserialized);
assert!(deserialized.submatches.is_empty());
}
#[test]
fn find_text_response_empty_vec() {
let response: FindTextResponse = vec![];
let json = serde_json::to_string(&response).unwrap();
assert_eq!(json, "[]");
let deserialized: FindTextResponse = serde_json::from_str(&json).unwrap();
assert_eq!(response, deserialized);
}
#[test]
fn find_files_response_empty() {
let response: FindFilesResponse = vec![];
let json = serde_json::to_string(&response).unwrap();
assert_eq!(json, "[]");
let deserialized: FindFilesResponse = serde_json::from_str(&json).unwrap();
assert_eq!(response, deserialized);
}
#[test]
fn find_symbols_response_empty() {
let response: FindSymbolsResponse = vec![];
let json = serde_json::to_string(&response).unwrap();
assert_eq!(json, "[]");
let deserialized: FindSymbolsResponse = serde_json::from_str(&json).unwrap();
assert_eq!(response, deserialized);
}
}