Skip to main content

kotlin_codegen/
code.rs

1//! [`KtCode`] — an indentation-aware builder for raw statement/expression
2//! bodies. Declarations are modeled ([`super::model`]); their *bodies* are
3//! raw Kotlin text structured through this builder, so nesting and
4//! indentation are correct by construction without modeling every statement.
5
6/// One body element: a raw line at the current level, or a nested block.
7#[derive(Clone, Debug)]
8enum Item {
9    /// A single raw line (no leading indentation; may be empty).
10    Line(String),
11    /// A line broken across multiple lines **at render time** (when its
12    /// rendered level is known) if it exceeds the width budget — one call
13    /// argument / lambda parameter per line, same layout as
14    /// [`KtCode::raw_reindent_wrapped`].
15    Wrapped(String),
16    /// `opener` + indented children + `closer` (`}` / `})` / `} finally {`-style
17    /// continuations are expressed as sibling blocks). An **empty** opener or
18    /// closer emits no line at all — children still indent — which is how
19    /// block *continuations* (the `finally` half of a `try/finally`) attach.
20    Block {
21        opener: String,
22        children: KtCode,
23        closer: String,
24    },
25}
26
27/// A sequence of raw lines and nested blocks plus the imports its text
28/// references. Rendered with 4-space indentation per nesting level.
29#[derive(Clone, Debug, Default)]
30pub struct KtCode {
31    items: Vec<Item>,
32    /// FQNs referenced only inside this raw text (forwarded to the file's
33    /// `ImportSet` at render time).
34    imports: Vec<String>,
35}
36
37impl KtCode {
38    pub fn new() -> Self {
39        Self::default()
40    }
41
42    pub fn is_empty(&self) -> bool {
43        self.items.is_empty()
44    }
45
46    /// Append one raw line.
47    pub fn line(mut self, s: impl Into<String>) -> Self {
48        self.items.push(Item::Line(s.into()));
49        self
50    }
51
52    /// Append a multi-line string: each line lands at the current level.
53    pub fn lines(mut self, text: &str) -> Self {
54        for l in text.lines() {
55            self.items.push(Item::Line(l.to_string()));
56        }
57        self
58    }
59
60    /// Append one line that is width-broken at render time (one call
61    /// argument / lambda parameter per line) if it exceeds the budget at its
62    /// actual nesting level. Use for composed statements of unbounded width
63    /// (call expressions, guards); plain [`Self::line`] for short fixed text.
64    pub fn wline(mut self, s: impl Into<String>) -> Self {
65        self.items.push(Item::Wrapped(s.into()));
66        self
67    }
68
69    /// Append `try { body } finally { fin }`. `opener_prefix` glues a binding
70    /// onto the `try` (e.g. `"val __ret = "`).
71    pub fn try_finally(self, opener_prefix: impl Into<String>, body: KtCode, fin: KtCode) -> Self {
72        self.blk_with(
73            format!("{}try {{", opener_prefix.into()),
74            "} finally {",
75            |c| c.push(body),
76        )
77        .blk_with("", "}", |c| c.push(fin))
78    }
79
80    /// Append a nested block: `opener` at the current level, children one
81    /// level deeper, then `}`.
82    pub fn blk(self, opener: impl Into<String>, f: impl FnOnce(KtCode) -> KtCode) -> Self {
83        self.blk_with(opener, "}", f)
84    }
85
86    /// [`Self::blk`] with an explicit closer (`})`, `},`, …).
87    pub fn blk_with(
88        mut self,
89        opener: impl Into<String>,
90        closer: impl Into<String>,
91        f: impl FnOnce(KtCode) -> KtCode,
92    ) -> Self {
93        self.items.push(Item::Block {
94            opener: opener.into(),
95            children: f(KtCode::new()),
96            closer: closer.into(),
97        });
98        self
99    }
100
101    /// Append another `KtCode`'s items (same level) and imports.
102    pub fn push(mut self, other: KtCode) -> Self {
103        self.items.extend(other.items);
104        self.imports.extend(other.imports);
105        self
106    }
107
108    /// Register an FQN referenced only inside this raw text.
109    pub fn import(mut self, fqn: impl Into<String>) -> Self {
110        self.imports.push(fqn.into());
111        self
112    }
113
114    /// Build from a flat multi-line blob by recomputing nesting from brace
115    /// balance — for migrating bodies composed as unindented strings. Tracks
116    /// Kotlin string literals (incl. escapes) and `//` comments so braces
117    /// inside them don't count. A line's level drops first by its leading
118    /// closers (`}`, `})` …), then the rest of its delta applies to the
119    /// following lines.
120    pub fn raw_reindent(text: &str) -> Self {
121        Self::reindent_inner(text, false)
122    }
123
124    /// Like [`Self::raw_reindent`], but additionally breaks any line that would
125    /// exceed the renderer's maximum signature width across multiple lines,
126    /// one call argument (or lambda parameter) per line — the body-side analogue
127    /// of the width-aware signature layout in the renderer. The brace
128    /// delta that drives nesting is still computed from the original (un-broken)
129    /// line, so the wrapping never disturbs the surrounding block structure.
130    pub fn raw_reindent_wrapped(text: &str) -> Self {
131        Self::reindent_inner(text, true)
132    }
133
134    fn reindent_inner(text: &str, wrap: bool) -> Self {
135        let mut out = KtCode::new();
136        let mut level: usize = 0;
137        for raw in text.lines() {
138            let line = raw.trim();
139            if line.is_empty() {
140                out.items.push(Item::Line(String::new()));
141                continue;
142            }
143            let (leading_close, delta) = brace_profile(line);
144            level = level.saturating_sub(leading_close);
145            if wrap {
146                wrap_line(line, level, &mut out);
147            } else {
148                push_line(&mut out, level, line);
149            }
150            // Remaining delta after the leading closers were applied.
151            let net = delta + leading_close as i64;
152            if net > 0 {
153                level += net as usize;
154            } else {
155                level = level.saturating_sub((-net) as usize);
156            }
157        }
158        // The reindenter produced absolute indentation; mark items as
159        // pre-indented by wrapping: we emit them at the caller's level via
160        // render(), which prepends the base indent — exactly what we want.
161        out
162    }
163
164    pub(crate) fn collect_imports(&self, sink: &mut Vec<String>) {
165        sink.extend(self.imports.iter().cloned());
166        for it in &self.items {
167            if let Item::Block { children, .. } = it {
168                children.collect_imports(sink);
169            }
170        }
171    }
172
173    /// Render with `level` leading 4-space indents per line.
174    pub fn render(&self, level: usize, out: &mut String) {
175        for it in &self.items {
176            match it {
177                Item::Line(l) => {
178                    if l.is_empty() {
179                        out.push('\n');
180                    } else {
181                        for _ in 0..level {
182                            out.push_str("    ");
183                        }
184                        out.push_str(l);
185                        out.push('\n');
186                    }
187                }
188                Item::Wrapped(l) => {
189                    // Break against the budget at the now-known level; the
190                    // produced lines carry their absolute indentation, so the
191                    // temp renders at level 0.
192                    let mut tmp = KtCode::new();
193                    wrap_line(l, level, &mut tmp);
194                    tmp.render(0, out);
195                }
196                Item::Block {
197                    opener,
198                    children,
199                    closer,
200                } => {
201                    if !opener.is_empty() {
202                        for _ in 0..level {
203                            out.push_str("    ");
204                        }
205                        out.push_str(opener);
206                        out.push('\n');
207                    }
208                    children.render(level + 1, out);
209                    if !closer.is_empty() {
210                        for _ in 0..level {
211                            out.push_str("    ");
212                        }
213                        out.push_str(closer);
214                        out.push('\n');
215                    }
216                }
217            }
218        }
219    }
220}
221
222/// `(leading_closers, total_brace_delta)` of one trimmed line, ignoring
223/// braces inside string literals and `//` comments. `leading_closers` counts
224/// the `}` characters before any opener/content (so `}` and `})` and `} }`
225/// prefixes dedent the line itself); `total_brace_delta` is opens − closes
226/// over the whole line.
227fn brace_profile(line: &str) -> (usize, i64) {
228    let mut leading_close = 0usize;
229    let mut seen_content = false;
230    let mut delta: i64 = 0;
231    let mut chars = line.chars().peekable();
232    let mut in_str = false;
233    let mut in_char = false;
234    while let Some(c) = chars.next() {
235        if in_str {
236            match c {
237                '\\' => {
238                    let _ = chars.next();
239                }
240                '"' => in_str = false,
241                _ => {}
242            }
243            continue;
244        }
245        if in_char {
246            match c {
247                '\\' => {
248                    let _ = chars.next();
249                }
250                '\'' => in_char = false,
251                _ => {}
252            }
253            continue;
254        }
255        match c {
256            '"' => {
257                in_str = true;
258                seen_content = true;
259            }
260            '\'' => {
261                in_char = true;
262                seen_content = true;
263            }
264            '/' if chars.peek() == Some(&'/') => break,
265            '{' => {
266                delta += 1;
267                seen_content = true;
268            }
269            '}' => {
270                delta -= 1;
271                if !seen_content {
272                    leading_close += 1;
273                }
274            }
275            c if c.is_whitespace() || c == ')' || c == ',' || c == ';' => {
276                // closers like `})` / `},` keep counting as leading
277            }
278            _ => seen_content = true,
279        }
280    }
281    (leading_close, delta)
282}
283
284/// Width budget for breaking a body line, matching the signature layout.
285const MAX_LINE_WIDTH: usize = super::render::MAX_SIGNATURE_WIDTH;
286
287/// Push `text` as a `Line` carrying `level` 4-space indents (the same absolute
288/// indentation [`KtCode::reindent_inner`] bakes in for non-wrapped lines).
289fn push_line(out: &mut KtCode, level: usize, text: &str) {
290    let mut s = String::with_capacity(level * 4 + text.len());
291    for _ in 0..level {
292        s.push_str("    ");
293    }
294    s.push_str(text);
295    out.items.push(Item::Line(s));
296}
297
298fn fits(line: &str, level: usize) -> bool {
299    level * 4 + line.len() <= MAX_LINE_WIDTH
300}
301
302fn is_ident_byte(c: u8) -> bool {
303    c.is_ascii_alphanumeric() || c == b'_'
304}
305
306/// Is the `(` at byte `open` a *call* paren — preceded by an identifier whose
307/// word is not a control-flow keyword (those can't take a trailing comma)?
308fn is_call_paren(line: &str, open: usize) -> bool {
309    let b = line.as_bytes();
310    if open == 0 || !is_ident_byte(b[open - 1]) {
311        return false;
312    }
313    let mut j = open;
314    while j > 0 && is_ident_byte(b[j - 1]) {
315        j -= 1;
316    }
317    !matches!(
318        &line[j..open],
319        "if" | "while" | "for" | "when" | "catch" | "synchronized"
320    )
321}
322
323/// Byte index of the `-` of the first top-level `->` in `line[start..end)`
324/// (bracket depth 0, ignoring string/char literals), or `None`.
325fn find_arrow(line: &str, start: usize, end: usize) -> Option<usize> {
326    let b = line.as_bytes();
327    let mut depth = 0i32;
328    let mut in_str = false;
329    let mut in_char = false;
330    let mut i = start;
331    while i < end {
332        let c = b[i];
333        if in_str {
334            if c == b'\\' {
335                i += 2;
336                continue;
337            }
338            if c == b'"' {
339                in_str = false;
340            }
341            i += 1;
342            continue;
343        }
344        if in_char {
345            if c == b'\\' {
346                i += 2;
347                continue;
348            }
349            if c == b'\'' {
350                in_char = false;
351            }
352            i += 1;
353            continue;
354        }
355        match c {
356            b'"' => in_str = true,
357            b'\'' => in_char = true,
358            b'(' | b'{' | b'[' => depth += 1,
359            b')' | b'}' | b']' => depth -= 1,
360            b'-' if depth == 0 && i + 1 < end && b[i + 1] == b'>' => return Some(i),
361            _ => {}
362        }
363        i += 1;
364    }
365    None
366}
367
368/// Split `s` at top-level commas, tracking `(){}[]` and generic `<>` depth and
369/// ignoring string/char literals (an arrow `->` is not a generic closer).
370fn split_top_commas(s: &str) -> Vec<&str> {
371    let b = s.as_bytes();
372    let mut depth = 0i32;
373    let mut angle = 0i32;
374    let mut in_str = false;
375    let mut in_char = false;
376    let mut parts = Vec::new();
377    let mut start = 0usize;
378    let mut i = 0usize;
379    while i < b.len() {
380        let c = b[i];
381        if in_str {
382            if c == b'\\' {
383                i += 2;
384                continue;
385            }
386            if c == b'"' {
387                in_str = false;
388            }
389            i += 1;
390            continue;
391        }
392        if in_char {
393            if c == b'\\' {
394                i += 2;
395                continue;
396            }
397            if c == b'\'' {
398                in_char = false;
399            }
400            i += 1;
401            continue;
402        }
403        match c {
404            b'"' => in_str = true,
405            b'\'' => in_char = true,
406            b'/' if i + 1 < b.len() && b[i + 1] == b'/' => break,
407            b'(' | b'{' | b'[' => depth += 1,
408            b')' | b'}' | b']' => depth -= 1,
409            b'<' if i > 0 && is_ident_byte(b[i - 1]) => angle += 1,
410            b'>' if !(i > 0 && b[i - 1] == b'-') && angle > 0 => angle -= 1,
411            b',' if depth == 0 && angle == 0 => {
412                parts.push(&s[start..i]);
413                start = i + 1;
414            }
415            _ => {}
416        }
417        i += 1;
418    }
419    parts.push(&s[start..]);
420    parts
421}
422
423/// The outermost breakable construct on a line.
424enum Construct {
425    /// A call: byte index of `(` and its matching `)`.
426    Call { open: usize, close: usize },
427    /// A lambda: byte index of `{`, its matching `}`, and the `-` of its `->`.
428    Lambda {
429        open: usize,
430        close: usize,
431        arrow: Option<usize>,
432    },
433}
434
435/// Find the depth-0 construct with the largest span (ties keep the leftmost):
436/// a call `ident(…)` or a lambda `{ … -> … }`. Only the *outermost* construct
437/// is returned — nested calls/lambdas are reached by recursing into its parts.
438fn find_break(line: &str) -> Option<Construct> {
439    let b = line.as_bytes();
440    let mut stack: Vec<(u8, usize)> = Vec::new();
441    let mut in_str = false;
442    let mut in_char = false;
443    let mut best: Option<Construct> = None;
444    let mut best_span = 0usize;
445    let mut i = 0usize;
446    while i < b.len() {
447        let c = b[i];
448        if in_str {
449            if c == b'\\' {
450                i += 2;
451                continue;
452            }
453            if c == b'"' {
454                in_str = false;
455            }
456            i += 1;
457            continue;
458        }
459        if in_char {
460            if c == b'\\' {
461                i += 2;
462                continue;
463            }
464            if c == b'\'' {
465                in_char = false;
466            }
467            i += 1;
468            continue;
469        }
470        match c {
471            b'"' => in_str = true,
472            b'\'' => in_char = true,
473            b'/' if i + 1 < b.len() && b[i + 1] == b'/' => break,
474            b'(' | b'{' | b'[' => stack.push((c, i)),
475            b')' | b'}' | b']' => {
476                if let Some((open_c, open_i)) = stack.pop() {
477                    if stack.is_empty() {
478                        // The candidate construct, if this depth-0 close yields one.
479                        let cand = if open_c == b'(' && c == b')' {
480                            (is_call_paren(line, open_i) && !line[open_i + 1..i].trim().is_empty())
481                                .then_some(Construct::Call {
482                                    open: open_i,
483                                    close: i,
484                                })
485                        } else if open_c == b'{' && c == b'}' {
486                            Some(Construct::Lambda {
487                                open: open_i,
488                                close: i,
489                                arrow: find_arrow(line, open_i + 1, i),
490                            })
491                        } else {
492                            None
493                        };
494                        if let Some(cand) = cand {
495                            let span = i - open_i;
496                            if best.is_none() || span > best_span {
497                                best_span = span;
498                                best = Some(cand);
499                            }
500                        }
501                    }
502                }
503            }
504            _ => {}
505        }
506        i += 1;
507    }
508    best
509}
510
511/// Emit `line` at `level`, breaking it one argument/parameter per line when it
512/// exceeds the width budget. Recurses into the broken-out parts so nested calls
513/// and the inline callback lambda are formatted too.
514fn wrap_line(line: &str, level: usize, out: &mut KtCode) {
515    if fits(line, level) {
516        push_line(out, level, line);
517        return;
518    }
519    match find_break(line) {
520        Some(Construct::Call { open, close }) => {
521            push_line(out, level, &line[..=open]);
522            for arg in split_top_commas(&line[open + 1..close]) {
523                let arg = arg.trim();
524                if arg.is_empty() {
525                    continue;
526                }
527                wrap_line(&format!("{arg},"), level + 1, out);
528            }
529            push_line(out, level, &line[close..]);
530        }
531        Some(Construct::Lambda { open, close, arrow }) => {
532            push_line(out, level, &format!("{}{{", &line[..open]));
533            let inner = &line[open + 1..close];
534            match arrow {
535                Some(arr) => {
536                    let arr_in = arr - (open + 1);
537                    for p in split_top_commas(inner[..arr_in].trim()) {
538                        let p = p.trim();
539                        if p.is_empty() {
540                            continue;
541                        }
542                        push_line(out, level + 1, &format!("{p},"));
543                    }
544                    push_line(out, level + 1, "->");
545                    wrap_line(inner[arr_in + 2..].trim(), level + 1, out);
546                }
547                None => wrap_line(inner.trim(), level + 1, out),
548            }
549            push_line(out, level, &format!("}}{}", &line[close + 1..]));
550        }
551        None => push_line(out, level, line),
552    }
553}
554
555#[cfg(test)]
556mod tests;