1use crate::comment::Comment;
2use crate::declaration::Declaration;
3use crate::import::Import;
4use crate::module_header::ModuleHeader;
5use crate::node::Spanned;
6use crate::token::Token;
7
8#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
14#[derive(Clone, Debug, PartialEq, Eq)]
15pub struct ElmModule {
16 pub header: Spanned<ModuleHeader>,
18
19 #[cfg_attr(
25 feature = "serde",
26 serde(default, skip_serializing_if = "Option::is_none")
27 )]
28 pub module_documentation: Option<Spanned<String>>,
29
30 pub imports: Vec<Spanned<Import>>,
32
33 pub declarations: Vec<Spanned<Declaration>>,
35
36 pub comments: Vec<Spanned<Comment>>,
41}
42
43impl ElmModule {
44 pub fn leading_comments(&self, decl_index: usize) -> Vec<&Spanned<Comment>> {
54 if decl_index >= self.declarations.len() {
55 return Vec::new();
56 }
57
58 let decl_start = self.declarations[decl_index].span.start.offset;
59
60 let prev_end = if decl_index > 0 {
62 self.declarations[decl_index - 1].span.end.offset
63 } else if let Some(last_import) = self.imports.last() {
64 last_import.span.end.offset
65 } else {
66 self.header.span.end.offset
67 };
68
69 self.comments
70 .iter()
71 .filter(|c| c.span.start.offset > prev_end && c.span.end.offset <= decl_start)
72 .collect()
73 }
74
75 pub fn trailing_comment(&self, decl_index: usize) -> Option<&Spanned<Comment>> {
78 if decl_index >= self.declarations.len() {
79 return None;
80 }
81
82 let decl_end_line = self.declarations[decl_index].span.end.line;
83
84 let next_start = if decl_index + 1 < self.declarations.len() {
86 self.declarations[decl_index + 1].span.start.offset
87 } else {
88 usize::MAX
89 };
90
91 self.comments.iter().find(|c| {
92 c.span.start.line == decl_end_line
93 && c.span.start.offset < next_start
94 && matches!(c.value, Comment::Line(_))
95 })
96 }
97
98 pub fn module_comments(&self) -> Vec<&Spanned<Comment>> {
101 let first_decl_start = self
102 .declarations
103 .first()
104 .map(|d| d.span.start.offset)
105 .unwrap_or(usize::MAX);
106
107 self.comments
108 .iter()
109 .filter(|c| c.span.end.offset <= first_decl_start)
110 .collect()
111 }
112}
113
114pub fn extract_comments(tokens: &[Spanned<Token>]) -> Vec<Spanned<Comment>> {
120 tokens
121 .iter()
122 .filter_map(|tok| match &tok.value {
123 Token::LineComment(text) => Some(Spanned::new(tok.span, Comment::Line(text.clone()))),
124 Token::BlockComment(text) => Some(Spanned::new(tok.span, Comment::Block(text.clone()))),
125 Token::DocComment(text) => Some(Spanned::new(tok.span, Comment::Doc(text.clone()))),
126 _ => None,
127 })
128 .collect()
129}
130
131pub fn associate_comments(
142 module: &ElmModule,
143 all_comments: &[Spanned<Comment>],
144) -> Vec<Vec<Spanned<Comment>>> {
145 let mut result = Vec::with_capacity(module.declarations.len());
146
147 for (i, decl) in module.declarations.iter().enumerate() {
148 let decl_start_line = decl.span.start.line;
149
150 let prev_start_line = if i > 0 {
152 module.declarations[i - 1].span.start.line
153 } else if let Some(last_import) = module.imports.last() {
154 last_import.span.end.line
155 } else {
156 module.header.span.end.line
157 };
158
159 let leading: Vec<Spanned<Comment>> = all_comments
160 .iter()
161 .filter(|c| c.span.start.line > prev_start_line && c.span.start.line < decl_start_line)
162 .cloned()
163 .collect();
164
165 result.push(leading);
166 }
167
168 result
169}