aboutsummaryrefslogtreecommitdiff
path: root/crates/libeditor/src/completion.rs
blob: 7e8669822132122eeb2e40b6f07a024fee2a5c2a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
use libsyntax2::{
    File, TextUnit, AstNode,
    ast::self,
    algo::{
        ancestors,
    },
};

use {
    AtomEdit, find_node_at_offset,
    scope::{FnScopes, ModuleScope},
};

#[derive(Debug)]
pub struct CompletionItem {
    pub name: String,
    pub snippet: Option<String>
}

pub fn scope_completion(file: &File, offset: TextUnit) -> Option<Vec<CompletionItem>> {
    // Insert a fake ident to get a valid parse tree
    let file = {
        let edit = AtomEdit::insert(offset, "intellijRulezz".to_string());
        // Don't bother with completion if incremental reparse fails
        file.incremental_reparse(&edit)?
    };
    let name_ref = find_node_at_offset::<ast::NameRef>(file.syntax(), offset)?;
    let mut res = Vec::new();
    if let Some(fn_def) = ancestors(name_ref.syntax()).filter_map(ast::FnDef::cast).next() {
        let scopes = FnScopes::new(fn_def);
        complete_fn(name_ref, &scopes, &mut res);
    }
    if let Some(root) = ancestors(name_ref.syntax()).filter_map(ast::Root::cast).next() {
        let scope = ModuleScope::new(root);
        res.extend(
            scope.entries().iter()
                .map(|entry| CompletionItem {
                    name: entry.name().to_string(),
                    snippet: None,
                })
        )
    }
    Some(res)
}

fn complete_fn(name_ref: ast::NameRef, scopes: &FnScopes, acc: &mut Vec<CompletionItem>) {
    acc.extend(
        scopes.scope_chain(name_ref.syntax())
            .flat_map(|scope| scopes.entries(scope).iter())
            .map(|entry| CompletionItem {
                name: entry.name().to_string(),
                snippet: None,
            })
    )
}

#[cfg(test)]
mod tests {
    use super::*;
    use test_utils::{assert_eq_dbg, extract_offset};

    fn do_check(code: &str, expected_completions: &str) {
        let (off, code) = extract_offset(&code);
        let file = File::parse(&code);
        let completions = scope_completion(&file, off).unwrap();
        assert_eq_dbg(expected_completions, &completions);
    }

    #[test]
    fn test_completion_let_scope() {
        do_check(r"
            fn quux(x: i32) {
                let y = 92;
                1 + <|>;
                let z = ();
            }
            ", r#"[CompletionItem { name: "y" },
                   CompletionItem { name: "x" },
                   CompletionItem { name: "quux" }]"#);
    }

    #[test]
    fn test_completion_if_let_scope() {
        do_check(r"
            fn quux() {
                if let Some(x) = foo() {
                    let y = 92;
                };
                if let Some(a) = bar() {
                    let b = 62;
                    1 + <|>
                }
            }
            ", r#"[CompletionItem { name: "b" },
                   CompletionItem { name: "a" },
                   CompletionItem { name: "quux" }]"#);
    }

    #[test]
    fn test_completion_for_scope() {
        do_check(r"
            fn quux() {
                for x in &[1, 2, 3] {
                    <|>
                }
            }
            ", r#"[CompletionItem { name: "x" },
                   CompletionItem { name: "quux" }]"#);
    }

    #[test]
    fn test_completion_mod_scope() {
        do_check(r"
            struct Foo;
            enum Baz {}
            fn quux() {
                <|>
            }
            ", r#"[CompletionItem { name: "Foo" },
                   CompletionItem { name: "Baz" },
                   CompletionItem { name: "quux" }]"#);
    }
}