1use rustc_hash::FxHashSet;
31
32use rowan::{Direction, NodeOrToken, TextRange};
33use salsa::Database as Db;
34use squawk_syntax::SyntaxKind;
35use squawk_syntax::ast::{self, AstNode, AstToken};
36
37use crate::db::{File, parse};
38
39#[derive(Debug, Clone, PartialEq, Eq)]
40pub enum FoldKind {
41 ArgList,
42 Array,
43 Comment,
44 FunctionCall,
45 Join,
46 List,
47 Statement,
48 Subquery,
49 Tuple,
50}
51
52#[derive(Debug, Clone, PartialEq, Eq)]
53pub struct Fold {
54 pub range: TextRange,
55 pub kind: FoldKind,
56}
57
58#[salsa::tracked]
59pub fn folding_ranges(db: &dyn Db, file: File) -> Vec<Fold> {
60 let parse = parse(db, file);
61
62 let mut folds = vec![];
63 let mut visited_comments = FxHashSet::default();
64
65 for element in parse.tree().syntax().descendants_with_tokens() {
66 match &element {
67 NodeOrToken::Token(token) => {
68 if let Some(comment) = ast::Comment::cast(token.clone())
69 && !visited_comments.contains(&comment)
70 && let Some(range) =
71 contiguous_range_for_comment(comment, &mut visited_comments)
72 {
73 folds.push(Fold {
74 range,
75 kind: FoldKind::Comment,
76 });
77 }
78 }
79 NodeOrToken::Node(node) => {
80 if let Some(kind) = fold_kind(node.kind()) {
81 if !node.text().contains_char('\n') {
82 continue;
83 }
84 let start = node
86 .children_with_tokens()
87 .find(|e| match e {
88 NodeOrToken::Token(t) => {
89 let kind = t.kind();
90 kind != SyntaxKind::COMMENT && kind != SyntaxKind::WHITESPACE
91 }
92 NodeOrToken::Node(_) => true,
93 })
94 .map(|e| e.text_range().start())
95 .unwrap_or_else(|| node.text_range().start());
96 folds.push(Fold {
97 range: TextRange::new(start, node.text_range().end()),
98 kind,
99 });
100 }
101 }
102 }
103 }
104
105 folds
106}
107
108fn fold_kind(kind: SyntaxKind) -> Option<FoldKind> {
109 if ast::Stmt::can_cast(kind) {
110 return Some(FoldKind::Statement);
111 }
112
113 match kind {
114 SyntaxKind::ARG_LIST | SyntaxKind::TABLE_ARG_LIST | SyntaxKind::PARAM_LIST => {
115 Some(FoldKind::ArgList)
116 }
117 SyntaxKind::ARRAY_EXPR => Some(FoldKind::Array),
118 SyntaxKind::CALL_EXPR => Some(FoldKind::FunctionCall),
119 SyntaxKind::JOIN => Some(FoldKind::Join),
120 SyntaxKind::PAREN_SELECT => Some(FoldKind::Subquery),
121 SyntaxKind::TUPLE_EXPR => Some(FoldKind::Tuple),
122 SyntaxKind::WHEN_CLAUSE_LIST
123 | SyntaxKind::ALTER_OPTION_LIST
124 | SyntaxKind::ATTRIBUTE_LIST
125 | SyntaxKind::BEGIN_FUNC_OPTION_LIST
126 | SyntaxKind::CHECKPOINT_OPTION_LIST
127 | SyntaxKind::COLUMN_LIST
128 | SyntaxKind::COLUMN_REF_LIST
129 | SyntaxKind::CONFLICT_INDEX_ITEM_LIST
130 | SyntaxKind::CONSTRAINT_EXCLUSION_LIST
131 | SyntaxKind::COPY_OPTION_LIST
132 | SyntaxKind::DATABASE_OPTION_LIST
133 | SyntaxKind::EXPLAIN_OPTION_LIST
134 | SyntaxKind::DROP_OP_CLASS_OPTION_LIST
135 | SyntaxKind::FDW_OPTION_LIST
136 | SyntaxKind::FUNCTION_SIG_LIST
137 | SyntaxKind::PROCEDURE_SIG_LIST
138 | SyntaxKind::ROUTINE_SIG_LIST
139 | SyntaxKind::FUNC_OPTION_LIST
140 | SyntaxKind::GRANT_ROLE_OPTION_LIST
141 | SyntaxKind::GROUP_BY_LIST
142 | SyntaxKind::JSON_TABLE_COLUMN_LIST
143 | SyntaxKind::OPERATOR_CLASS_OPTION_LIST
144 | SyntaxKind::OPTION_ITEM_LIST
145 | SyntaxKind::OP_SIG_LIST
146 | SyntaxKind::PARTITION_ITEM_LIST
147 | SyntaxKind::PARTITION_LIST
148 | SyntaxKind::TABLE_NAME_REF_LIST
149 | SyntaxKind::REINDEX_OPTION_LIST
150 | SyntaxKind::RELATION_LIST
151 | SyntaxKind::RETURNING_OPTION_LIST
152 | SyntaxKind::REVOKE_COMMAND_LIST
153 | SyntaxKind::ROLE_OPTION_LIST
154 | SyntaxKind::ROLE_REF_LIST
155 | SyntaxKind::ROW_LIST
156 | SyntaxKind::RULE_STMT_LIST
157 | SyntaxKind::SEQUENCE_OPTION_LIST
158 | SyntaxKind::SET_COLUMN_LIST
159 | SyntaxKind::SET_EXPR_LIST
160 | SyntaxKind::SET_OPTIONS_LIST
161 | SyntaxKind::SORT_BY_LIST
162 | SyntaxKind::TABLE_AND_COLUMNS_LIST
163 | SyntaxKind::TABLE_LIST
164 | SyntaxKind::TARGET_LIST
165 | SyntaxKind::TRANSACTION_MODE_LIST
166 | SyntaxKind::TRIGGER_EVENT_LIST
167 | SyntaxKind::VACUUM_OPTION_LIST
168 | SyntaxKind::VARIANT_LIST
169 | SyntaxKind::EXPR_AS_NAME_LIST
170 | SyntaxKind::XML_COLUMN_OPTION_LIST
171 | SyntaxKind::XML_NAMESPACE_LIST
172 | SyntaxKind::XML_TABLE_COLUMN_LIST
173 | SyntaxKind::LABEL_AND_PROPERTIES_LIST
174 | SyntaxKind::PATH_PATTERN_LIST => Some(FoldKind::List),
175 _ => None,
176 }
177}
178
179fn contiguous_range_for_comment(
180 first: ast::Comment,
181 visited: &mut FxHashSet<ast::Comment>,
182) -> Option<TextRange> {
183 visited.insert(first.clone());
184
185 let group_kind = first.kind();
187 if !group_kind.is_line() {
188 return None;
189 }
190
191 let mut last = first.clone();
192 for element in first.syntax().siblings_with_tokens(Direction::Next) {
193 match element {
194 NodeOrToken::Token(token) => {
195 if let Some(ws) = ast::Whitespace::cast(token.clone())
196 && !ws.spans_multiple_lines()
197 {
198 continue;
200 }
201 if let Some(c) = ast::Comment::cast(token) {
202 visited.insert(c.clone());
203 last = c;
204 continue;
205 }
206 break;
210 }
211 NodeOrToken::Node(_) => break,
212 }
213 }
214
215 if first != last {
216 Some(TextRange::new(
217 first.syntax().text_range().start(),
218 last.syntax().text_range().end(),
219 ))
220 } else {
221 None
223 }
224}
225
226#[cfg(test)]
227mod tests {
228 use insta::assert_snapshot;
229
230 use crate::db::{Database, File};
231
232 use super::*;
233
234 fn fold_kind_str(kind: &FoldKind) -> &'static str {
235 match kind {
236 FoldKind::ArgList => "arglist",
237 FoldKind::Array => "array",
238 FoldKind::Comment => "comment",
239 FoldKind::FunctionCall => "function_call",
240 FoldKind::Join => "join",
241 FoldKind::List => "list",
242 FoldKind::Statement => "statement",
243 FoldKind::Subquery => "subquery",
244 FoldKind::Tuple => "tuple",
245 }
246 }
247
248 #[must_use]
249 fn check(sql: &str) -> String {
250 let db = Database::default();
251 let file = File::new(&db, sql.to_string().into());
252 let folds = folding_ranges(&db, file);
253
254 if folds.is_empty() {
255 return sql.to_string();
256 }
257
258 #[derive(PartialEq, Eq, PartialOrd, Ord)]
259 struct Event<'a> {
260 offset: usize,
261 is_end: bool,
262 kind: &'a str,
263 }
264
265 let mut events: Vec<Event<'_>> = vec![];
266 for fold in &folds {
267 let start: usize = fold.range.start().into();
268 let end: usize = fold.range.end().into();
269 let kind = fold_kind_str(&fold.kind);
270 events.push(Event {
271 offset: start,
272 is_end: false,
273 kind,
274 });
275 events.push(Event {
276 offset: end,
277 is_end: true,
278 kind,
279 });
280 }
281 events.sort();
282
283 let mut output = String::new();
284 let mut pos = 0usize;
285 for event in &events {
286 if event.offset > pos {
287 output.push_str(&sql[pos..event.offset]);
288 pos = event.offset;
289 }
290 if event.is_end {
291 output.push_str("</fold>");
292 } else {
293 output.push_str(&format!("<fold {}>", event.kind));
294 }
295 }
296 if pos < sql.len() {
297 output.push_str(&sql[pos..]);
298 }
299 output
300 }
301
302 #[test]
303 fn fold_create_table() {
304 assert_snapshot!(check("
305create table t (
306 id int,
307 name text
308);"), @"
309 <fold statement>create table t <fold arglist>(
310 id int,
311 name text
312 )</fold>;</fold>
313 ");
314 }
315
316 #[test]
317 fn fold_select() {
318 assert_snapshot!(check("
319select
320 id,
321 name
322from t;"), @"
323 <fold statement>select
324 <fold list>id,
325 name</fold>
326 from t;</fold>
327 ");
328 }
329
330 #[test]
331 fn do_not_fold_single_line_comment() {
332 assert_snapshot!(check("
333-- a comment
334select 1;"), @"
335 -- a comment
336 select 1;
337 ");
338 }
339
340 #[test]
341 fn fold_comments_does_not_apply_when_diff_comment_types() {
342 assert_snapshot!(check("
343/* first part */
344-- second part
345select 1;"), @"
346 /* first part */
347 -- second part
348 select 1;
349 ");
350 }
351
352 #[test]
353 fn fold_comments_and_multi_statements() {
354 assert_snapshot!(check("
355-- this is
356
357-- a comment
358-- with some more
359select a, b, 3
360 from t
361 where c > 10;"), @"
362 -- this is
363
364 <fold comment>-- a comment
365 -- with some more</fold>
366 <fold statement>select a, b, 3
367 from t
368 where c > 10;</fold>
369 ");
370 }
371
372 #[test]
373 fn fold_comments_does_not_apply_when_whitespace_between() {
374 assert_snapshot!(check("
375-- this is
376
377-- a comment
378-- with some more
379select 1;"), @"
380 -- this is
381
382 <fold comment>-- a comment
383 -- with some more</fold>
384 select 1;
385 ");
386 }
387
388 #[test]
389 fn fold_multiline_comments() {
390 assert_snapshot!(check("
391-- this is
392-- a comment
393select 1;"), @"
394 <fold comment>-- this is
395 -- a comment</fold>
396 select 1;
397 ");
398 }
399
400 #[test]
401 fn fold_single_line_no_fold() {
402 assert_snapshot!(check("select 1;"), @"select 1;");
403 }
404
405 #[test]
406 fn fold_subquery() {
407 assert_snapshot!(check("
408select * from (
409 select id from t
410);"), @"
411 <fold statement>select * from <fold statement>(
412 select id from t
413 )</fold>;</fold>
414 ");
415 }
416
417 #[test]
418 fn fold_case_when() {
419 assert_snapshot!(check("
420select
421 case
422 when x = 1 then 'a'
423 when x = 2 then 'b'
424 end
425from t;"), @"
426 <fold statement>select
427 <fold list>case
428 <fold list>when x = 1 then 'a'
429 when x = 2 then 'b'</fold>
430 end</fold>
431 from t;</fold>
432 ");
433 }
434
435 #[test]
436 fn fold_join() {
437 assert_snapshot!(check("
438select *
439from a
440join b
441 on a.id = b.id;"), @"
442 <fold statement>select *
443 from a
444 <fold join>join b
445 on a.id = b.id</fold>;</fold>
446 ");
447 }
448
449 #[test]
450 fn fold_array_literal() {
451 assert_snapshot!(check("
452select * from t where
453 x = any(array[
454 1,
455 2,
456 3
457 ]);"), @"
458 <fold statement>select * from t where
459 x = <fold function_call>any(<fold array>array[
460 1,
461 2,
462 3
463 ]</fold>)</fold>;</fold>
464 ");
465 }
466
467 #[test]
468 fn fold_tuple_literal() {
469 assert_snapshot!(check("
470select (
471 1,
472 2,
473 3
474);"), @"
475 <fold statement>select <fold list><fold tuple>(
476 1,
477 2,
478 3
479 )</fold></fold>;</fold>
480 ");
481 }
482
483 #[test]
484 fn fold_tuple_bin_expr() {
485 assert_snapshot!(check("
486select * from x
487 where z in (
488 1,
489 2,
490 3,
491 4,
492 5
493 );
494"), @"
495 <fold statement>select * from x
496 where z in <fold tuple>(
497 1,
498 2,
499 3,
500 4,
501 5
502 )</fold>;</fold>
503 ");
504 }
505
506 #[test]
507 fn fold_function_call() {
508 assert_snapshot!(check("
509select coalesce(
510 a,
511 b,
512 c
513);"), @"
514 <fold statement>select <fold function_call><fold list>coalesce<fold arglist>(
515 a,
516 b,
517 c
518 )</fold></fold></fold>;</fold>
519 ");
520 }
521
522 #[test]
523 fn fold_create_enum() {
524 assert_snapshot!(check("
525create type status as enum (
526 'active',
527 'inactive'
528);"), @"
529 <fold statement>create type status as enum <fold list>(
530 'active',
531 'inactive'
532 )</fold>;</fold>
533 ");
534 }
535
536 #[test]
537 fn fold_insert_values() {
538 assert_snapshot!(check("
539insert into t (id, name)
540values
541 (1, 'a'),
542 (2, 'b');"), @"
543 <fold statement>insert into t (id, name)
544 <fold statement>values
545 <fold list>(1, 'a'),
546 (2, 'b')</fold></fold>;</fold>
547 ");
548 }
549
550 #[test]
551 fn no_fold_single_line_create_table() {
552 assert_snapshot!(check("create table t (id int);"), @"create table t (id int);");
553 }
554
555 #[test]
556 fn list_variants() {
557 let unhandled_list_kinds: Vec<SyntaxKind> = (0..SyntaxKind::__LAST as u16)
558 .map(SyntaxKind::from)
559 .filter(|kind| format!("{kind:?}").ends_with("_LIST"))
560 .filter(|kind| fold_kind(*kind).is_none())
561 .collect();
562
563 assert_eq!(
564 unhandled_list_kinds,
565 vec![],
566 "All _LIST SyntaxKind variants should be handled in fold_kind"
567 );
568 }
569}