gioser

tawasuyu/brahman

Rust 582TOML 222Markdown 73JSON 198635 Sutichaykuna

⎇ e77a32f4d6a09fe29701811f4d2e60dd2cb678db crates/modules/semantic_dht/minga-vfs/src/render.rs Rust · 268 siq'ikuna · 8.6 KB Ñawpa willay Pi imata llamkarqan ch'uya

feat(minga): minga-vfs — proyecta el repo como filesystem FUSE e77a32f4d · sergio ·

1//! Renderizado de un `SemanticNode` a texto legible. Lógica **pura**:
2//! sin IO, sin FUSE, sin `sled`. El VFS la usa para materializar el
3//! contenido de cada archivo virtual bajo demanda, pero es reutilizable
4//! por cualquier frontend (web, TUI).
5//!
6//! Dos vistas complementarias:
7//!
8//! - [`render_source`]: reconstrucción **canónica** del código fuente.
9//!   El AST semántico descartó whitespace y comentarios al ingerir
10//!   (son `extra` en tree-sitter), así que esto NO recupera el archivo
11//!   original byte-a-byte: es una forma *normalizada*, re-indentada por
12//!   estructura de llaves. Para lenguajes con bloques por llaves
13//!   (Rust/TS/JS/Go) sale legible; Python —cuya estructura vive en la
14//!   indentación, y la indentación es trivia— sale como una secuencia
15//!   de tokens en pocas líneas. Es esperado: el hash es de la
16//!   estructura, no del formato.
17//!
18//! - [`render_sexp`]: el árbol como S-expression indentada. Vista
19//!   exacta y sin pérdida de lo que el store guarda de verdad.
20
21use minga_core::SemanticNode;
22
23/// Reconstruye el código fuente de un subárbol en forma canónica.
24///
25/// Recolecta los tokens hoja en orden y los re-imprime con un
26/// pretty-printer mínimo, consciente de llaves: indenta tras `{`,
27/// desindenta antes de `}`, corta línea tras `;`. El resultado termina
28/// siempre con exactamente un `\n`.
29pub fn render_source(node: &SemanticNode) -> String {
30    let mut tokens = Vec::new();
31    collect_leaves(node, &mut tokens);
32    pretty_print(&tokens)
33}
34
35/// Recolecta el texto de los nodos hoja en orden de recorrido (DFS).
36/// Sólo las hojas tienen `leaf_text`; los nodos internos se recurren.
37fn collect_leaves(node: &SemanticNode, out: &mut Vec<String>) {
38    match &node.leaf_text {
39        Some(bytes) => {
40            let text = String::from_utf8_lossy(bytes);
41            let trimmed = text.trim();
42            if !trimmed.is_empty() {
43                out.push(trimmed.to_string());
44            }
45        }
46        None => {
47            for child in &node.children {
48                collect_leaves(child, out);
49            }
50        }
51    }
52}
53
54/// Tokens que no quieren un espacio a su izquierda (puntuación que se
55/// pega al token anterior). Incluye `(` y `[`: en una vista normalizada
56/// se pegan al identificador previo (`main()`, `v[0]`) — el caso de
57/// llamada/indexado es el dominante.
58fn no_space_before(t: &str) -> bool {
59    matches!(
60        t,
61        ")" | "]" | "," | ";" | "." | "::" | "?" | ":" | "!" | "(" | "["
62    )
63}
64
65/// Tokens tras los cuales no va espacio (abren un grupo o son prefijos
66/// que se pegan al token siguiente).
67fn no_space_after(t: &str) -> bool {
68    matches!(t, "(" | "[" | "." | "::" | "!" | "#")
69}
70
71/// ¿Hace falta un espacio entre `prev` y `cur` en una misma línea?
72fn needs_space(cur: &str, prev: &str) -> bool {
73    !no_space_before(cur) && !no_space_after(prev)
74}
75
76fn push_indent(out: &mut String, indent: usize) {
77    for _ in 0..indent {
78        out.push_str("    ");
79    }
80}
81
82/// Re-imprime una secuencia de tokens con indentación por llaves.
83fn pretty_print(tokens: &[String]) -> String {
84    let mut out = String::new();
85    let mut indent: usize = 0;
86    // ¿Hay ya contenido en la línea en curso?
87    let mut line_open = false;
88
89    for (i, tok) in tokens.iter().enumerate() {
90        let t = tok.as_str();
91        let next = tokens.get(i + 1).map(String::as_str);
92        match t {
93            "{" => {
94                if line_open {
95                    out.push(' ');
96                }
97                out.push('{');
98                indent += 1;
99                out.push('\n');
100                line_open = false;
101            }
102            "}" => {
103                indent = indent.saturating_sub(1);
104                if line_open {
105                    out.push('\n');
106                }
107                push_indent(&mut out, indent);
108                out.push('}');
109                line_open = true;
110                // `} else`, `},`, `};`, `})`, `}.` se quedan en línea;
111                // cualquier otra cosa abre línea nueva.
112                if !matches!(next, Some("else") | Some(",") | Some(";") | Some(")") | Some(".")) {
113                    out.push('\n');
114                    line_open = false;
115                }
116            }
117            ";" => {
118                out.push(';');
119                out.push('\n');
120                line_open = false;
121            }
122            _ => {
123                if !line_open {
124                    push_indent(&mut out, indent);
125                    line_open = true;
126                } else if let Some(prev) = tokens.get(i.wrapping_sub(1)).map(String::as_str) {
127                    if i > 0 && needs_space(t, prev) {
128                        out.push(' ');
129                    }
130                }
131                out.push_str(t);
132            }
133        }
134    }
135
136    // Final canónico: exactamente un newline.
137    while out.ends_with([' ', '\t', '\n']) {
138        out.pop();
139    }
140    out.push('\n');
141    out
142}
143
144/// Renderiza el subárbol como S-expression indentada (2 espacios por
145/// nivel). Cada nodo es `(kind ...)`; los nodos con `field_name` lo
146/// prefijan como `field: (kind ...)`; las hojas llevan su texto entre
147/// comillas. Es la representación literal de lo que hay en el store.
148pub fn render_sexp(node: &SemanticNode) -> String {
149    let mut out = String::new();
150    sexp_node(node, 0, &mut out);
151    out.push('\n');
152    out
153}
154
155fn sexp_node(node: &SemanticNode, depth: usize, out: &mut String) {
156    for _ in 0..depth {
157        out.push_str("  ");
158    }
159    // Convención tree-sitter: el nombre de campo va FUERA del paréntesis.
160    if let Some(field) = &node.field_name {
161        out.push_str(field);
162        out.push_str(": ");
163    }
164    out.push('(');
165    out.push_str(&node.kind);
166
167    match &node.leaf_text {
168        Some(bytes) => {
169            out.push(' ');
170            out.push('"');
171            out.push_str(&escape(&String::from_utf8_lossy(bytes)));
172            out.push('"');
173            out.push(')');
174        }
175        None if node.children.is_empty() => {
176            out.push(')');
177        }
178        None => {
179            for child in &node.children {
180                out.push('\n');
181                sexp_node(child, depth + 1, out);
182            }
183            out.push(')');
184        }
185    }
186}
187
188/// Escapa una cadena para que quepa entre comillas en la S-expression.
189fn escape(s: &str) -> String {
190    let mut out = String::with_capacity(s.len());
191    for c in s.chars() {
192        match c {
193            '\\' => out.push_str("\\\\"),
194            '"' => out.push_str("\\\""),
195            '\n' => out.push_str("\\n"),
196            '\r' => out.push_str("\\r"),
197            '\t' => out.push_str("\\t"),
198            _ => out.push(c),
199        }
200    }
201    out
202}
203
204#[cfg(test)]
205mod tests {
206    use super::*;
207    use minga_core::ast::SemanticNode;
208
209    fn leaf(kind: &str, text: &str) -> SemanticNode {
210        SemanticNode {
211            kind: kind.to_string(),
212            field_name: None,
213            leaf_text: Some(text.as_bytes().to_vec()),
214            children: Vec::new(),
215        }
216    }
217
218    fn branch(kind: &str, children: Vec<SemanticNode>) -> SemanticNode {
219        SemanticNode {
220            kind: kind.to_string(),
221            field_name: None,
222            leaf_text: None,
223            children,
224        }
225    }
226
227    #[test]
228    fn source_indents_on_braces() {
229        // tokens: fn main ( ) { let x = 1 ; }
230        let tree = branch(
231            "fn_item",
232            vec![
233                leaf("fn", "fn"),
234                leaf("ident", "main"),
235                leaf("(", "("),
236                leaf(")", ")"),
237                leaf("{", "{"),
238                leaf("let", "let"),
239                leaf("ident", "x"),
240                leaf("=", "="),
241                leaf("int", "1"),
242                leaf(";", ";"),
243                leaf("}", "}"),
244            ],
245        );
246        let out = render_source(&tree);
247        assert!(out.contains("fn main()"), "tokens pegados a paréntesis: {out:?}");
248        assert!(out.contains("    let x = 1;"), "cuerpo indentado: {out:?}");
249        assert!(out.ends_with("}\n"), "termina en una sola llave + newline: {out:?}");
250    }
251
252    #[test]
253    fn sexp_shows_kinds_fields_and_leaves() {
254        let mut id = leaf("identifier", "x");
255        id.field_name = Some("name".to_string());
256        let tree = branch("declaration", vec![id]);
257        let out = render_sexp(&tree);
258        assert!(out.contains("(declaration"));
259        assert!(out.contains("name: (identifier \"x\")"));
260    }
261
262    #[test]
263    fn sexp_escapes_special_chars() {
264        let out = render_sexp(&leaf("string", "a\"b\nc"));
265        assert!(out.contains("\\\""), "comilla escapada: {out:?}");
266        assert!(out.contains("\\n"), "newline escapado: {out:?}");
267    }
268}