1use std::{
2 fmt, fs,
3 io::{self, Write},
4 ops::ControlFlow,
5 path::{Path, PathBuf},
6 sync::atomic::{AtomicUsize, Ordering},
7 time::{Duration, Instant},
8};
9
10use anstyle::{AnsiColor, Color, RgbColor};
11use anyhow::{Context, Result, anyhow};
12use clap::ValueEnum;
13use log::info;
14use schemars::JsonSchema;
15use serde::{Deserialize, Serialize};
16use tree_sitter::{
17 InputEdit, Language, LogType, ParseOptions, ParseState, Parser, Point, Range, Tree, TreeCursor,
18 ffi,
19};
20
21use crate::{fuzz::edits::Edit, paint::paint, util};
22
23#[derive(Debug, Default, Serialize, JsonSchema)]
24pub struct Stats {
25 pub successful_parses: usize,
26 pub total_parses: usize,
27 pub total_bytes: usize,
28 pub total_duration: Duration,
29}
30
31impl fmt::Display for Stats {
32 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33 let duration_us = self.total_duration.as_micros();
34 let success_rate = if self.total_parses > 0 {
35 format!(
36 "{:.2}%",
37 ((self.successful_parses as f64) / (self.total_parses as f64)) * 100.0,
38 )
39 } else {
40 "N/A".to_string()
41 };
42 let duration_str = match (self.total_parses, duration_us) {
43 (0, _) => "N/A".to_string(),
44 (_, 0) => "0 bytes/ms".to_string(),
45 (_, _) => format!(
46 "{} bytes/ms",
47 ((self.total_bytes as u128) * 1_000) / duration_us
48 ),
49 };
50 writeln!(
51 f,
52 "Total parses: {}; successful parses: {}; failed parses: {}; success percentage: {success_rate}; average speed: {duration_str}",
53 self.total_parses,
54 self.successful_parses,
55 self.total_parses - self.successful_parses,
56 )
57 }
58}
59
60#[derive(Debug, Copy, Clone)]
62pub struct ParseTheme {
63 pub node_kind: Option<Color>,
65 pub node_text: Option<Color>,
67 pub field: Option<Color>,
69 pub row_color: Option<Color>,
71 pub row_color_named: Option<Color>,
73 pub extra: Option<Color>,
75 pub error: Option<Color>,
77 pub missing: Option<Color>,
79 pub line_feed: Option<Color>,
81 pub backtick: Option<Color>,
83 pub literal: Option<Color>,
85}
86
87impl ParseTheme {
88 const GRAY: Color = Color::Rgb(RgbColor(118, 118, 118));
89 const LIGHT_GRAY: Color = Color::Rgb(RgbColor(166, 172, 181));
90 const ORANGE: Color = Color::Rgb(RgbColor(255, 153, 51));
91 const YELLOW: Color = Color::Rgb(RgbColor(219, 219, 173));
92 const GREEN: Color = Color::Rgb(RgbColor(101, 192, 67));
93
94 #[must_use]
95 pub const fn empty() -> Self {
96 Self {
97 node_kind: None,
98 node_text: None,
99 field: None,
100 row_color: None,
101 row_color_named: None,
102 extra: None,
103 error: None,
104 missing: None,
105 line_feed: None,
106 backtick: None,
107 literal: None,
108 }
109 }
110}
111
112impl Default for ParseTheme {
113 fn default() -> Self {
114 Self {
115 node_kind: Some(AnsiColor::BrightCyan.into()),
116 node_text: Some(Self::GRAY),
117 field: Some(AnsiColor::Blue.into()),
118 row_color: Some(AnsiColor::White.into()),
119 row_color_named: Some(AnsiColor::BrightCyan.into()),
120 extra: Some(AnsiColor::BrightMagenta.into()),
121 error: Some(AnsiColor::Red.into()),
122 missing: Some(Self::ORANGE),
123 line_feed: Some(Self::LIGHT_GRAY),
124 backtick: Some(Self::GREEN),
125 literal: Some(Self::YELLOW),
126 }
127 }
128}
129
130#[derive(Debug, Copy, Clone, Deserialize, Serialize)]
131pub struct Rgb(pub u8, pub u8, pub u8);
132
133impl From<Rgb> for RgbColor {
134 fn from(val: Rgb) -> Self {
135 Self(val.0, val.1, val.2)
136 }
137}
138
139#[derive(Debug, Copy, Clone, Default, Deserialize, Serialize)]
140#[serde(rename_all = "kebab-case")]
141pub struct Config {
142 pub parse_theme: Option<ParseThemeRaw>,
143}
144
145#[derive(Debug, Copy, Clone, Default, Deserialize, Serialize)]
146#[serde(rename_all = "kebab-case")]
147pub struct ParseThemeRaw {
148 pub node_kind: Option<Rgb>,
149 pub node_text: Option<Rgb>,
150 pub field: Option<Rgb>,
151 pub row_color: Option<Rgb>,
152 pub row_color_named: Option<Rgb>,
153 pub extra: Option<Rgb>,
154 pub error: Option<Rgb>,
155 pub missing: Option<Rgb>,
156 pub line_feed: Option<Rgb>,
157 pub backtick: Option<Rgb>,
158 pub literal: Option<Rgb>,
159}
160
161impl From<ParseThemeRaw> for ParseTheme {
162 fn from(value: ParseThemeRaw) -> Self {
163 let val_or_default = |val: Option<Rgb>, default: Option<Color>| -> Option<Color> {
164 val.map_or(default, |v| Some(Color::Rgb(v.into())))
165 };
166 let default = Self::default();
167
168 Self {
169 node_kind: val_or_default(value.node_kind, default.node_kind),
170 node_text: val_or_default(value.node_text, default.node_text),
171 field: val_or_default(value.field, default.field),
172 row_color: val_or_default(value.row_color, default.row_color),
173 row_color_named: val_or_default(value.row_color_named, default.row_color_named),
174 extra: val_or_default(value.extra, default.extra),
175 error: val_or_default(value.error, default.error),
176 missing: val_or_default(value.missing, default.missing),
177 line_feed: val_or_default(value.line_feed, default.line_feed),
178 backtick: val_or_default(value.backtick, default.backtick),
179 literal: val_or_default(value.literal, default.literal),
180 }
181 }
182}
183
184#[derive(Copy, Clone, PartialEq, Eq)]
185pub enum ParseOutput {
186 Normal,
187 Quiet,
188 Xml,
189 Cst,
190 Dot,
191}
192
193#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize)]
199pub struct ParsePoint {
200 pub row: usize,
201 pub column: usize,
202}
203
204impl From<Point> for ParsePoint {
205 fn from(value: Point) -> Self {
206 Self {
207 row: value.row,
208 column: value.column,
209 }
210 }
211}
212
213#[derive(Serialize, Default, Debug, Clone)]
214pub struct ParseSummary {
215 pub file: PathBuf,
216 pub successful: bool,
217 pub start: Option<ParsePoint>,
218 pub end: Option<ParsePoint>,
219 pub duration: Option<Duration>,
220 pub bytes: Option<usize>,
221}
222
223impl ParseSummary {
224 #[must_use]
225 pub fn new(path: &Path) -> Self {
226 Self {
227 file: path.to_path_buf(),
228 successful: false,
229 ..Default::default()
230 }
231 }
232}
233
234#[derive(Serialize, Debug)]
235pub struct ParseStats {
236 pub parse_summaries: Vec<ParseSummary>,
237 pub cumulative_stats: Stats,
238 pub source_count: usize,
239}
240
241impl Default for ParseStats {
242 fn default() -> Self {
243 Self {
244 parse_summaries: Vec::new(),
245 cumulative_stats: Stats::default(),
246 source_count: 1,
247 }
248 }
249}
250
251#[derive(Serialize, ValueEnum, Debug, Copy, Clone, Default, Eq, PartialEq)]
252pub enum ParseDebugType {
253 #[default]
254 Quiet,
255 Normal,
256 Pretty,
257}
258
259pub struct ParseFileOptions<'a> {
260 pub edits: &'a [&'a str],
261 pub output: ParseOutput,
262 pub stats: &'a mut ParseStats,
263 pub print_time: bool,
264 pub timeout: u64,
265 pub debug: ParseDebugType,
266 pub debug_graph: bool,
267 pub cancellation_flag: Option<&'a AtomicUsize>,
268 pub encoding: Option<u32>,
269 pub open_log: bool,
270 pub no_ranges: bool,
271 pub parse_theme: &'a ParseTheme,
272}
273
274#[derive(Copy, Clone)]
275pub struct ParseResult {
276 pub successful: bool,
277 pub bytes: usize,
278 pub duration: Option<Duration>,
279}
280
281pub fn parse_file_at_path(
282 parser: &mut Parser,
283 language: &Language,
284 path: &Path,
285 name: &str,
286 max_path_length: usize,
287 opts: &mut ParseFileOptions,
288) -> Result<()> {
289 #[expect(
290 clippy::collection_is_never_read,
291 reason = "value is held for its Drop side effect"
292 )]
293 let mut _log_session = None;
294 parser.set_language(language)?;
295 let mut source_code = fs::read(path).with_context(|| format!("Error reading {name:?}"))?;
296
297 if opts.debug_graph {
299 _log_session = Some(util::log_graphs(parser, "log.html", opts.open_log)?);
300 }
301 else if opts.debug != ParseDebugType::Quiet {
303 let mut curr_version: usize = 0;
304 let debug = opts.debug;
305 parser.set_logger(Some(Box::new(move |log_type, message| {
306 if debug == ParseDebugType::Normal {
307 if log_type == LogType::Lex {
308 write!(&mut io::stderr(), " ").unwrap();
309 }
310 writeln!(&mut io::stderr(), "{message}").unwrap();
311 } else {
312 #[rustfmt::skip]
313 let colors = &[
314 AnsiColor::White, AnsiColor::Red, AnsiColor::Blue, AnsiColor::Green,
315 AnsiColor::Cyan, AnsiColor::Yellow, AnsiColor::Magenta,
316 AnsiColor::BrightWhite, AnsiColor::BrightRed, AnsiColor::BrightBlue,
317 AnsiColor::BrightGreen, AnsiColor::BrightCyan, AnsiColor::BrightYellow,
318 AnsiColor::BrightMagenta,
319 ];
320 if message.starts_with("process version:") {
321 let comma_idx = message.find(',').unwrap();
322 curr_version = message["process version:".len()..comma_idx]
323 .parse()
324 .unwrap();
325 }
326 let color = Some(colors[curr_version % colors.len()]);
327 let prefix = if log_type == LogType::Lex { " " } else { "" };
328 writeln!(&mut io::stderr(), "{prefix}{}", paint(color, message)).unwrap();
329 }
330 })));
331 }
332
333 let parse_time = Instant::now();
334
335 #[inline]
336 fn is_utf16_le_bom(bom_bytes: &[u8]) -> bool {
337 bom_bytes == [0xFF, 0xFE]
338 }
339
340 #[inline]
341 fn is_utf16_be_bom(bom_bytes: &[u8]) -> bool {
342 bom_bytes == [0xFE, 0xFF]
343 }
344
345 let encoding = match opts.encoding {
346 None if source_code.len() >= 2 => {
347 if is_utf16_le_bom(&source_code[0..2]) {
348 Some(ffi::TSInputEncodingUTF16LE)
349 } else if is_utf16_be_bom(&source_code[0..2]) {
350 Some(ffi::TSInputEncodingUTF16BE)
351 } else {
352 None
353 }
354 }
355 _ => opts.encoding,
356 };
357
358 let start_time = Instant::now();
364 let progress_callback = &mut |_: &ParseState| {
365 if let Some(cancellation_flag) = opts.cancellation_flag
366 && cancellation_flag.load(Ordering::SeqCst) != 0
367 {
368 return ControlFlow::Break(());
369 }
370
371 if opts.timeout > 0 && start_time.elapsed().as_micros() > u128::from(opts.timeout) {
372 return ControlFlow::Break(());
373 }
374
375 ControlFlow::Continue(())
376 };
377
378 let parse_opts = ParseOptions::new().progress_callback(progress_callback);
379
380 let tree = match encoding {
381 Some(encoding) if encoding == ffi::TSInputEncodingUTF16LE => {
382 let source_code_utf16 = source_code
383 .as_chunks::<2>()
384 .0
385 .iter()
386 .map(|&chunk| u16::from_le_bytes(chunk))
387 .collect::<Vec<_>>();
388 parser.parse_utf16_le_with_options(
389 &mut |i, _| {
390 if i < source_code_utf16.len() {
391 &source_code_utf16[i..]
392 } else {
393 &[]
394 }
395 },
396 None,
397 Some(parse_opts),
398 )
399 }
400 Some(encoding) if encoding == ffi::TSInputEncodingUTF16BE => {
401 let source_code_utf16 = source_code
402 .as_chunks::<2>()
403 .0
404 .iter()
405 .map(|&chunk| u16::from_be_bytes(chunk))
406 .collect::<Vec<_>>();
407 parser.parse_utf16_be_with_options(
408 &mut |i, _| {
409 if i < source_code_utf16.len() {
410 &source_code_utf16[i..]
411 } else {
412 &[]
413 }
414 },
415 None,
416 Some(parse_opts),
417 )
418 }
419 _ => parser.parse_with_options(
420 &mut |i, _| {
421 if i < source_code.len() {
422 &source_code[i..]
423 } else {
424 &[]
425 }
426 },
427 None,
428 Some(parse_opts),
429 ),
430 };
431 let parse_duration = parse_time.elapsed();
432
433 let stdout = io::stdout();
434 let mut stdout = io::BufWriter::with_capacity(64 * 1024, stdout.lock());
435
436 if let Some(mut tree) = tree {
437 if opts.debug_graph && !opts.edits.is_empty() {
438 info!("BEFORE:\n{}", String::from_utf8_lossy(&source_code));
439 }
440
441 let edit_time = Instant::now();
442 for (i, edit) in opts.edits.iter().enumerate() {
443 let edit = parse_edit_flag(&source_code, edit)?;
444 perform_edit(&mut tree, &mut source_code, &edit)?;
445 tree = parser.parse(&source_code, Some(&tree)).unwrap();
446
447 if opts.debug_graph {
448 info!("AFTER {i}:\n{}", String::from_utf8_lossy(&source_code));
449 }
450 }
451 let edit_duration = edit_time.elapsed();
452
453 parser.stop_printing_dot_graphs();
454
455 let parse_duration_ms = parse_duration.as_micros() as f64 / 1e3;
456 let edit_duration_ms = edit_duration.as_micros() as f64 / 1e3;
457 let mut cursor = tree.walk();
458
459 if opts.output == ParseOutput::Normal {
460 let mut needs_newline = false;
461 let mut indent_level = 0;
462 let mut did_visit_children = false;
463 loop {
464 let node = cursor.node();
465 let is_named = node.is_named();
466 if did_visit_children {
467 if is_named {
468 stdout.write_all(b")")?;
469 needs_newline = true;
470 }
471 if cursor.goto_next_sibling() {
472 did_visit_children = false;
473 } else if cursor.goto_parent() {
474 did_visit_children = true;
475 indent_level -= 1;
476 } else {
477 break;
478 }
479 } else {
480 if is_named {
481 if needs_newline {
482 stdout.write_all(b"\n")?;
483 }
484 for _ in 0..indent_level {
485 stdout.write_all(b" ")?;
486 }
487 let start = node.start_position();
488 let end = node.end_position();
489 if let Some(field_name) = cursor.field_name() {
490 write!(&mut stdout, "{field_name}: ")?;
491 }
492 write!(&mut stdout, "({}", node.kind())?;
493 if !opts.no_ranges {
494 write!(
495 &mut stdout,
496 " [{}, {}] - [{}, {}]",
497 start.row, start.column, end.row, end.column
498 )?;
499 }
500 needs_newline = true;
501 }
502 if cursor.goto_first_child() {
503 did_visit_children = false;
504 indent_level += 1;
505 } else {
506 did_visit_children = true;
507 }
508 }
509 }
510 cursor.reset(tree.root_node());
511 writeln!(&mut stdout)?;
512 }
513
514 if opts.output == ParseOutput::Cst {
515 render_cst(&source_code, &tree, &mut cursor, opts, &mut stdout)?;
516 }
517
518 if opts.output == ParseOutput::Xml {
519 let mut needs_newline = false;
520 let mut indent_level = 2;
521 let mut did_visit_children = false;
522 let mut had_named_children = false;
523 let mut tags = Vec::<&str>::new();
524
525 if opts.stats.parse_summaries.is_empty() {
527 writeln!(&mut stdout, "<?xml version=\"1.0\"?>")?;
528 writeln!(&mut stdout, "<sources>")?;
529 }
530 writeln!(&mut stdout, " <source name=\"{}\">", path.display())?;
531
532 loop {
533 let node = cursor.node();
534 let is_named = node.is_named();
535 if did_visit_children {
536 if is_named {
537 let tag = tags.pop();
538 if had_named_children {
539 for _ in 0..indent_level {
540 stdout.write_all(b" ")?;
541 }
542 }
543 write!(&mut stdout, "</{}>", tag.expect("there is a tag"))?;
544 if let Some(parent) = node.parent()
546 && parent.child(parent.child_count() - 1).unwrap() == node
547 {
548 stdout.write_all(b"\n")?;
549 }
550 needs_newline = true;
551 }
552 if cursor.goto_next_sibling() {
553 did_visit_children = false;
554 had_named_children = false;
555 } else if cursor.goto_parent() {
556 did_visit_children = true;
557 had_named_children = is_named;
558 indent_level -= 1;
559 if !is_named && needs_newline {
560 stdout.write_all(b"\n")?;
561 for _ in 0..indent_level {
562 stdout.write_all(b" ")?;
563 }
564 }
565 } else {
566 break;
567 }
568 } else {
569 if is_named {
570 if needs_newline {
571 stdout.write_all(b"\n")?;
572 }
573 for _ in 0..indent_level {
574 stdout.write_all(b" ")?;
575 }
576 write!(&mut stdout, "<{}", node.kind())?;
577 if let Some(field_name) = cursor.field_name() {
578 write!(&mut stdout, " field=\"{field_name}\"")?;
579 }
580 let start = node.start_position();
581 let end = node.end_position();
582 write!(
583 &mut stdout,
584 " srow=\"{}\" scol=\"{}\" erow=\"{}\" ecol=\"{}\">",
585 start.row, start.column, end.row, end.column
586 )?;
587 tags.push(node.kind());
588 needs_newline = true;
589 }
590 if cursor.goto_first_child() {
591 did_visit_children = false;
592 had_named_children = false;
593 indent_level += 1;
594 } else {
595 did_visit_children = true;
596 let start = node.start_byte();
597 let end = node.end_byte();
598 let value =
599 std::str::from_utf8(&source_code[start..end]).expect("has a string");
600 if !is_named && needs_newline {
601 stdout.write_all(b"\n")?;
602 for _ in 0..indent_level {
603 stdout.write_all(b" ")?;
604 }
605 }
606 write!(&mut stdout, "{}", html_escape::encode_text(value))?;
607 }
608 }
609 }
610 writeln!(&mut stdout)?;
611 writeln!(&mut stdout, " </source>")?;
612
613 if opts.stats.parse_summaries.len() == opts.stats.source_count - 1 {
615 writeln!(&mut stdout, "</sources>")?;
616 }
617 cursor.reset(tree.root_node());
618 }
619
620 if opts.output == ParseOutput::Dot {
621 util::print_tree_graph(&tree, "log.html", opts.open_log).unwrap();
622 }
623
624 let mut first_error = None;
625 let mut earliest_node_with_error = None;
626 'outer: loop {
627 let node = cursor.node();
628 if node.has_error() {
629 if earliest_node_with_error.is_none() {
630 earliest_node_with_error = Some(node);
631 }
632 if node.is_error() || node.is_missing() {
633 first_error = Some(node);
634 break;
635 }
636
637 if !cursor.goto_first_child() {
642 let earliest = earliest_node_with_error.unwrap();
643 while cursor.goto_parent() {
644 if cursor.node().parent().is_some_and(|p| p == earliest) {
645 while cursor.goto_next_sibling() {
646 let sibling = cursor.node();
647 if sibling.is_error() || sibling.is_missing() {
648 first_error = Some(sibling);
649 break 'outer;
650 }
651 if sibling.has_error() && cursor.goto_first_child() {
652 continue 'outer;
653 }
654 }
655 break;
656 }
657 }
658 break;
659 }
660 } else if !cursor.goto_next_sibling() {
661 break;
662 }
663 }
664
665 if first_error.is_some() || opts.print_time {
666 let path = path.to_string_lossy();
667 write!(
668 &mut stdout,
669 "{:width$}\tParse: {parse_duration_ms:>7.2} ms\t{:>6} bytes/ms",
670 name,
671 (source_code.len() as u128 * 1_000_000) / parse_duration.as_nanos(),
672 width = max_path_length
673 )?;
674 if let Some(node) = first_error {
675 let node_kind = node.kind();
676 let mut node_text = String::with_capacity(node_kind.len());
677 for c in node_kind.chars() {
678 if let Some(escaped) = escape_invisible(c) {
679 node_text += escaped;
680 } else {
681 node_text.push(c);
682 }
683 }
684 write!(&mut stdout, "\t(")?;
685 if node.is_missing() {
686 if node.is_named() {
687 write!(&mut stdout, "MISSING {node_text}")?;
688 } else {
689 write!(&mut stdout, "MISSING \"{node_text}\"")?;
690 }
691 } else {
692 write!(&mut stdout, "{node_text}")?;
693 }
694
695 let start = node.start_position();
696 let end = node.end_position();
697 write!(
698 &mut stdout,
699 " [{}, {}] - [{}, {}])",
700 start.row, start.column, end.row, end.column
701 )?;
702 }
703 if !opts.edits.is_empty() {
704 write!(
705 &mut stdout,
706 "\n{:width$}\tEdit: {edit_duration_ms:>7.2} ms",
707 " ".repeat(path.len()),
708 width = max_path_length,
709 )?;
710 }
711 writeln!(&mut stdout)?;
712 }
713
714 opts.stats.parse_summaries.push(ParseSummary {
715 file: path.to_path_buf(),
716 successful: first_error.is_none(),
717 start: Some(tree.root_node().start_position().into()),
718 end: Some(tree.root_node().end_position().into()),
719 duration: Some(parse_duration),
720 bytes: Some(source_code.len()),
721 });
722
723 return Ok(());
724 }
725 parser.stop_printing_dot_graphs();
726
727 if opts.print_time {
728 let duration = parse_time.elapsed();
729 let duration_ms = duration.as_micros() as f64 / 1e3;
730 writeln!(
731 &mut stdout,
732 "{:width$}\tParse: {duration_ms:>7.2} ms\t(timed out)",
733 path.to_str().unwrap(),
734 width = max_path_length
735 )?;
736 }
737
738 opts.stats.parse_summaries.push(ParseSummary {
739 file: path.to_path_buf(),
740 successful: false,
741 start: None,
742 end: None,
743 duration: None,
744 bytes: Some(source_code.len()),
745 });
746
747 Ok(())
748}
749
750const fn escape_invisible(c: char) -> Option<&'static str> {
751 Some(match c {
752 '\n' => "\\n",
753 '\r' => "\\r",
754 '\t' => "\\t",
755 '\0' => "\\0",
756 '\\' => "\\\\",
757 '\x0b' => "\\v",
758 '\x0c' => "\\f",
759 _ => return None,
760 })
761}
762
763const fn escape_delimiter(c: char) -> Option<&'static str> {
764 Some(match c {
765 '`' => "\\`",
766 '\"' => "\\\"",
767 _ => return None,
768 })
769}
770
771pub fn render_cst<'a, 'b: 'a>(
772 source_code: &[u8],
773 tree: &'b Tree,
774 cursor: &mut TreeCursor<'a>,
775 opts: &ParseFileOptions,
776 out: &mut impl Write,
777) -> io::Result<()> {
778 let lossy_source_code = String::from_utf8_lossy(source_code);
779 let total_width = lossy_source_code
780 .lines()
781 .enumerate()
782 .map(|(row, col)| {
783 row.checked_ilog10().unwrap_or(0) as usize
784 + col.len().checked_ilog10().unwrap_or(0) as usize
785 + 1
786 })
787 .max()
788 .unwrap_or(1);
789 let mut indent_level = usize::from(!opts.no_ranges);
790 let mut did_visit_children = false;
791 let mut in_error = false;
792 loop {
793 if did_visit_children {
794 if cursor.goto_next_sibling() {
795 did_visit_children = false;
796 } else if cursor.goto_parent() {
797 did_visit_children = true;
798 indent_level -= 1;
799 if !cursor.node().has_error() {
800 in_error = false;
801 }
802 } else {
803 break;
804 }
805 } else {
806 cst_render_node(
807 opts,
808 cursor,
809 source_code,
810 out,
811 total_width,
812 indent_level,
813 in_error,
814 )?;
815 if cursor.goto_first_child() {
816 did_visit_children = false;
817 indent_level += 1;
818 if cursor.node().has_error() {
819 in_error = true;
820 }
821 } else {
822 did_visit_children = true;
823 }
824 }
825 }
826 cursor.reset(tree.root_node());
827 Ok(())
828}
829
830struct CstNodeText<'a>(&'a str);
831
832impl std::fmt::Display for CstNodeText<'_> {
833 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
834 use std::fmt::Write as _;
835 for c in self.0.chars() {
836 match escape_invisible(c).or_else(|| escape_delimiter(c)) {
837 Some(esc) => f.write_str(esc)?,
838 None => f.write_char(c)?,
839 }
840 }
841 Ok(())
842 }
843}
844
845fn write_node_text(
846 opts: &ParseFileOptions,
847 out: &mut impl Write,
848 cursor: &TreeCursor,
849 is_named: bool,
850 source: &str,
851 color: Option<impl Into<Color> + Copy>,
852 text_info: (usize, usize),
853) -> io::Result<()> {
854 let (total_width, indent_level) = text_info;
855 let (quote, quote_color) = if is_named {
856 ('`', opts.parse_theme.backtick)
857 } else {
858 ('\"', color.map(std::convert::Into::into))
859 };
860
861 if !is_named {
862 write!(
863 out,
864 "{}{}{}",
865 paint(quote_color, quote),
866 paint(color, CstNodeText(source)),
867 paint(quote_color, quote),
868 )?;
869 } else {
870 let multiline = source.contains('\n');
871 for (i, line) in source.split_inclusive('\n').enumerate() {
872 if line.is_empty() {
873 break;
874 }
875 let mut node_range = cursor.node().range();
876 node_range.start_point.row += i;
879 node_range.end_point.row = node_range.start_point.row;
880 node_range.end_point.column = line.len()
881 + if i == 0 {
882 node_range.start_point.column
883 } else {
884 0
885 };
886 if multiline {
887 writeln!(out)?;
888 if !opts.no_ranges {
889 write!(
890 out,
891 "{}",
892 CstNodeRange {
893 opts,
894 has_field_name: cursor.field_name().is_some(),
895 is_named,
896 is_multiline: true,
897 total_width,
898 range: node_range,
899 }
900 )?;
901 }
902 for _ in 0..=indent_level {
903 write!(out, " ")?;
904 }
905 } else {
906 write!(out, " ")?;
907 }
908 write!(
909 out,
910 "{}{}{}",
911 paint(quote_color, quote),
912 paint(color, CstLineFeed { source: line, opts }),
913 paint(quote_color, quote),
914 )?;
915 }
916 }
917
918 Ok(())
919}
920
921struct CstLineFeed<'src, 'opt> {
922 source: &'src str,
923 opts: &'src ParseFileOptions<'opt>,
924}
925
926impl std::fmt::Display for CstLineFeed<'_, '_> {
927 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
928 #[cfg(windows)]
929 let lf = "\r\n";
930 #[cfg(not(windows))]
931 let lf = "\n";
932 let painted = paint(self.opts.parse_theme.line_feed, CstNodeText(lf));
933 let mut parts = self.source.split(lf);
934 if let Some(first) = parts.next() {
935 write!(f, "{}", CstNodeText(first))?;
936 }
937 for part in parts {
938 write!(f, "{painted}{}", CstNodeText(part))?;
939 }
940 Ok(())
941 }
942}
943
944struct CstNodeRange<'src, 'opt> {
945 opts: &'src ParseFileOptions<'opt>,
946 has_field_name: bool,
947 is_named: bool,
948 is_multiline: bool,
949 total_width: usize,
950 range: Range,
951}
952
953impl std::fmt::Display for CstNodeRange<'_, '_> {
954 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
955 let start = self.range.start_point;
956 let end = self.range.end_point;
957 let range_color = if self.is_named && !self.is_multiline && !self.has_field_name {
958 self.opts.parse_theme.row_color_named
959 } else {
960 self.opts.parse_theme.row_color
961 };
962 let remaining_width = |row: usize, col: usize| {
963 (self
964 .total_width
965 .saturating_sub(row.checked_ilog10().unwrap_or(0) as usize)
966 .saturating_sub(col.checked_ilog10().unwrap_or(0) as usize))
967 .max(1)
968 };
969 let remaining_width_start = remaining_width(start.row, start.column);
970 let remaining_width_end = remaining_width(end.row, end.column);
971 write!(
972 f,
973 "{}",
974 paint(
975 range_color,
976 format_args!(
977 "{}:{}{:remaining_width_start$}- {}:{}{:remaining_width_end$}",
978 start.row, start.column, ' ', end.row, end.column, ' ',
979 ),
980 )
981 )
982 }
983}
984
985fn cst_render_node(
986 opts: &ParseFileOptions,
987 cursor: &TreeCursor,
988 source_code: &[u8],
989 out: &mut impl Write,
990 total_width: usize,
991 indent_level: usize,
992 in_error: bool,
993) -> io::Result<()> {
994 let node = cursor.node();
995 let is_named = node.is_named();
996 if !opts.no_ranges {
997 write!(
998 out,
999 "{}",
1000 CstNodeRange {
1001 opts,
1002 has_field_name: cursor.field_name().is_some(),
1003 is_named,
1004 is_multiline: false,
1005 total_width,
1006 range: node.range(),
1007 }
1008 )?;
1009 }
1010 write!(
1011 out,
1012 "{}{}",
1013 " ".repeat(indent_level),
1014 if in_error && !node.has_error() {
1015 " "
1016 } else {
1017 ""
1018 }
1019 )?;
1020 if is_named {
1021 if let Some(field_name) = cursor.field_name() {
1022 write!(
1023 out,
1024 "{}",
1025 paint(opts.parse_theme.field, format_args!("{field_name}: "))
1026 )?;
1027 }
1028
1029 if node.has_error() || node.is_error() {
1030 write!(out, "{}", paint(opts.parse_theme.error, "•"))?;
1031 }
1032
1033 let kind_color = if node.is_error() {
1034 opts.parse_theme.error
1035 } else if node.is_extra() || node.parent().is_some_and(|p| p.is_extra() && !p.is_error()) {
1036 opts.parse_theme.extra
1037 } else {
1038 opts.parse_theme.node_kind
1039 };
1040 write!(out, "{}", paint(kind_color, node.kind()))?;
1041
1042 if node.child_count() == 0 {
1043 write_node_text(
1045 opts,
1046 out,
1047 cursor,
1048 is_named,
1049 &String::from_utf8_lossy(&source_code[node.start_byte()..node.end_byte()]),
1050 opts.parse_theme.node_text,
1051 (total_width, indent_level),
1052 )?;
1053 }
1054 } else if node.is_missing() {
1055 write!(out, "{}: ", paint(opts.parse_theme.missing, "MISSING"))?;
1056 write!(out, "\"{}\"", paint(opts.parse_theme.missing, node.kind()))?;
1057 } else {
1058 write_node_text(
1060 opts,
1061 out,
1062 cursor,
1063 is_named,
1064 node.kind(),
1065 opts.parse_theme.literal,
1066 (total_width, indent_level),
1067 )?;
1068 }
1069 writeln!(out)?;
1070
1071 Ok(())
1072}
1073
1074pub fn perform_edit(tree: &mut Tree, input: &mut Vec<u8>, edit: &Edit) -> Result<InputEdit> {
1075 let start_byte = edit.position;
1076 let old_end_byte = edit.position + edit.deleted_length;
1077 let new_end_byte = edit.position + edit.inserted_text.len();
1078 let start_position = position_for_offset(input, start_byte)?;
1079 let old_end_position = position_for_offset(input, old_end_byte)?;
1080 input.splice(start_byte..old_end_byte, edit.inserted_text.iter().copied());
1081 let new_end_position = position_for_offset(input, new_end_byte)?;
1082 let edit = InputEdit {
1083 start_byte,
1084 old_end_byte,
1085 new_end_byte,
1086 start_position,
1087 old_end_position,
1088 new_end_position,
1089 };
1090 tree.edit(&edit);
1091 Ok(edit)
1092}
1093
1094fn parse_edit_flag(source_code: &[u8], flag: &str) -> Result<Edit> {
1095 let error = || {
1096 anyhow!(
1097 concat!(
1098 "Invalid edit string '{}'. ",
1099 "Edit strings must match the pattern '<START_BYTE_OR_POSITION> <REMOVED_LENGTH> <NEW_TEXT>'"
1100 ),
1101 flag
1102 )
1103 };
1104
1105 let mut parts = flag.split(' ');
1110 let position = parts.next().ok_or_else(error)?;
1111 let deleted_length = parts.next().ok_or_else(error)?;
1112 let inserted_text = parts.collect::<Vec<_>>().join(" ").into_bytes();
1113
1114 let position = if position == "$" {
1116 source_code.len()
1117 } else if position.contains(',') {
1118 let mut parts = position.split(',');
1119 let row = parts.next().ok_or_else(error)?;
1120 let row = row.parse::<usize>().map_err(|_| error())?;
1121 let column = parts.next().ok_or_else(error)?;
1122 let column = column.parse::<usize>().map_err(|_| error())?;
1123 offset_for_position(source_code, Point { row, column })?
1124 } else {
1125 position.parse::<usize>().map_err(|_| error())?
1126 };
1127
1128 let deleted_length = deleted_length.parse::<usize>().map_err(|_| error())?;
1130
1131 Ok(Edit {
1132 position,
1133 deleted_length,
1134 inserted_text,
1135 })
1136}
1137
1138pub fn offset_for_position(input: &[u8], position: Point) -> Result<usize> {
1139 let mut row = 0;
1140 let mut line_start = 0;
1141 for line_end in memchr::memchr_iter(b'\n', input) {
1142 if row == position.row {
1143 if position.column > line_end - line_start {
1144 return Err(anyhow!("Failed to address a column: {}", position.column));
1145 }
1146 return Ok(line_start + position.column);
1147 }
1148 row += 1;
1149 line_start = line_end + 1;
1150 }
1151
1152 if row != position.row {
1153 return Err(anyhow!("Failed to address a row: {}", position.row));
1154 }
1155 if position.column > input.len() - line_start {
1156 return Err(anyhow!("Failed to address a column over the end"));
1157 }
1158 Ok(line_start + position.column)
1159}
1160
1161pub fn position_for_offset(input: &[u8], offset: usize) -> Result<Point> {
1162 if offset > input.len() {
1163 return Err(anyhow!("Failed to address an offset: {offset}"));
1164 }
1165 let mut result = Point { row: 0, column: 0 };
1166 let mut last = 0;
1167 for pos in memchr::memchr_iter(b'\n', &input[..offset]) {
1168 result.row += 1;
1169 last = pos;
1170 }
1171 result.column = if result.row > 0 {
1172 offset - last - 1
1173 } else {
1174 offset
1175 };
1176 Ok(result)
1177}
1178
1179#[cfg(test)]
1180mod tests {
1181 use super::{offset_for_position, parse_edit_flag};
1182 use tree_sitter::Point;
1183
1184 #[test]
1185 fn offset_for_position_uses_zero_based_line_and_column_coordinates() {
1186 let input = b"abc\n";
1187 assert_eq!(
1188 offset_for_position(input, Point { row: 0, column: 0 }).unwrap(),
1189 0
1190 );
1191 assert_eq!(
1192 offset_for_position(input, Point { row: 0, column: 1 }).unwrap(),
1193 1
1194 );
1195 assert_eq!(
1196 offset_for_position(input, Point { row: 0, column: 3 }).unwrap(),
1197 3
1198 );
1199 assert_eq!(
1200 offset_for_position(input, Point { row: 1, column: 0 }).unwrap(),
1201 4
1202 );
1203 }
1204
1205 #[test]
1206 fn offset_for_position_rejects_out_of_bounds_coordinates() {
1207 let input = b"abc\ndef";
1208 assert!(offset_for_position(input, Point { row: 0, column: 4 }).is_err());
1209 assert!(offset_for_position(input, Point { row: 2, column: 0 }).is_err());
1210 }
1211
1212 #[test]
1213 fn parse_edit_flag_resolves_first_line_positions() {
1214 let edit = parse_edit_flag(b"abc\n", "0,0 0 X").unwrap();
1215 assert_eq!(edit.position, 0);
1216 assert_eq!(edit.deleted_length, 0);
1217 assert_eq!(edit.inserted_text, b"X");
1218 }
1219}