baz_difftastic/parse/
syntax.rs

1//! Syntax tree definitions with change metadata.
2
3#![allow(clippy::mutable_key_type)] // Hash for Syntax doesn't use mutable fields.
4
5use std::{cell::Cell, env, fmt, hash::Hash, num::NonZeroU32};
6
7use line_numbers::LinePositions;
8use line_numbers::SingleLineSpan;
9use typed_arena::Arena;
10
11use self::Syntax::*;
12use crate::words::split_words_and_numbers;
13use crate::{
14    diff::changes::ChangeKind,
15    diff::changes::{ChangeKind::*, ChangeMap},
16    diff::myers_diff,
17    hash::DftHashMap,
18    lines::is_all_whitespace,
19};
20
21/// A Debug implementation that does not recurse into the
22/// corresponding node mentioned for Unchanged. Otherwise we will
23/// infinitely loop on unchanged nodes, which both point to the other.
24impl<'a> fmt::Debug for ChangeKind<'a> {
25    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
26        let desc = match self {
27            Unchanged(node) => format!("Unchanged(ID: {})", node.id()),
28            ReplacedComment(lhs_node, rhs_node) | ReplacedString(lhs_node, rhs_node) => {
29                let change_kind = if let ReplacedComment(_, _) = self {
30                    "ReplacedComment"
31                } else {
32                    "ReplacedString"
33                };
34
35                format!(
36                    "{}(lhs ID: {}, rhs ID: {})",
37                    change_kind,
38                    lhs_node.id(),
39                    rhs_node.id()
40                )
41            }
42            Novel => "Novel".to_owned(),
43        };
44        f.write_str(&desc)
45    }
46}
47
48pub type SyntaxId = NonZeroU32;
49
50/// Fields that are common to both `Syntax::List` and `Syntax::Atom`.
51pub struct SyntaxInfo<'a> {
52    /// The previous node with the same parent as this one.
53    previous_sibling: Cell<Option<&'a Syntax<'a>>>,
54    /// The next node with the same parent as this one.
55    next_sibling: Cell<Option<&'a Syntax<'a>>>,
56    /// The syntax node that occurs before this one, in a depth-first
57    /// tree traversal.
58    prev: Cell<Option<&'a Syntax<'a>>>,
59    /// The parent syntax node, if present.
60    parent: Cell<Option<&'a Syntax<'a>>>,
61    /// The number of nodes that are ancestors of this one.
62    num_ancestors: Cell<u32>,
63    pub num_after: Cell<usize>,
64    /// A number that uniquely identifies this syntax node.
65    unique_id: Cell<SyntaxId>,
66    /// A number that uniquely identifies the content of this syntax
67    /// node. This may be the same as nodes on the other side of the
68    /// diff, or nodes at different positions.
69    ///
70    /// Values are sequential, not hashes. Collisions never occur.
71    content_id: Cell<u32>,
72    /// Is this the only node with this content? Ignores nodes on the
73    /// other side.
74    content_is_unique: Cell<bool>,
75}
76
77impl<'a> SyntaxInfo<'a> {
78    pub fn new() -> Self {
79        Self {
80            previous_sibling: Cell::new(None),
81            next_sibling: Cell::new(None),
82            prev: Cell::new(None),
83            parent: Cell::new(None),
84            num_ancestors: Cell::new(0),
85            num_after: Cell::new(0),
86            unique_id: Cell::new(NonZeroU32::new(u32::MAX).unwrap()),
87            content_id: Cell::new(0),
88            content_is_unique: Cell::new(false),
89        }
90    }
91}
92
93impl<'a> Default for SyntaxInfo<'a> {
94    fn default() -> Self {
95        Self::new()
96    }
97}
98
99pub enum Syntax<'a> {
100    List {
101        info: SyntaxInfo<'a>,
102        open_position: Vec<SingleLineSpan>,
103        open_content: String,
104        children: Vec<&'a Syntax<'a>>,
105        close_position: Vec<SingleLineSpan>,
106        close_content: String,
107        num_descendants: u32,
108    },
109    Atom {
110        info: SyntaxInfo<'a>,
111        position: Vec<SingleLineSpan>,
112        content: String,
113        kind: AtomKind,
114    },
115}
116
117fn dbg_pos(pos: &[SingleLineSpan]) -> String {
118    match pos {
119        [] => "-".into(),
120        [pos] => format!("{}:{}-{}", pos.line.0, pos.start_col, pos.end_col),
121        [start, .., end] => format!(
122            "{}:{}-{}:{}",
123            start.line.0, start.start_col, end.line.0, end.end_col
124        ),
125    }
126}
127
128impl<'a> fmt::Debug for Syntax<'a> {
129    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130        match self {
131            List {
132                open_content,
133                open_position,
134                children,
135                close_content,
136                close_position,
137                info,
138                ..
139            } => {
140                let mut ds = f.debug_struct(&format!(
141                    "List id:{} content:{}",
142                    self.id(),
143                    self.content_id()
144                ));
145
146                ds.field("open_content", &open_content)
147                    .field("open_position", &dbg_pos(open_position))
148                    .field("children", &children)
149                    .field("close_content", &close_content)
150                    .field("close_position", &dbg_pos(close_position));
151
152                if env::var("DFT_VERBOSE").is_ok() {
153                    let next_sibling_s = match info.next_sibling.get() {
154                        Some(List { .. }) => "Some(List)",
155                        Some(Atom { .. }) => "Some(Atom)",
156                        None => "None",
157                    };
158                    ds.field("next_sibling", &next_sibling_s);
159                }
160
161                ds.finish()
162            }
163            Atom {
164                content,
165                position,
166                info,
167                kind: highlight,
168                ..
169            } => {
170                let mut ds = f.debug_struct(&format!(
171                    "Atom id:{} content:{}",
172                    self.id(),
173                    self.content_id()
174                ));
175                ds.field("content", &content);
176                ds.field("position", &dbg_pos(position));
177
178                if env::var("DFT_VERBOSE").is_ok() {
179                    ds.field("highlight", highlight);
180                    let next_sibling_s = match info.next_sibling.get() {
181                        Some(List { .. }) => "Some(List)",
182                        Some(Atom { .. }) => "Some(Atom)",
183                        None => "None",
184                    };
185                    ds.field("next_sibling", &next_sibling_s);
186                }
187
188                ds.finish()
189            }
190        }
191    }
192}
193
194impl<'a> Syntax<'a> {
195    pub fn new_list(
196        arena: &'a Arena<Syntax<'a>>,
197        open_content: &str,
198        open_position: Vec<SingleLineSpan>,
199        children: Vec<&'a Syntax<'a>>,
200        close_content: &str,
201        close_position: Vec<SingleLineSpan>,
202    ) -> &'a Syntax<'a> {
203        // Skip empty atoms: they aren't displayed, so there's no
204        // point making our syntax tree bigger. These occur when we're
205        // parsing incomplete or malformed programs.
206        let children = children
207            .into_iter()
208            .filter(|n| match n {
209                List { .. } => true,
210                Atom { content, .. } => !content.is_empty(),
211            })
212            .collect::<Vec<_>>();
213
214        // Don't bother creating a list if we have no open/close and
215        // there's only one child. This occurs in small files with
216        // thorough tree-sitter parsers: you get parse trees like:
217        //
218        // (compilation-unit (top-level-def (function ...)))
219        //
220        // This is a small performance win as it makes the difftastic
221        // syntax tree smaller. It also really helps when looking at
222        // debug output for small inputs.
223        if children.len() == 1 && open_content.is_empty() && close_content.is_empty() {
224            return children[0];
225        }
226
227        let mut num_descendants = 0;
228        for child in &children {
229            num_descendants += match child {
230                List {
231                    num_descendants, ..
232                } => *num_descendants + 1,
233                Atom { .. } => 1,
234            };
235        }
236
237        arena.alloc(List {
238            info: SyntaxInfo::default(),
239            open_position,
240            open_content: open_content.into(),
241            close_content: close_content.into(),
242            close_position,
243            children,
244            num_descendants,
245        })
246    }
247
248    pub fn new_atom(
249        arena: &'a Arena<Syntax<'a>>,
250        mut position: Vec<SingleLineSpan>,
251        mut content: &str,
252        kind: AtomKind,
253    ) -> &'a Syntax<'a> {
254        // If a parser hasn't cleaned up \r on CRLF files with
255        // comments, discard it.
256        if content.ends_with('\r') {
257            content = &content[..content.len() - 1];
258        }
259
260        if kind == AtomKind::Comment && content.ends_with('\n') {
261            position.pop();
262            content = &content[..content.len() - 1];
263        }
264
265        arena.alloc(Atom {
266            info: SyntaxInfo::default(),
267            position,
268            content: content.into(),
269            kind,
270        })
271    }
272
273    pub fn info(&self) -> &SyntaxInfo<'a> {
274        match self {
275            List { info, .. } | Atom { info, .. } => info,
276        }
277    }
278
279    pub fn parent(&self) -> Option<&'a Syntax<'a>> {
280        self.info().parent.get()
281    }
282
283    pub fn next_sibling(&self) -> Option<&'a Syntax<'a>> {
284        self.info().next_sibling.get()
285    }
286
287    /// A unique ID of this syntax node. Every node is guaranteed to
288    /// have a different value.
289    pub fn id(&self) -> SyntaxId {
290        self.info().unique_id.get()
291    }
292
293    /// A content ID of this syntax node. Two nodes have the same
294    /// content ID if they have the same content, regardless of
295    /// position.
296    pub fn content_id(&self) -> u32 {
297        self.info().content_id.get()
298    }
299
300    pub fn content_is_unique(&self) -> bool {
301        self.info().content_is_unique.get()
302    }
303
304    pub fn num_ancestors(&self) -> u32 {
305        self.info().num_ancestors.get()
306    }
307
308    pub fn dbg_content(&self) -> String {
309        match self {
310            List {
311                open_content,
312                open_position,
313                close_content,
314                ..
315            } => {
316                let line = open_position
317                    .first()
318                    .map(|p| p.line.display())
319                    .unwrap_or_else(|| "?".to_owned());
320
321                format!("line:{} {} ... {}", line, open_content, close_content)
322            }
323            Atom {
324                content, position, ..
325            } => {
326                let line = position
327                    .first()
328                    .map_or_else(|| "?".to_owned(), |p| p.line.display());
329
330                format!("line:{} {}", line, content)
331            }
332        }
333    }
334}
335
336pub fn comment_positions<'a>(nodes: &[&'a Syntax<'a>]) -> Vec<SingleLineSpan> {
337    fn walk_comment_positions(node: &Syntax<'_>, positions: &mut Vec<SingleLineSpan>) {
338        match node {
339            List { children, .. } => {
340                for child in children {
341                    walk_comment_positions(child, positions);
342                }
343            }
344            Atom { position, kind, .. } => {
345                if matches!(kind, AtomKind::Comment) {
346                    positions.extend(position);
347                }
348            }
349        }
350    }
351
352    let mut positions = vec![];
353    for node in nodes {
354        walk_comment_positions(node, &mut positions);
355    }
356
357    positions
358}
359
360/// Initialise all the fields in `SyntaxInfo`.
361pub fn init_all_info<'a>(lhs_roots: &[&'a Syntax<'a>], rhs_roots: &[&'a Syntax<'a>]) {
362    init_info(lhs_roots, rhs_roots);
363    init_next_prev(lhs_roots);
364    init_next_prev(rhs_roots);
365}
366
367fn init_info<'a>(lhs_roots: &[&'a Syntax<'a>], rhs_roots: &[&'a Syntax<'a>]) {
368    let mut id = NonZeroU32::new(1).unwrap();
369    init_info_on_side(lhs_roots, &mut id);
370    init_info_on_side(rhs_roots, &mut id);
371
372    let mut existing = DftHashMap::default();
373    set_content_id(lhs_roots, &mut existing);
374    set_content_id(rhs_roots, &mut existing);
375
376    set_content_is_unique(lhs_roots);
377    set_content_is_unique(rhs_roots);
378}
379
380type ContentKey = (Option<String>, Option<String>, Vec<u32>, bool, bool);
381
382fn set_content_id(nodes: &[&Syntax], existing: &mut DftHashMap<ContentKey, u32>) {
383    for node in nodes {
384        let key: ContentKey = match node {
385            List {
386                open_content,
387                close_content,
388                children,
389                ..
390            } => {
391                // Recurse first, so children all have their content_id set.
392                set_content_id(children, existing);
393
394                let children_content_ids: Vec<_> =
395                    children.iter().map(|c| c.info().content_id.get()).collect();
396
397                (
398                    Some(open_content.clone()),
399                    Some(close_content.clone()),
400                    children_content_ids,
401                    true,
402                    true,
403                )
404            }
405            Atom {
406                content,
407                kind: highlight,
408                ..
409            } => {
410                let is_comment = *highlight == AtomKind::Comment;
411                let clean_content = if is_comment && content.lines().count() > 1 {
412                    content
413                        .lines()
414                        .map(|l| l.trim_start())
415                        .collect::<Vec<_>>()
416                        .join("\n")
417                        .to_string()
418                } else {
419                    content.clone()
420                };
421                (Some(clean_content), None, vec![], false, is_comment)
422            }
423        };
424
425        // Ensure the ID is always greater than zero, so we can
426        // distinguish an uninitialised SyntaxInfo value.
427        let next_id = existing.len() as u32 + 1;
428        let content_id = existing.entry(key).or_insert(next_id);
429        node.info().content_id.set(*content_id);
430    }
431}
432
433fn set_num_after(nodes: &[&Syntax], parent_num_after: usize) {
434    for (i, node) in nodes.iter().enumerate() {
435        let num_after = parent_num_after + nodes.len() - 1 - i;
436        node.info().num_after.set(num_after);
437
438        if let List { children, .. } = node {
439            set_num_after(children, num_after);
440        }
441    }
442}
443pub fn init_next_prev<'a>(roots: &[&'a Syntax<'a>]) {
444    set_prev_sibling(roots);
445    set_next_sibling(roots);
446    set_prev(roots, None);
447}
448
449/// Set all the `SyntaxInfo` values for all the `roots` on a single
450/// side (LHS or RHS).
451fn init_info_on_side<'a>(roots: &[&'a Syntax<'a>], next_id: &mut SyntaxId) {
452    set_parent(roots, None);
453    set_num_ancestors(roots, 0);
454    set_num_after(roots, 0);
455    set_unique_id(roots, next_id);
456}
457
458fn set_unique_id(nodes: &[&Syntax], next_id: &mut SyntaxId) {
459    for node in nodes {
460        node.info().unique_id.set(*next_id);
461        *next_id = NonZeroU32::new(u32::from(*next_id) + 1)
462            .expect("Should not have more than u32::MAX nodes");
463        if let List { children, .. } = node {
464            set_unique_id(children, next_id);
465        }
466    }
467}
468
469/// Assumes that `set_content_id` has already run.
470fn find_nodes_with_unique_content(nodes: &[&Syntax], counts: &mut DftHashMap<u32, usize>) {
471    for node in nodes {
472        *counts.entry(node.content_id()).or_insert(0) += 1;
473        if let List { children, .. } = node {
474            find_nodes_with_unique_content(children, counts);
475        }
476    }
477}
478
479fn set_content_is_unique_from_counts(nodes: &[&Syntax], counts: &DftHashMap<u32, usize>) {
480    for node in nodes {
481        let count = counts
482            .get(&node.content_id())
483            .expect("Count should be present");
484        node.info().content_is_unique.set(*count == 1);
485
486        if let List { children, .. } = node {
487            set_content_is_unique_from_counts(children, counts);
488        }
489    }
490}
491
492fn set_content_is_unique(nodes: &[&Syntax]) {
493    let mut counts = DftHashMap::default();
494    find_nodes_with_unique_content(nodes, &mut counts);
495    set_content_is_unique_from_counts(nodes, &counts);
496}
497
498fn set_prev_sibling<'a>(nodes: &[&'a Syntax<'a>]) {
499    let mut prev = None;
500
501    for node in nodes {
502        node.info().previous_sibling.set(prev);
503        prev = Some(node);
504
505        if let List { children, .. } = node {
506            set_prev_sibling(children);
507        }
508    }
509}
510
511fn set_next_sibling<'a>(nodes: &[&'a Syntax<'a>]) {
512    for (i, node) in nodes.iter().enumerate() {
513        let sibling = nodes.get(i + 1).copied();
514        node.info().next_sibling.set(sibling);
515
516        if let List { children, .. } = node {
517            set_next_sibling(children);
518        }
519    }
520}
521
522/// For every syntax node in the tree, mark the previous node
523/// according to a preorder traversal.
524fn set_prev<'a>(nodes: &[&'a Syntax<'a>], parent: Option<&'a Syntax<'a>>) {
525    for (i, node) in nodes.iter().enumerate() {
526        let node_prev = if i == 0 { parent } else { Some(nodes[i - 1]) };
527
528        node.info().prev.set(node_prev);
529        if let List { children, .. } = node {
530            set_prev(children, Some(node));
531        }
532    }
533}
534
535fn set_parent<'a>(nodes: &[&'a Syntax<'a>], parent: Option<&'a Syntax<'a>>) {
536    for node in nodes {
537        node.info().parent.set(parent);
538        if let List { children, .. } = node {
539            set_parent(children, Some(node));
540        }
541    }
542}
543
544fn set_num_ancestors(nodes: &[&Syntax], num_ancestors: u32) {
545    for node in nodes {
546        node.info().num_ancestors.set(num_ancestors);
547
548        if let List { children, .. } = node {
549            set_num_ancestors(children, num_ancestors + 1);
550        }
551    }
552}
553
554impl<'a> PartialEq for Syntax<'a> {
555    fn eq(&self, other: &Self) -> bool {
556        debug_assert!(self.content_id() > 0);
557        debug_assert!(other.content_id() > 0);
558        self.content_id() == other.content_id()
559    }
560}
561impl<'a> Eq for Syntax<'a> {}
562
563/// Different types of strings. We want to diff these the same way,
564/// but highlight them differently.
565#[derive(PartialEq, Eq, Debug, Clone, Copy, Hash)]
566pub enum StringKind {
567    /// A string literal, such as `"foo"`.
568    StringLiteral,
569    /// Plain text, such as the content of `<p>foo</p>`.
570    Text,
571}
572
573#[derive(PartialEq, Eq, Debug, Clone, Copy, Hash)]
574pub enum AtomKind {
575    Normal,
576    // TODO: We should either have a AtomWithWords(HighlightKind) or a
577    // separate String, Text and Comment kind.
578    String(StringKind),
579    Type,
580    Comment,
581    Keyword,
582    TreeSitterError,
583}
584
585/// Unlike atoms, tokens can be delimiters like `{`.
586#[derive(PartialEq, Eq, Debug, Clone, Copy)]
587pub enum TokenKind {
588    Delimiter,
589    Atom(AtomKind),
590}
591
592/// A matched token (an atom, a delimiter, or a comment word).
593#[derive(PartialEq, Eq, Debug, Clone)]
594pub enum MatchKind {
595    UnchangedToken {
596        highlight: TokenKind,
597        self_pos: Vec<SingleLineSpan>,
598        opposite_pos: Vec<SingleLineSpan>,
599    },
600    Novel {
601        highlight: TokenKind,
602    },
603    NovelLinePart {
604        highlight: TokenKind,
605        self_pos: SingleLineSpan,
606        opposite_pos: Vec<SingleLineSpan>,
607    },
608    NovelWord {
609        highlight: TokenKind,
610    },
611    Ignored {
612        highlight: TokenKind,
613    },
614}
615
616impl MatchKind {
617    pub fn is_novel(&self) -> bool {
618        matches!(
619            self,
620            MatchKind::Novel { .. } | MatchKind::NovelWord { .. } | MatchKind::NovelLinePart { .. }
621        )
622    }
623}
624
625#[derive(Debug, Clone, PartialEq, Eq)]
626pub struct MatchedPos {
627    pub kind: MatchKind,
628    pub pos: SingleLineSpan,
629}
630
631/// Given the text `content` from a comment or strings, split it into
632/// MatchedPos values for the novel and unchanged words.
633///
634/// If there is negligible text in common with `opposite_content`,
635/// treat the whole `content` as a single novel region.
636fn split_atom_words(
637    content: &str,
638    pos: &[SingleLineSpan],
639    opposite_content: &str,
640    opposite_pos: &[SingleLineSpan],
641    kind: AtomKind,
642) -> Vec<MatchedPos> {
643    debug_assert!(kind == AtomKind::Comment || matches!(kind, AtomKind::String(_)));
644
645    // TODO: merge adjacent single-line comments unless there are
646    // blank lines between them.
647    let content_parts = split_words_and_numbers(content);
648    let other_parts = split_words_and_numbers(opposite_content);
649
650    let word_diffs = myers_diff::slice_by_hash(&content_parts, &other_parts);
651
652    if !has_common_words(&word_diffs) {
653        return pos
654            .iter()
655            .map(|line| MatchedPos {
656                kind: MatchKind::Novel {
657                    highlight: TokenKind::Atom(kind),
658                },
659                pos: *line,
660            })
661            .collect();
662    }
663
664    let content_newlines = LinePositions::from(content);
665    let opposite_content_newlines = LinePositions::from(opposite_content);
666
667    let mut offset = 0;
668    let mut opposite_offset = 0;
669
670    let mut mps = vec![];
671    for diff_res in word_diffs {
672        match diff_res {
673            myers_diff::DiffResult::Left(word) => {
674                // This word is novel to this side.
675                if !is_all_whitespace(word) {
676                    mps.push(MatchedPos {
677                        kind: MatchKind::NovelWord {
678                            highlight: TokenKind::Atom(kind),
679                        },
680                        pos: content_newlines.from_region_relative_to(
681                            // TODO: don't assume a single line atom.
682                            pos[0],
683                            offset,
684                            offset + word.len(),
685                        )[0],
686                    });
687                }
688                offset += word.len();
689            }
690            myers_diff::DiffResult::Both(word, opposite_word) => {
691                // This word is present on both sides.
692                // TODO: don't assume this atom is on a single line.
693                let word_pos =
694                    content_newlines.from_region_relative_to(pos[0], offset, offset + word.len())
695                        [0];
696                let opposite_word_pos = opposite_content_newlines.from_region_relative_to(
697                    opposite_pos[0],
698                    opposite_offset,
699                    opposite_offset + opposite_word.len(),
700                );
701
702                mps.push(MatchedPos {
703                    kind: MatchKind::NovelLinePart {
704                        highlight: TokenKind::Atom(kind),
705                        self_pos: word_pos,
706                        opposite_pos: opposite_word_pos,
707                    },
708                    pos: word_pos,
709                });
710                offset += word.len();
711                opposite_offset += opposite_word.len();
712            }
713            myers_diff::DiffResult::Right(opposite_word) => {
714                // Only exists on other side, nothing to do on this side.
715                opposite_offset += opposite_word.len();
716            }
717        }
718    }
719
720    mps
721}
722
723/// Are there sufficient common words that we should only highlight
724/// individual changed words?
725fn has_common_words(word_diffs: &Vec<myers_diff::DiffResult<&&str>>) -> bool {
726    let mut novel_count = 0;
727    let mut unchanged_count = 0;
728
729    for word_diff in word_diffs {
730        match word_diff {
731            myers_diff::DiffResult::Both(word, _) => {
732                if **word != " " {
733                    unchanged_count += 1;
734                }
735            }
736            _ => {
737                novel_count += 1;
738            }
739        }
740    }
741
742    // We want more than two unchanged words, because the text content
743    // includes the comment or string delimiters.
744    //
745    // A sufficiently similar set of words is when more than 50% of
746    // the words are common between the two sides. We multiply by two
747    // because non-matching words gives us two novel words, whereas
748    // matched words only gives us one unchanged word.
749    unchanged_count > 2 && unchanged_count * 2 >= novel_count
750}
751
752/// Skip line spans at the beginning or end that have zero width.
753fn filter_empty_ends(line_spans: &[SingleLineSpan]) -> Vec<SingleLineSpan> {
754    let mut spans: Vec<SingleLineSpan> = vec![];
755
756    for (i, span) in line_spans.iter().enumerate() {
757        if (i == 0 || i == line_spans.len() - 1) && span.start_col == span.end_col {
758            continue;
759        }
760
761        spans.push(*span);
762    }
763
764    spans
765}
766
767impl MatchedPos {
768    fn new(
769        ck: ChangeKind,
770        highlight: TokenKind,
771        pos: &[SingleLineSpan],
772        is_close_delim: bool,
773    ) -> Vec<Self> {
774        // Don't create a MatchedPos for empty positions at the start
775        // or end. We still want empty positions in the middle of
776        // multiline atoms, as a multiline string literal may include
777        // empty lines.
778        let pos = filter_empty_ends(pos);
779
780        match ck {
781            ReplacedComment(this, opposite) | ReplacedString(this, opposite) => {
782                let this_content = match this {
783                    List { .. } => unreachable!(),
784                    Atom { content, .. } => content,
785                };
786                let (opposite_content, opposite_pos) = match opposite {
787                    List { .. } => unreachable!(),
788                    Atom {
789                        content, position, ..
790                    } => (content, position),
791                };
792
793                let kind = if let ReplacedString(this, _) = ck {
794                    match this {
795                        Atom {
796                            kind: AtomKind::String(StringKind::Text),
797                            ..
798                        } => AtomKind::String(StringKind::Text),
799                        _ => AtomKind::String(StringKind::StringLiteral),
800                    }
801                } else {
802                    AtomKind::Comment
803                };
804
805                split_atom_words(this_content, &pos, opposite_content, opposite_pos, kind)
806            }
807            Unchanged(opposite) => {
808                let opposite_pos = match opposite {
809                    List {
810                        open_position,
811                        close_position,
812                        ..
813                    } => {
814                        if is_close_delim {
815                            close_position.clone()
816                        } else {
817                            open_position.clone()
818                        }
819                    }
820                    Atom { position, .. } => position.clone(),
821                };
822
823                let opposite_pos_len = opposite_pos.len();
824                let kind = MatchKind::UnchangedToken {
825                    highlight,
826                    self_pos: pos.to_vec(),
827                    opposite_pos,
828                };
829
830                // Create a MatchedPos for every line that `pos` covers.
831                let mut mps = vec![];
832                for line_pos in &pos {
833                    mps.push(Self {
834                        kind: kind.clone(),
835                        pos: *line_pos,
836                    });
837
838                    // Ensure we have the same number of unchanged
839                    // MatchedPos on the LHS and RHS. This allows us
840                    // to consider unchanged MatchedPos values
841                    // pairwise.
842                    if mps.len() == opposite_pos_len {
843                        break;
844                    }
845                }
846                mps
847            }
848            Novel => {
849                let kind = MatchKind::Novel { highlight };
850                // Create a MatchedPos for every line that `pos` covers.
851                let mut mps = vec![];
852                for line_pos in &pos {
853                    // Don't create a MatchedPos for entirely empty positions. This
854                    // occurs when we have lists with empty open/close
855                    // delimiter positions, such as the top-level list of syntax items.
856                    if pos.len() == 1 && line_pos.start_col == line_pos.end_col {
857                        continue;
858                    }
859
860                    mps.push(Self {
861                        kind: kind.clone(),
862                        pos: *line_pos,
863                    });
864                }
865
866                mps
867            }
868        }
869    }
870}
871
872/// Walk `nodes` and return a vec of all the changed positions.
873pub fn change_positions<'a>(
874    nodes: &[&'a Syntax<'a>],
875    change_map: &ChangeMap<'a>,
876) -> Vec<MatchedPos> {
877    let mut positions = Vec::new();
878    change_positions_(nodes, change_map, &mut positions);
879    positions
880}
881
882fn change_positions_<'a>(
883    nodes: &[&'a Syntax<'a>],
884    change_map: &ChangeMap<'a>,
885    positions: &mut Vec<MatchedPos>,
886) {
887    for node in nodes {
888        let change = change_map
889            .get(node)
890            .unwrap_or_else(|| panic!("Should have changes set in all nodes: {:#?}", node));
891
892        match node {
893            List {
894                open_position,
895                children,
896                close_position,
897                ..
898            } => {
899                positions.extend(MatchedPos::new(
900                    change,
901                    TokenKind::Delimiter,
902                    open_position,
903                    false,
904                ));
905
906                change_positions_(children, change_map, positions);
907
908                positions.extend(MatchedPos::new(
909                    change,
910                    TokenKind::Delimiter,
911                    close_position,
912                    true,
913                ));
914            }
915            Atom { position, kind, .. } => {
916                positions.extend(MatchedPos::new(
917                    change,
918                    TokenKind::Atom(*kind),
919                    position,
920                    false,
921                ));
922            }
923        }
924    }
925}
926
927pub fn zip_pad_shorter<Tx: Clone, Ty: Clone>(
928    lhs: &[Tx],
929    rhs: &[Ty],
930) -> Vec<(Option<Tx>, Option<Ty>)> {
931    let mut res = vec![];
932
933    let mut lhs_iter = lhs.iter();
934    let mut rhs_iter = rhs.iter();
935    loop {
936        match (lhs_iter.next(), rhs_iter.next()) {
937            (None, None) => break,
938            (x, y) => res.push((x.cloned(), y.cloned())),
939        }
940    }
941
942    res
943}
944
945/// Zip `lhs` with `rhs`, but repeat the last item from the shorter
946/// slice.
947pub fn zip_repeat_shorter<Tx: Clone, Ty: Clone>(lhs: &[Tx], rhs: &[Ty]) -> Vec<(Tx, Ty)> {
948    let lhs_last: Tx = match lhs.last() {
949        Some(last) => last.clone(),
950        None => return vec![],
951    };
952    let rhs_last: Ty = match rhs.last() {
953        Some(last) => last.clone(),
954        None => return vec![],
955    };
956
957    let mut res = vec![];
958    let mut lhs_iter = lhs.iter();
959    let mut rhs_iter = rhs.iter();
960    loop {
961        match (lhs_iter.next(), rhs_iter.next()) {
962            (None, None) => break,
963            (x, y) => res.push((
964                x.cloned().unwrap_or_else(|| lhs_last.clone()),
965                y.cloned().unwrap_or_else(|| rhs_last.clone()),
966            )),
967        }
968    }
969
970    res
971}
972
973#[cfg(test)]
974mod tests {
975    use pretty_assertions::assert_eq;
976
977    use super::*;
978
979    /// Consider comment atoms as distinct to other atoms even if the
980    /// content matches otherwise.
981    #[test]
982    fn test_comment_and_atom_differ() {
983        let pos = vec![SingleLineSpan {
984            line: 0.into(),
985            start_col: 2,
986            end_col: 3,
987        }];
988
989        let arena = Arena::new();
990
991        let comment = Syntax::new_atom(&arena, pos.clone(), "foo", AtomKind::Comment);
992        let atom = Syntax::new_atom(&arena, pos, "foo", AtomKind::Normal);
993        init_all_info(&[comment], &[atom]);
994
995        assert_ne!(comment, atom);
996    }
997
998    #[test]
999    fn test_new_atom_truncates_carriage_return() {
1000        let arena = Arena::new();
1001        let position = vec![];
1002        let content = "foo\r";
1003
1004        let atom = Syntax::new_atom(&arena, position, content, AtomKind::Comment);
1005
1006        match atom {
1007            List { .. } => unreachable!(),
1008            Atom { content, .. } => {
1009                assert_eq!(content, "foo");
1010            }
1011        }
1012    }
1013
1014    #[test]
1015    fn test_new_atom_truncates_trailing_newline() {
1016        let arena = Arena::new();
1017        let position = vec![
1018            SingleLineSpan {
1019                line: 0.into(),
1020                start_col: 0,
1021                end_col: 8,
1022            },
1023            SingleLineSpan {
1024                line: 1.into(),
1025                start_col: 0,
1026                end_col: 1,
1027            },
1028        ];
1029        let content = ";; hello\n";
1030
1031        let atom = Syntax::new_atom(&arena, position, content, AtomKind::Comment);
1032
1033        match atom {
1034            List { .. } => unreachable!(),
1035            Atom {
1036                position, content, ..
1037            } => {
1038                assert_eq!(content, ";; hello");
1039                assert_eq!(
1040                    *position,
1041                    vec![SingleLineSpan {
1042                        line: 0.into(),
1043                        start_col: 0,
1044                        end_col: 8,
1045                    }]
1046                );
1047            }
1048        }
1049    }
1050
1051    /// Ignore the syntax highighting kind when comparing
1052    /// atoms. Sometimes changing delimiter wrapping can change
1053    /// whether a parser thinks that a node is e.g. a type.
1054    #[test]
1055    fn test_atom_equality_ignores_highlighting() {
1056        let pos = vec![SingleLineSpan {
1057            line: 0.into(),
1058            start_col: 2,
1059            end_col: 3,
1060        }];
1061
1062        let arena = Arena::new();
1063
1064        let type_atom = Syntax::new_atom(&arena, pos.clone(), "foo", AtomKind::Type);
1065        let atom = Syntax::new_atom(&arena, pos, "foo", AtomKind::Normal);
1066        init_all_info(&[type_atom], &[atom]);
1067
1068        assert_eq!(type_atom, atom);
1069    }
1070
1071    #[test]
1072    fn test_flatten_trivial_list() {
1073        let pos = vec![SingleLineSpan {
1074            line: 0.into(),
1075            start_col: 2,
1076            end_col: 3,
1077        }];
1078
1079        let arena = Arena::new();
1080        let atom = Syntax::new_atom(&arena, pos, "foo", AtomKind::Normal);
1081
1082        let trivial_list = Syntax::new_list(&arena, "", vec![], vec![atom], "", vec![]);
1083
1084        assert!(matches!(trivial_list, Atom { .. }));
1085    }
1086
1087    #[test]
1088    fn test_ignore_empty_atoms() {
1089        let pos = vec![SingleLineSpan {
1090            line: 0.into(),
1091            start_col: 2,
1092            end_col: 2,
1093        }];
1094
1095        let arena = Arena::new();
1096        let atom = Syntax::new_atom(&arena, pos, "", AtomKind::Normal);
1097
1098        let trivial_list = Syntax::new_list(&arena, "(", vec![], vec![atom], ")", vec![]);
1099
1100        match trivial_list {
1101            List { children, .. } => {
1102                assert_eq!(children.len(), 0);
1103            }
1104            Atom { .. } => unreachable!(),
1105        }
1106    }
1107
1108    #[test]
1109    fn test_multiline_comment_ignores_leading_whitespace() {
1110        let pos = vec![SingleLineSpan {
1111            line: 0.into(),
1112            start_col: 2,
1113            end_col: 3,
1114        }];
1115
1116        let arena = Arena::new();
1117
1118        let x = Syntax::new_atom(&arena, pos.clone(), "foo\nbar", AtomKind::Comment);
1119        let y = Syntax::new_atom(&arena, pos, "foo\n    bar", AtomKind::Comment);
1120        init_all_info(&[x], &[y]);
1121
1122        assert_eq!(x, y);
1123    }
1124
1125    #[test]
1126    fn test_split_atom_words() {
1127        let content = "abc def ghi novel";
1128        let pos = vec![SingleLineSpan {
1129            line: 0.into(),
1130            start_col: 0,
1131            end_col: 17,
1132        }];
1133
1134        let opposite_content = "abc def ghi";
1135        let opposite_pos = vec![SingleLineSpan {
1136            line: 0.into(),
1137            start_col: 0,
1138            end_col: 11,
1139        }];
1140
1141        let res = split_atom_words(
1142            content,
1143            &pos,
1144            opposite_content,
1145            &opposite_pos,
1146            AtomKind::Comment,
1147        );
1148        assert_eq!(
1149            res,
1150            vec![
1151                MatchedPos {
1152                    kind: MatchKind::NovelLinePart {
1153                        highlight: TokenKind::Atom(AtomKind::Comment),
1154                        self_pos: SingleLineSpan {
1155                            line: 0.into(),
1156                            start_col: 0,
1157                            end_col: 3
1158                        },
1159                        opposite_pos: vec![SingleLineSpan {
1160                            line: 0.into(),
1161                            start_col: 0,
1162                            end_col: 3
1163                        }]
1164                    },
1165                    pos: SingleLineSpan {
1166                        line: 0.into(),
1167                        start_col: 0,
1168                        end_col: 3
1169                    }
1170                },
1171                MatchedPos {
1172                    kind: MatchKind::NovelLinePart {
1173                        highlight: TokenKind::Atom(AtomKind::Comment),
1174                        self_pos: SingleLineSpan {
1175                            line: 0.into(),
1176                            start_col: 3,
1177                            end_col: 4
1178                        },
1179                        opposite_pos: vec![SingleLineSpan {
1180                            line: 0.into(),
1181                            start_col: 3,
1182                            end_col: 4
1183                        }]
1184                    },
1185                    pos: SingleLineSpan {
1186                        line: 0.into(),
1187                        start_col: 3,
1188                        end_col: 4
1189                    }
1190                },
1191                MatchedPos {
1192                    kind: MatchKind::NovelLinePart {
1193                        highlight: TokenKind::Atom(AtomKind::Comment),
1194                        self_pos: SingleLineSpan {
1195                            line: 0.into(),
1196                            start_col: 4,
1197                            end_col: 7
1198                        },
1199                        opposite_pos: vec![SingleLineSpan {
1200                            line: 0.into(),
1201                            start_col: 4,
1202                            end_col: 7
1203                        }]
1204                    },
1205                    pos: SingleLineSpan {
1206                        line: 0.into(),
1207                        start_col: 4,
1208                        end_col: 7
1209                    }
1210                },
1211                MatchedPos {
1212                    kind: MatchKind::NovelLinePart {
1213                        highlight: TokenKind::Atom(AtomKind::Comment),
1214                        self_pos: SingleLineSpan {
1215                            line: 0.into(),
1216                            start_col: 7,
1217                            end_col: 8
1218                        },
1219                        opposite_pos: vec![SingleLineSpan {
1220                            line: 0.into(),
1221                            start_col: 7,
1222                            end_col: 8
1223                        }]
1224                    },
1225                    pos: SingleLineSpan {
1226                        line: 0.into(),
1227                        start_col: 7,
1228                        end_col: 8
1229                    }
1230                },
1231                MatchedPos {
1232                    kind: MatchKind::NovelLinePart {
1233                        highlight: TokenKind::Atom(AtomKind::Comment),
1234                        self_pos: SingleLineSpan {
1235                            line: 0.into(),
1236                            start_col: 8,
1237                            end_col: 11
1238                        },
1239                        opposite_pos: vec![SingleLineSpan {
1240                            line: 0.into(),
1241                            start_col: 8,
1242                            end_col: 11
1243                        }]
1244                    },
1245                    pos: SingleLineSpan {
1246                        line: 0.into(),
1247                        start_col: 8,
1248                        end_col: 11
1249                    }
1250                },
1251                MatchedPos {
1252                    kind: MatchKind::NovelWord {
1253                        highlight: TokenKind::Atom(AtomKind::Comment)
1254                    },
1255                    pos: SingleLineSpan {
1256                        line: 0.into(),
1257                        start_col: 12,
1258                        end_col: 17
1259                    }
1260                }
1261            ],
1262        );
1263    }
1264}