Skip to main content

searchfox_lib/
field_layout.rs

1use 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(&current_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(&current_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}