Skip to main content

squawk_ide/
folding_ranges.rs

1// via https://github.com/rust-lang/rust-analyzer/blob/8d75311400a108d7ffe17dc9c38182c566952e6e/crates/ide/src/folding_ranges.rs#L47
2//
3// Permission is hereby granted, free of charge, to any
4// person obtaining a copy of this software and associated
5// documentation files (the "Software"), to deal in the
6// Software without restriction, including without
7// limitation the rights to use, copy, modify, merge,
8// publish, distribute, sublicense, and/or sell copies of
9// the Software, and to permit persons to whom the Software
10// is furnished to do so, subject to the following
11// conditions:
12//
13// The above copyright notice and this permission notice
14// shall be included in all copies or substantial portions
15// of the Software.
16//
17// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
18// ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
19// TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
20// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
21// SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
22// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
23// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
24// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
25// DEALINGS IN THE SOFTWARE.
26
27// NOTE: pretty much copied as is but simplfied a fair bit. I don't use folding
28// much so not sure if this is optimal.
29
30use rustc_hash::FxHashSet;
31
32use rowan::{NodeOrToken, SyntaxText, TextRange};
33use salsa::Database as Db;
34use squawk_line_index::find_newline;
35use squawk_syntax::SyntaxKind;
36use squawk_syntax::ast::{self, AstNode, AstToken};
37
38use crate::comments::line_comment_group;
39use crate::db::{File, parse};
40
41#[derive(Debug, Clone, PartialEq, Eq)]
42pub enum FoldKind {
43    ArgList,
44    Array,
45    Comment,
46    FunctionCall,
47    Join,
48    List,
49    Statement,
50    Subquery,
51    Tuple,
52    WhereClause,
53}
54
55#[derive(Debug, Clone, PartialEq, Eq)]
56pub struct Fold {
57    pub range: TextRange,
58    pub kind: FoldKind,
59}
60
61#[salsa::tracked]
62pub fn folding_ranges(db: &dyn Db, file: File) -> Vec<Fold> {
63    let parse = parse(db, file);
64
65    let mut folds = vec![];
66    let mut visited_comments = FxHashSet::default();
67
68    for element in parse.tree().syntax().descendants_with_tokens() {
69        match &element {
70            NodeOrToken::Token(token) => {
71                if let Some(comment) = ast::Comment::cast(token.clone())
72                    && !visited_comments.contains(&comment)
73                    && let Some(range) =
74                        contiguous_range_for_comment(comment, &mut visited_comments)
75                {
76                    folds.push(Fold {
77                        range,
78                        kind: FoldKind::Comment,
79                    });
80                }
81            }
82            NodeOrToken::Node(node) => {
83                if let Some(kind) = fold_kind(node.kind()) {
84                    if !contains_newline(&node.text()) {
85                        continue;
86                    }
87                    // skip any leading whitespace / comments
88                    let start = node
89                        .children_with_tokens()
90                        .find(|e| match e {
91                            NodeOrToken::Token(t) => {
92                                let kind = t.kind();
93                                kind != SyntaxKind::COMMENT && kind != SyntaxKind::WHITESPACE
94                            }
95                            NodeOrToken::Node(_) => true,
96                        })
97                        .map(|e| e.text_range().start())
98                        .unwrap_or_else(|| node.text_range().start());
99                    folds.push(Fold {
100                        range: TextRange::new(start, node.text_range().end()),
101                        kind,
102                    });
103                }
104            }
105        }
106    }
107
108    folds
109}
110
111fn contains_newline(text: &SyntaxText) -> bool {
112    text.try_for_each_chunk(|chunk| {
113        if find_newline(chunk).is_some() {
114            Err(())
115        } else {
116            Ok(())
117        }
118    })
119    .is_err()
120}
121
122fn fold_kind(kind: SyntaxKind) -> Option<FoldKind> {
123    if ast::Stmt::can_cast(kind) {
124        return Some(FoldKind::Statement);
125    }
126
127    match kind {
128        SyntaxKind::ARG_LIST | SyntaxKind::TABLE_ARG_LIST | SyntaxKind::PARAM_LIST => {
129            Some(FoldKind::ArgList)
130        }
131        SyntaxKind::ARRAY_EXPR => Some(FoldKind::Array),
132        SyntaxKind::CALL_EXPR => Some(FoldKind::FunctionCall),
133        SyntaxKind::JOIN => Some(FoldKind::Join),
134        SyntaxKind::PAREN_SELECT => Some(FoldKind::Subquery),
135        SyntaxKind::TUPLE_EXPR => Some(FoldKind::Tuple),
136        SyntaxKind::WHERE_CLAUSE => Some(FoldKind::WhereClause),
137        SyntaxKind::WHEN_CLAUSE_LIST
138        | SyntaxKind::ALIAS_COLUMN_LIST
139        | SyntaxKind::ALTER_OPTION_LIST
140        | SyntaxKind::ALTER_TYPE_ATTRIBUTE_ACTION_LIST
141        | SyntaxKind::ATTRIBUTE_LIST
142        | SyntaxKind::BEGIN_FUNC_OPTION_LIST
143        | SyntaxKind::CHECKPOINT_OPTION_LIST
144        | SyntaxKind::COLUMN_LIST
145        | SyntaxKind::COLUMN_REF_LIST
146        | SyntaxKind::COLUMN_TARGET_LIST
147        | SyntaxKind::COMPOSITE_FIELD_LIST
148        | SyntaxKind::CONFLICT_INDEX_ITEM_LIST
149        | SyntaxKind::CONSTRAINT_COLUMN_REF_LIST
150        | SyntaxKind::CONSTRAINT_EXCLUSION_LIST
151        | SyntaxKind::COPY_OPTION_LIST
152        | SyntaxKind::DATABASE_OPTION_LIST
153        | SyntaxKind::EXPLAIN_OPTION_LIST
154        | SyntaxKind::DROP_OP_CLASS_OPTION_LIST
155        | SyntaxKind::FDW_OPTION_LIST
156        | SyntaxKind::FOREIGN_KEY_COLUMN_LIST
157        | SyntaxKind::FUNCTION_SIG_LIST
158        | SyntaxKind::PROCEDURE_SIG_LIST
159        | SyntaxKind::ROUTINE_SIG_LIST
160        | SyntaxKind::FUNC_OPTION_LIST
161        | SyntaxKind::GRANT_ROLE_OPTION_LIST
162        | SyntaxKind::GROUP_BY_LIST
163        | SyntaxKind::JSON_TABLE_COLUMN_LIST
164        | SyntaxKind::OPERATOR_CLASS_OPTION_LIST
165        | SyntaxKind::OPTION_ALTER_OPTION_LIST
166        | SyntaxKind::OPTION_ITEM_LIST
167        | SyntaxKind::OP_SIG_LIST
168        | SyntaxKind::PARTITION_ITEM_LIST
169        | SyntaxKind::PARTITION_LIST
170        | SyntaxKind::TABLE_NAME_REF_LIST
171        | SyntaxKind::REINDEX_OPTION_LIST
172        | SyntaxKind::RELATION_LIST
173        | SyntaxKind::RETURNING_OPTION_LIST
174        | SyntaxKind::REVOKE_COMMAND_LIST
175        | SyntaxKind::ROLE_OPTION_LIST
176        | SyntaxKind::ROLE_REF_LIST
177        | SyntaxKind::ROW_LIST
178        | SyntaxKind::RULE_STMT_LIST
179        | SyntaxKind::SEQUENCE_OPTION_LIST
180        | SyntaxKind::SET_COLUMN_LIST
181        | SyntaxKind::SET_EXPR_LIST
182        | SyntaxKind::SET_OPTIONS_LIST
183        | SyntaxKind::SORT_BY_LIST
184        | SyntaxKind::TABLE_AND_COLUMNS_LIST
185        | SyntaxKind::TABLE_LIST
186        | SyntaxKind::TARGET_LIST
187        | SyntaxKind::TRANSACTION_MODE_LIST
188        | SyntaxKind::TRIGGER_EVENT_LIST
189        | SyntaxKind::VACUUM_OPTION_LIST
190        | SyntaxKind::VARIANT_LIST
191        | SyntaxKind::EXPR_AS_COLUMN_NAME_LIST
192        | SyntaxKind::EXPR_AS_ELEMENT_TAG_LIST
193        | SyntaxKind::EXPR_AS_PROPERTY_NAME_LIST
194        | SyntaxKind::EXPR_AS_XML_ATTR_LIST
195        | SyntaxKind::XML_COLUMN_OPTION_LIST
196        | SyntaxKind::XML_NAMESPACE_LIST
197        | SyntaxKind::XML_TABLE_COLUMN_LIST
198        | SyntaxKind::LABEL_AND_PROPERTIES_LIST
199        | SyntaxKind::PATH_PATTERN_LIST => Some(FoldKind::List),
200        _ => None,
201    }
202}
203
204fn contiguous_range_for_comment(
205    comment: ast::Comment,
206    visited: &mut FxHashSet<ast::Comment>,
207) -> Option<TextRange> {
208    visited.insert(comment.clone());
209
210    // Only fold comments of the same flavor
211    if !comment.kind().is_line() {
212        return None;
213    }
214
215    let group = line_comment_group(&comment);
216    visited.extend(group.iter().cloned());
217
218    // A group of one element cannot be folded
219    let [first, .., last] = group.as_slice() else {
220        return None;
221    };
222
223    Some(TextRange::new(
224        first.syntax().text_range().start(),
225        last.syntax().text_range().end(),
226    ))
227}
228
229#[cfg(test)]
230mod tests {
231    use insta::assert_snapshot;
232
233    use crate::db::{Database, File};
234
235    use super::*;
236
237    fn fold_kind_str(kind: &FoldKind) -> &'static str {
238        match kind {
239            FoldKind::ArgList => "arglist",
240            FoldKind::Array => "array",
241            FoldKind::Comment => "comment",
242            FoldKind::FunctionCall => "function_call",
243            FoldKind::Join => "join",
244            FoldKind::List => "list",
245            FoldKind::Statement => "statement",
246            FoldKind::Subquery => "subquery",
247            FoldKind::Tuple => "tuple",
248            FoldKind::WhereClause => "where_clause",
249        }
250    }
251
252    #[must_use]
253    fn check(sql: &str) -> String {
254        let db = Database::default();
255        let file = File::new(&db, sql.to_string().into());
256        let folds = folding_ranges(&db, file);
257
258        if folds.is_empty() {
259            return sql.to_string();
260        }
261
262        #[derive(PartialEq, Eq, PartialOrd, Ord)]
263        struct Event<'a> {
264            offset: usize,
265            is_end: bool,
266            kind: &'a str,
267        }
268
269        let mut events: Vec<Event<'_>> = vec![];
270        for fold in &folds {
271            let start: usize = fold.range.start().into();
272            let end: usize = fold.range.end().into();
273            let kind = fold_kind_str(&fold.kind);
274            events.push(Event {
275                offset: start,
276                is_end: false,
277                kind,
278            });
279            events.push(Event {
280                offset: end,
281                is_end: true,
282                kind,
283            });
284        }
285        events.sort();
286
287        let mut output = String::new();
288        let mut pos = 0usize;
289        for event in &events {
290            if event.offset > pos {
291                output.push_str(&sql[pos..event.offset]);
292                pos = event.offset;
293            }
294            if event.is_end {
295                output.push_str("</fold>");
296            } else {
297                output.push_str(&format!("<fold {}>", event.kind));
298            }
299        }
300        if pos < sql.len() {
301            output.push_str(&sql[pos..]);
302        }
303        output
304    }
305
306    #[test]
307    fn fold_create_table() {
308        assert_snapshot!(check("
309create table t (
310  id int,
311  name text
312);"), @"
313        <fold statement>create table t <fold arglist>(
314          id int,
315          name text
316        )</fold>;</fold>
317        ");
318    }
319
320    #[test]
321    fn fold_select() {
322        assert_snapshot!(check("
323select
324  id,
325  name
326from t;"), @"
327        <fold statement>select
328          <fold list>id,
329          name</fold>
330        from t;</fold>
331        ");
332    }
333
334    #[test]
335    fn do_not_fold_single_line_comment() {
336        assert_snapshot!(check("
337-- a comment
338select 1;"), @"
339        -- a comment
340        select 1;
341        ");
342    }
343
344    #[test]
345    fn fold_comments_does_not_apply_when_diff_comment_types() {
346        assert_snapshot!(check("
347/* first part */
348-- second part
349select 1;"), @"
350        /* first part */
351        -- second part
352        select 1;
353        ");
354    }
355
356    #[test]
357    fn fold_comments_does_not_apply_when_block_comment_follows() {
358        assert_snapshot!(check("
359-- first part
360/* second part */
361select 1;"), @"
362        -- first part
363        /* second part */
364        select 1;
365        ");
366    }
367
368    #[test]
369    fn fold_comments_groups_across_statements() {
370        assert_snapshot!(check("
371select 1; -- a
372-- b
373select 2;"), @"
374        select 1; <fold comment>-- a
375        -- b</fold>
376        select 2;
377        ");
378    }
379
380    #[test]
381    fn fold_comments_groups_nested_in_a_list() {
382        assert_snapshot!(check("
383select 1, -- a
384  -- b
385  2;"), @"
386        <fold statement>select <fold list>1, <fold comment>-- a
387          -- b</fold>
388          2</fold>;</fold>
389        ");
390    }
391
392    #[test]
393    fn fold_comments_with_cr_line_endings() {
394        assert_snapshot!(check(&"
395-- this is
396-- a comment
397
398-- separate
399select 1;".replace('\n', "\r")).replace('\r', "\n"), @"
400        <fold comment>-- this is
401        -- a comment</fold>
402
403        -- separate
404        select 1;
405        ");
406    }
407
408    #[test]
409    fn fold_comments_and_multi_statements() {
410        assert_snapshot!(check("
411-- this is
412
413-- a comment
414-- with some more
415select a, b, 3
416  from t
417  where c > 10;"), @"
418        -- this is
419
420        <fold comment>-- a comment
421        -- with some more</fold>
422        <fold statement>select a, b, 3
423          from t
424          where c > 10;</fold>
425        ");
426    }
427
428    #[test]
429    fn fold_comments_does_not_apply_when_whitespace_between() {
430        assert_snapshot!(check("
431-- this is
432
433-- a comment
434-- with some more
435select 1;"), @"
436        -- this is
437
438        <fold comment>-- a comment
439        -- with some more</fold>
440        select 1;
441        ");
442    }
443
444    #[test]
445    fn fold_multiline_comments() {
446        assert_snapshot!(check("
447-- this is
448-- a comment
449select 1;"), @"
450        <fold comment>-- this is
451        -- a comment</fold>
452        select 1;
453        ");
454    }
455
456    #[test]
457    fn fold_multiline_comments_with_windows_line_endings() {
458        assert_snapshot!(
459            format!(
460                "{:?}",
461                check("-- this is\r\n-- a comment\r\nselect 1;")
462            ),
463            @r#""<fold comment>-- this is\r\n-- a comment</fold>\r\nselect 1;""#
464        );
465    }
466
467    #[test]
468    fn fold_multiline_comments_with_cr_line_endings() {
469        assert_snapshot!(
470            format!(
471                "{:?}",
472                check("-- this is\r-- a comment\rselect 1;")
473            ),
474            @r#""<fold comment>-- this is\r-- a comment</fold>\rselect 1;""#
475        );
476    }
477
478    #[test]
479    fn fold_comments_with_cr_line_endings_does_not_apply_when_whitespace_between() {
480        assert_snapshot!(
481            format!(
482                "{:?}",
483                check("-- this is\r\r-- a comment\r-- with some more\rselect 1;")
484            ),
485            @r#""-- this is\r\r<fold comment>-- a comment\r-- with some more</fold>\rselect 1;""#
486        );
487    }
488
489    #[test]
490    fn fold_statement_with_cr_line_endings() {
491        assert_snapshot!(
492            format!("{:?}", check("select\r  id,\r  name\rfrom t;")),
493            @r#""<fold statement>select\r  <fold list>id,\r  name</fold>\rfrom t;</fold>""#
494        );
495    }
496
497    #[test]
498    fn fold_single_line_no_fold() {
499        assert_snapshot!(check("select 1;"), @"select 1;");
500    }
501
502    #[test]
503    fn fold_subquery() {
504        assert_snapshot!(check("
505select * from (
506  select id from t
507);"), @"
508        <fold statement>select * from <fold statement>(
509          select id from t
510        )</fold>;</fold>
511        ");
512    }
513
514    #[test]
515    fn fold_case_when() {
516        assert_snapshot!(check("
517select
518  case
519    when x = 1 then 'a'
520    when x = 2 then 'b'
521  end
522from t;"), @"
523        <fold statement>select
524          <fold list>case
525            <fold list>when x = 1 then 'a'
526            when x = 2 then 'b'</fold>
527          end</fold>
528        from t;</fold>
529        ");
530    }
531
532    #[test]
533    fn fold_join() {
534        assert_snapshot!(check("
535select *
536from a
537join b
538  on a.id = b.id;"), @"
539        <fold statement>select *
540        from a
541        <fold join>join b
542          on a.id = b.id</fold>;</fold>
543        ");
544    }
545
546    #[test]
547    fn fold_where_clause() {
548        assert_snapshot!(check("
549select *
550from t
551where
552  a = 1
553  and b = 2;"), @"
554        <fold statement>select *
555        from t
556        <fold where_clause>where
557          a = 1
558          and b = 2</fold>;</fold>
559        ");
560    }
561
562    #[test]
563    fn fold_array_literal() {
564        assert_snapshot!(check("
565select * from t where
566  x = any(array[
567    1,
568    2,
569    3
570  ]);"), @"
571        <fold statement>select * from t <fold where_clause>where
572          x = <fold function_call>any(<fold array>array[
573            1,
574            2,
575            3
576          ]</fold>)</fold></fold>;</fold>
577        ");
578    }
579
580    #[test]
581    fn fold_tuple_literal() {
582        assert_snapshot!(check("
583select (
584  1,
585  2,
586  3
587);"), @"
588        <fold statement>select <fold list><fold tuple>(
589          1,
590          2,
591          3
592        )</fold></fold>;</fold>
593        ");
594    }
595
596    #[test]
597    fn fold_tuple_bin_expr() {
598        assert_snapshot!(check("
599select * from x
600  where z in (
601    1,
602    2,
603    3,
604    4,
605    5
606  );
607"), @"
608        <fold statement>select * from x
609          <fold where_clause>where z in <fold tuple>(
610            1,
611            2,
612            3,
613            4,
614            5
615          )</fold></fold>;</fold>
616        ");
617    }
618
619    #[test]
620    fn fold_function_call() {
621        assert_snapshot!(check("
622select coalesce(
623  a,
624  b,
625  c
626);"), @"
627        <fold statement>select <fold function_call><fold list>coalesce<fold arglist>(
628          a,
629          b,
630          c
631        )</fold></fold></fold>;</fold>
632        ");
633    }
634
635    #[test]
636    fn fold_create_enum() {
637        assert_snapshot!(check("
638create type status as enum (
639  'active',
640  'inactive'
641);"), @"
642        <fold statement>create type status as enum <fold list>(
643          'active',
644          'inactive'
645        )</fold>;</fold>
646        ");
647    }
648
649    #[test]
650    fn fold_insert_values() {
651        assert_snapshot!(check("
652insert into t (id, name)
653values
654  (1, 'a'),
655  (2, 'b');"), @"
656        <fold statement>insert into t (id, name)
657        <fold statement>values
658          <fold list>(1, 'a'),
659          (2, 'b')</fold></fold>;</fold>
660        ");
661    }
662
663    #[test]
664    fn no_fold_single_line_create_table() {
665        assert_snapshot!(check("create table t (id int);"), @"create table t (id int);");
666    }
667
668    #[test]
669    fn list_variants() {
670        let unhandled_list_kinds: Vec<SyntaxKind> = (0..SyntaxKind::__LAST as u16)
671            .map(SyntaxKind::from)
672            .filter(|kind| format!("{kind:?}").ends_with("_LIST"))
673            .filter(|kind| fold_kind(*kind).is_none())
674            .collect();
675
676        assert_eq!(
677            unhandled_list_kinds,
678            vec![],
679            "All _LIST SyntaxKind variants should be handled in fold_kind"
680        );
681    }
682}