Skip to main content

tree_sitter_cli/
parse.rs

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/// Sets the color used in the output of `tree-sitter parse --cst`
61#[derive(Debug, Copy, Clone)]
62pub struct ParseTheme {
63    /// The color of node kinds
64    pub node_kind: Option<Color>,
65    /// The color of text associated with a node
66    pub node_text: Option<Color>,
67    /// The color of node fields
68    pub field: Option<Color>,
69    /// The color of the range information for unnamed nodes
70    pub row_color: Option<Color>,
71    /// The color of the range information for named nodes
72    pub row_color_named: Option<Color>,
73    /// The color of extra nodes
74    pub extra: Option<Color>,
75    /// The color of ERROR nodes
76    pub error: Option<Color>,
77    /// The color of MISSING nodes and their associated text
78    pub missing: Option<Color>,
79    /// The color of newline characters
80    pub line_feed: Option<Color>,
81    /// The color of backticks
82    pub backtick: Option<Color>,
83    /// The color of literals
84    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/// A position in a multi-line text document, in terms of rows and columns.
194///
195/// Rows and columns are zero-based.
196///
197/// This serves as a serializable wrapper for `Point`
198#[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    // Render an HTML graph if `--debug-graph` was passed
298    if opts.debug_graph {
299        _log_session = Some(util::log_graphs(parser, "log.html", opts.open_log)?);
300    }
301    // Log to stderr if `--debug` was passed
302    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    // If the `--cancel` flag was passed, then cancel the parse
359    // when the user types a newline.
360    //
361    // Additionally, if the `--time` flag was passed, end the parse
362    // after the specified number of microseconds.
363    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 we're parsing the first file, write the header
526            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                        // we only write a line in the case where it's the last sibling
545                        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 we parsed the last file, write the closing tag for the `sources` header
614            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 there's no more children, even though some outer node has an error,
638                // then that means that the first error is hidden, but the later error could be
639                // visible. So, we walk back up to the child of the first node with an error,
640                // and then check its siblings for errors.
641                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            // For each line of text, adjust the row by shifting it down `i` rows,
877            // and adjust the column by setting it to the length of *this* line.
878            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            // Node text from a pattern or external scanner
1044            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        // Terminal literals, like "fn"
1059        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    // Three whitespace-separated parts:
1106    // * edit position
1107    // * deleted length
1108    // * inserted text
1109    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    // Position can either be a byte_offset or row,column pair, separated by a comma
1115    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    // Deleted length must be a byte count.
1129    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}