1#![allow(clippy::mutable_key_type)] use 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
21impl<'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
50pub struct SyntaxInfo<'a> {
52 previous_sibling: Cell<Option<&'a Syntax<'a>>>,
54 next_sibling: Cell<Option<&'a Syntax<'a>>>,
56 prev: Cell<Option<&'a Syntax<'a>>>,
59 parent: Cell<Option<&'a Syntax<'a>>>,
61 num_ancestors: Cell<u32>,
63 pub num_after: Cell<usize>,
64 unique_id: Cell<SyntaxId>,
66 content_id: Cell<u32>,
72 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 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 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 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 pub fn id(&self) -> SyntaxId {
290 self.info().unique_id.get()
291 }
292
293 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
360pub 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 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 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
449fn 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
469fn 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
522fn 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#[derive(PartialEq, Eq, Debug, Clone, Copy, Hash)]
566pub enum StringKind {
567 StringLiteral,
569 Text,
571}
572
573#[derive(PartialEq, Eq, Debug, Clone, Copy, Hash)]
574pub enum AtomKind {
575 Normal,
576 String(StringKind),
579 Type,
580 Comment,
581 Keyword,
582 TreeSitterError,
583}
584
585#[derive(PartialEq, Eq, Debug, Clone, Copy)]
587pub enum TokenKind {
588 Delimiter,
589 Atom(AtomKind),
590}
591
592#[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
631fn 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 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 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 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 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 opposite_offset += opposite_word.len();
716 }
717 }
718 }
719
720 mps
721}
722
723fn 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 unchanged_count > 2 && unchanged_count * 2 >= novel_count
750}
751
752fn 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 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 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 if mps.len() == opposite_pos_len {
843 break;
844 }
845 }
846 mps
847 }
848 Novel => {
849 let kind = MatchKind::Novel { highlight };
850 let mut mps = vec![];
852 for line_pos in &pos {
853 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
872pub 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
945pub 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 #[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 #[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}