// SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: 2021 The Gleam contributors use std::cmp::Ordering; use ecow::EcoString; use crate::ast::SrcSpan; #[derive(Debug, PartialEq, Eq, Default)] pub struct ModuleExtra { pub module_comments: Vec, pub doc_comments: Vec, pub comments: Vec, pub empty_lines: Vec, pub new_lines: Vec, pub trailing_commas: Vec, } impl ModuleExtra { pub fn new() -> Self { Self::default() } /// Detects if a byte index is in a comment context pub fn is_within_comment(&self, byte_index: u32) -> bool { let cmp = |span: &SrcSpan| { if byte_index < span.start { Ordering::Greater } else if byte_index > span.end { Ordering::Less } else { Ordering::Equal } }; self.comments.binary_search_by(cmp).is_ok() || self.doc_comments.binary_search_by(cmp).is_ok() || self.module_comments.binary_search_by(cmp).is_ok() } pub fn has_comment_between(&self, start: u32, end: u32) -> bool { self.first_comment_between(start, end).is_some() } fn index_of_first_comment_between(&self, start: u32, end: u32) -> Option { self.comments .binary_search_by(|comment| { if comment.end < start { Ordering::Less } else if comment.start > end { Ordering::Greater } else { Ordering::Equal } }) .ok() } /// Returns the first comment overlapping the given source locations (inclusive) /// Note that the returned span covers the text of the comment, not the `//` pub fn first_comment_between(&self, start: u32, end: u32) -> Option { // Index of a comment, but not necessarily the first one. let index = self.index_of_first_comment_between(start, end)?; self.comments .get(0..=index)? .iter() .rev() .take_while(|comment| { // comment overlaps span (inclusive of endpoints) comment.end >= start && comment.start <= end }) .last() .copied() } pub fn last_comment_between(&self, start: u32, end: u32) -> Option { // We start from the first comment that we can find in between the given // start and end, this is really fast as we can find such index through // binary search. let mut index_of_last_comment = self.index_of_first_comment_between(start, end)?; // Then we go over all the comments we can find from that one that are // still in between the given indices. loop { let next_comment = self.comments.get(index_of_last_comment + 1); if let Some(next_comment) = next_comment && start <= next_comment.start && next_comment.end <= end { // The next comment is still in between the two indices, we keep // going. index_of_last_comment += 1; } else { // The next comment is outside of the given range, that means // the current one is the last comment we were looking for. // We can return it! return self.comments.get(index_of_last_comment).cloned(); } } } } #[derive(Debug, PartialEq, Eq)] pub struct Comment<'a> { pub start: u32, pub content: &'a str, } impl<'a> From<(&SrcSpan, &'a EcoString)> for Comment<'a> { fn from(value: (&SrcSpan, &'a EcoString)) -> Self { Self::from((value.0, value.1.as_str())) } } impl<'a> From<(&SrcSpan, &'a str)> for Comment<'a> { fn from(src: (&SrcSpan, &'a str)) -> Comment<'a> { let start = src.0.start; let end = src.0.end as usize; Comment { start, content: src .1 .get(start as usize..end) .expect("From span to comment"), } } } #[cfg(test)] mod tests { use crate::{ast::SrcSpan, parse::extra::ModuleExtra}; fn set_up_extra() -> ModuleExtra { let mut extra = ModuleExtra::new(); extra.comments = vec![ SrcSpan { start: 0, end: 10 }, SrcSpan { start: 20, end: 30 }, SrcSpan { start: 40, end: 50 }, SrcSpan { start: 60, end: 70 }, SrcSpan { start: 80, end: 90 }, SrcSpan { start: 90, end: 100, }, ]; extra } #[test] fn first_comment_between() { let extra = set_up_extra(); assert!(matches!( extra.first_comment_between(15, 85), Some(SrcSpan { start: 20, end: 30 }) )); } #[test] fn first_comment_between_equal_to_range() { let extra = set_up_extra(); assert!(matches!( extra.first_comment_between(40, 50), Some(SrcSpan { start: 40, end: 50 }) )); } #[test] fn first_comment_between_overlapping_start_of_range() { let extra = set_up_extra(); assert!(matches!( extra.first_comment_between(45, 80), Some(SrcSpan { start: 40, end: 50 }) )); } #[test] fn first_comment_between_overlapping_end_of_range() { let extra = set_up_extra(); assert!(matches!( extra.first_comment_between(35, 45), Some(SrcSpan { start: 40, end: 50 }) )); } #[test] fn first_comment_between_at_end_of_range() { let extra = set_up_extra(); assert!(matches!( extra.first_comment_between(55, 60), Some(SrcSpan { start: 60, end: 70 }) )); } }