1#[derive(Clone, Debug)]
8enum Item {
9 Line(String),
11 Wrapped(String),
16 Block {
21 opener: String,
22 children: KtCode,
23 closer: String,
24 },
25}
26
27#[derive(Clone, Debug, Default)]
30pub struct KtCode {
31 items: Vec<Item>,
32 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 pub fn line(mut self, s: impl Into<String>) -> Self {
48 self.items.push(Item::Line(s.into()));
49 self
50 }
51
52 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 pub fn wline(mut self, s: impl Into<String>) -> Self {
65 self.items.push(Item::Wrapped(s.into()));
66 self
67 }
68
69 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 pub fn blk(self, opener: impl Into<String>, f: impl FnOnce(KtCode) -> KtCode) -> Self {
83 self.blk_with(opener, "}", f)
84 }
85
86 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 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 pub fn import(mut self, fqn: impl Into<String>) -> Self {
110 self.imports.push(fqn.into());
111 self
112 }
113
114 pub fn raw_reindent(text: &str) -> Self {
121 Self::reindent_inner(text, false)
122 }
123
124 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 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 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 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 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
222fn 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 }
278 _ => seen_content = true,
279 }
280 }
281 (leading_close, delta)
282}
283
284const MAX_LINE_WIDTH: usize = super::render::MAX_SIGNATURE_WIDTH;
286
287fn 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
306fn 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
323fn 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
368fn 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
423enum Construct {
425 Call { open: usize, close: usize },
427 Lambda {
429 open: usize,
430 close: usize,
431 arrow: Option<usize>,
432 },
433}
434
435fn 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 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
511fn 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;