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::{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                    // skip any leading whitespace / comments
85                    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    // Only fold comments of the same flavor
186    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                    // Ignore whitespace without blank lines
199                    continue;
200                }
201                if let Some(c) = ast::Comment::cast(token) {
202                    visited.insert(c.clone());
203                    last = c;
204                    continue;
205                }
206                // The comment group ends because either:
207                // * An element of a different kind was reached
208                // * A comment of a different flavor was reached
209                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        // The group consists of only one element, therefore it cannot be folded
222        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}