searchfox_lib/
field_layout.rs1use crate::client::SearchfoxClient;
2use crate::types::SearchfoxResponse;
3use anyhow::Result;
4use reqwest::Url;
5use serde_json;
6use tabled::{
7 settings::{object::Rows, Color, Modify, Style},
8 Table, Tabled,
9};
10
11pub struct FieldLayoutQuery {
12 pub class_name: String,
13}
14
15#[derive(Tabled)]
16struct BaseClass {
17 offset: u64,
18 size: u64,
19 #[tabled(rename = "type")]
20 base_type: String,
21}
22
23#[derive(Tabled)]
24struct Field {
25 offset: u64,
26 size: u64,
27 #[tabled(rename = "type")]
28 field_type: String,
29 name: String,
30}
31
32fn wrap_cpp_type(type_str: &str, max_width: usize) -> String {
33 if type_str.len() <= max_width {
34 return type_str.to_string();
35 }
36
37 let mut result = String::new();
38 let mut current_line = String::new();
39 let mut depth = 0;
40 let mut i = 0;
41 let chars: Vec<char> = type_str.chars().collect();
42
43 while i < chars.len() {
44 let ch = chars[i];
45
46 match ch {
47 '<' => {
48 current_line.push(ch);
49 depth += 1;
50 if current_line.len() > max_width && depth == 1 {
51 result.push_str(¤t_line);
52 result.push('\n');
53 current_line.clear();
54 current_line.push_str(&" ".repeat(depth));
55 }
56 }
57 '>' => {
58 current_line.push(ch);
59 depth = depth.saturating_sub(1);
60 }
61 ',' => {
62 current_line.push(ch);
63 if i + 1 < chars.len() && chars[i + 1] == ' ' {
64 i += 1;
65 }
66 if depth > 0 && current_line.len() > max_width / 2 {
67 result.push_str(current_line.trim_end());
68 result.push('\n');
69 current_line.clear();
70 current_line.push_str(&" ".repeat(depth));
71 } else {
72 current_line.push(' ');
73 }
74 }
75 _ => {
76 current_line.push(ch);
77 }
78 }
79
80 if current_line.len() > max_width && !current_line.trim().is_empty() && depth > 0 {
81 if let Some(last_space) = current_line.rfind(' ') {
82 if last_space > max_width / 2 {
83 let (left, right) = current_line.split_at(last_space);
84 result.push_str(left.trim_end());
85 result.push('\n');
86 current_line = format!("{}{}", " ".repeat(depth), right.trim_start());
87 }
88 }
89 }
90
91 i += 1;
92 }
93
94 if !current_line.is_empty() {
95 result.push_str(¤t_line);
96 }
97
98 result
99}
100
101pub fn format_field_layout(class_name: &str, json: &serde_json::Value) -> String {
102 let mut output = String::new();
103 output.push_str(&format!("Field Layout: {}\n\n", class_name));
104
105 let terminal_width = terminal_size::terminal_size()
106 .map(|(w, _)| w.0 as usize)
107 .unwrap_or(100);
108
109 let type_col_max_width = (terminal_width.saturating_sub(40)).clamp(30, 60);
110
111 let symbol_key = format!("T_{}", class_name);
112
113 let mut found = false;
114
115 if let Some(tables) = json
116 .get("SymbolTreeTableList")
117 .and_then(|v| v.get("tables"))
118 .and_then(|v| v.as_array())
119 {
120 for table in tables {
121 if let Some(jumprefs) = table.get("jumprefs").and_then(|v| v.as_object()) {
122 if let Some(symbol_info) = jumprefs.get(&symbol_key) {
123 found = true;
124
125 let meta = if let Some(variants) = symbol_info
126 .get("meta")
127 .and_then(|m| m.get("variants"))
128 .and_then(|v| v.as_array())
129 {
130 variants.first()
131 } else {
132 symbol_info.get("meta")
133 };
134
135 if let Some(meta_obj) = meta {
136 if let Some(size) = meta_obj.get("sizeBytes").and_then(|v| v.as_u64()) {
137 output.push_str(&format!("Size: {} bytes", size));
138 }
139
140 if let Some(alignment) =
141 meta_obj.get("alignmentBytes").and_then(|v| v.as_u64())
142 {
143 output.push_str(&format!(", Alignment: {} bytes\n\n", alignment));
144 } else {
145 output.push_str("\n\n");
146 }
147
148 if let Some(supers) = meta_obj.get("supers").and_then(|v| v.as_array()) {
149 if !supers.is_empty() {
150 let mut base_classes = Vec::new();
151
152 for base in supers {
153 if let Some(base_obj) = base.as_object() {
154 let offset = base_obj
155 .get("offsetBytes")
156 .and_then(|v| v.as_u64())
157 .unwrap_or(0);
158 let size = base_obj
159 .get("sizeBytes")
160 .and_then(|v| v.as_u64())
161 .unwrap_or(0);
162 let base_sym = base_obj
163 .get("sym")
164 .and_then(|v| v.as_str())
165 .unwrap_or("unknown");
166 let base_type =
167 base_sym.strip_prefix("T_").unwrap_or(base_sym);
168 let wrapped_type =
169 wrap_cpp_type(base_type, type_col_max_width);
170
171 base_classes.push(BaseClass {
172 offset,
173 size,
174 base_type: wrapped_type,
175 });
176 }
177 }
178
179 let mut table = Table::new(&base_classes);
180 table
181 .with(Style::rounded())
182 .with(Modify::new(Rows::first()).with(Color::FG_GREEN));
183
184 output.push_str("Base Classes:\n");
185 output.push_str(&format!("{}\n\n", table));
186 }
187 }
188
189 if let Some(fields) = meta_obj.get("fields").and_then(|v| v.as_array()) {
190 if !fields.is_empty() {
191 let mut field_list = Vec::new();
192
193 for field in fields {
194 if let Some(field_obj) = field.as_object() {
195 let offset = field_obj
196 .get("offsetBytes")
197 .and_then(|v| v.as_u64())
198 .unwrap_or(0);
199 let size = field_obj
200 .get("sizeBytes")
201 .and_then(|v| v.as_u64())
202 .unwrap_or(0);
203 let field_type = field_obj
204 .get("type")
205 .and_then(|v| v.as_str())
206 .unwrap_or("unknown");
207 let name = field_obj
208 .get("pretty")
209 .and_then(|v| v.as_str())
210 .and_then(|s| s.split("::").last())
211 .unwrap_or("unnamed");
212 let wrapped_type =
213 wrap_cpp_type(field_type, type_col_max_width);
214
215 field_list.push(Field {
216 offset,
217 size,
218 field_type: wrapped_type,
219 name: name.to_string(),
220 });
221 }
222 }
223
224 let mut table = Table::new(&field_list);
225 table
226 .with(Style::rounded())
227 .with(Modify::new(Rows::first()).with(Color::FG_CYAN));
228
229 output.push_str("Fields:\n");
230 output.push_str(&format!("{}\n", table));
231 }
232 }
233 }
234 break;
235 }
236 }
237 }
238 }
239
240 if !found {
241 output.push_str("No field layout information found.\n");
242 output.push_str("This feature only works with C++ classes and structs.\n");
243 }
244
245 output
246}
247
248impl SearchfoxClient {
249 pub async fn search_field_layout(&self, query: &FieldLayoutQuery) -> Result<serde_json::Value> {
250 let query_string = format!("field-layout:'{}'", query.class_name);
251
252 let mut url = Url::parse(&format!(
253 "https://searchfox.org/{}/query/default",
254 self.repo
255 ))?;
256 url.query_pairs_mut().append_pair("q", &query_string);
257
258 let response = self.get(url).await?;
259
260 if !response.status().is_success() {
261 anyhow::bail!("Request failed: {}", response.status());
262 }
263
264 let response_text = response.text().await?;
265
266 match serde_json::from_str::<serde_json::Value>(&response_text) {
267 Ok(json) => {
268 if let Some(_symbol_tree) = json.get("SymbolTreeTableList") {
269 Ok(json)
270 } else {
271 match serde_json::from_str::<SearchfoxResponse>(&response_text) {
272 Ok(parsed_json) => {
273 let mut result = serde_json::json!({});
274 for (key, value) in &parsed_json {
275 if !key.starts_with('*')
276 && (value.as_array().is_some() || value.as_object().is_some())
277 {
278 result[key] = value.clone();
279 }
280 }
281 Ok(result)
282 }
283 Err(_) => Ok(json),
284 }
285 }
286 }
287 Err(_) => Ok(serde_json::json!({
288 "error": "Failed to parse response as JSON",
289 "raw_response": response_text
290 })),
291 }
292 }
293}