squawk_ide/
code_actions.rs

1use rowan::TextSize;
2use squawk_linter::Edit;
3use squawk_syntax::{
4    SyntaxKind,
5    ast::{self, AstNode},
6};
7
8#[derive(Debug, Clone)]
9pub struct CodeAction {
10    pub title: String,
11    pub edits: Vec<Edit>,
12}
13
14pub fn code_actions(file: ast::SourceFile, offset: TextSize) -> Option<Vec<CodeAction>> {
15    let mut actions = vec![];
16    remove_else_clause(&mut actions, &file, offset);
17    Some(actions)
18}
19
20fn remove_else_clause(
21    actions: &mut Vec<CodeAction>,
22    file: &ast::SourceFile,
23    offset: TextSize,
24) -> Option<()> {
25    let else_token = file
26        .syntax()
27        .token_at_offset(offset)
28        .find(|x| x.kind() == SyntaxKind::ELSE_KW)?;
29    let parent = else_token.parent()?;
30    let else_clause = ast::ElseClause::cast(parent)?;
31
32    let mut edits = vec![];
33    edits.push(Edit::delete(else_clause.syntax().text_range()));
34    if let Some(token) = else_token.prev_token() {
35        if token.kind() == SyntaxKind::WHITESPACE {
36            edits.push(Edit::delete(token.text_range()));
37        }
38    }
39
40    actions.push(CodeAction {
41        title: "Remove `else` clause".to_owned(),
42        edits,
43    });
44    Some(())
45}
46
47#[cfg(test)]
48mod test {
49    use super::*;
50    use crate::test_utils::fixture;
51    use insta::assert_snapshot;
52    use rowan::TextSize;
53    use squawk_syntax::ast;
54
55    fn apply_code_action(
56        f: impl Fn(&mut Vec<CodeAction>, &ast::SourceFile, TextSize) -> Option<()>,
57        sql: &str,
58    ) -> String {
59        let (offset, sql) = fixture(sql);
60        let parse = ast::SourceFile::parse(&sql);
61        assert_eq!(parse.errors(), vec![]);
62        let file: ast::SourceFile = parse.tree();
63
64        let mut actions = vec![];
65        f(&mut actions, &file, offset);
66
67        assert!(
68            !actions.is_empty(),
69            "We should always have actions for `apply_code_action`. If you want to ensure there are no actions, use `code_action_not_applicable` instead."
70        );
71
72        let action = &actions[0];
73        let mut result = sql.clone();
74
75        let mut edits = action.edits.clone();
76        edits.sort_by_key(|e| e.text_range.start());
77        check_overlap(&edits);
78        edits.reverse();
79
80        for edit in edits {
81            let start: usize = edit.text_range.start().into();
82            let end: usize = edit.text_range.end().into();
83            let replacement = edit.text.as_deref().unwrap_or("");
84            result.replace_range(start..end, replacement);
85        }
86
87        let reparse = ast::SourceFile::parse(&result);
88        assert_eq!(
89            reparse.errors(),
90            vec![],
91            "Code actions shouldn't cause syntax errors"
92        );
93
94        result
95    }
96
97    // There's an invariant where the edits can't overlap.
98    // For example, if we have an edit that deletes the full `else clause` and
99    // another edit that deletes the `else` keyword and they overlap, then
100    // vscode doesn't surface the code action.
101    fn check_overlap(edits: &[Edit]) {
102        for (edit_i, edit_j) in edits.iter().zip(edits.iter().skip(1)) {
103            if let Some(intersection) = edit_i.text_range.intersect(edit_j.text_range) {
104                assert!(
105                    intersection.is_empty(),
106                    "Edit ranges must not overlap: {:?} and {:?} intersect at {:?}",
107                    edit_i.text_range,
108                    edit_j.text_range,
109                    intersection
110                );
111            }
112        }
113    }
114
115    fn code_action_not_applicable(
116        f: impl Fn(&mut Vec<CodeAction>, &ast::SourceFile, TextSize) -> Option<()>,
117        sql: &str,
118    ) -> bool {
119        let (offset, sql) = fixture(sql);
120        let parse = ast::SourceFile::parse(&sql);
121        assert_eq!(parse.errors(), vec![]);
122        let file: ast::SourceFile = parse.tree();
123
124        let mut actions = vec![];
125        f(&mut actions, &file, offset);
126        actions.is_empty()
127    }
128
129    #[test]
130    fn remove_else_clause_() {
131        assert_snapshot!(apply_code_action(
132            remove_else_clause,
133            "select case x when true then 1 else$0 2 end;"),
134            @"select case x when true then 1 end;"
135        );
136    }
137
138    #[test]
139    fn remove_else_clause_before_token() {
140        assert_snapshot!(apply_code_action(
141            remove_else_clause,
142            "select case x when true then 1 $0else 2 end;"),
143            @"select case x when true then 1 end;"
144        );
145    }
146
147    #[test]
148    fn remove_else_clause_not_applicable() {
149        assert!(code_action_not_applicable(
150            remove_else_clause,
151            "select case x when true then 1 else 2 end$0;"
152        ));
153    }
154}