]> git.lizzy.rs Git - rust.git/blob - crates/ra_assists/src/assist_ctx.rs
ra_assists: assist "providers" can produce multiple assists
[rust.git] / crates / ra_assists / src / assist_ctx.rs
1 use hir::db::HirDatabase;
2 use ra_text_edit::TextEditBuilder;
3 use ra_db::FileRange;
4 use ra_syntax::{
5     SourceFile, TextRange, AstNode, TextUnit, SyntaxNode,
6     algo::{find_leaf_at_offset, find_node_at_offset, find_covering_node, LeafAtOffset},
7 };
8 use ra_fmt::{leading_indent, reindent};
9
10 use crate::{AssistLabel, AssistAction};
11
12 #[derive(Clone, Debug)]
13 pub(crate) enum Assist {
14     Unresolved(Vec<AssistLabel>),
15     Resolved(Vec<(AssistLabel, AssistAction)>),
16 }
17
18 /// `AssistCtx` allows to apply an assist or check if it could be applied.
19 ///
20 /// Assists use a somewhat over-engineered approach, given the current needs. The
21 /// assists workflow consists of two phases. In the first phase, a user asks for
22 /// the list of available assists. In the second phase, the user picks a
23 /// particular assist and it gets applied.
24 ///
25 /// There are two peculiarities here:
26 ///
27 /// * first, we ideally avoid computing more things then necessary to answer
28 ///   "is assist applicable" in the first phase.
29 /// * second, when we are applying assist, we don't have a guarantee that there
30 ///   weren't any changes between the point when user asked for assists and when
31 ///   they applied a particular assist. So, when applying assist, we need to do
32 ///   all the checks from scratch.
33 ///
34 /// To avoid repeating the same code twice for both "check" and "apply"
35 /// functions, we use an approach reminiscent of that of Django's function based
36 /// views dealing with forms. Each assist receives a runtime parameter,
37 /// `should_compute_edit`. It first check if an edit is applicable (potentially
38 /// computing info required to compute the actual edit). If it is applicable,
39 /// and `should_compute_edit` is `true`, it then computes the actual edit.
40 ///
41 /// So, to implement the original assists workflow, we can first apply each edit
42 /// with `should_compute_edit = false`, and then applying the selected edit
43 /// again, with `should_compute_edit = true` this time.
44 ///
45 /// Note, however, that we don't actually use such two-phase logic at the
46 /// moment, because the LSP API is pretty awkward in this place, and it's much
47 /// easier to just compute the edit eagerly :-)#[derive(Debug, Clone)]
48 #[derive(Debug)]
49 pub(crate) struct AssistCtx<'a, DB> {
50     pub(crate) db: &'a DB,
51     pub(crate) frange: FileRange,
52     source_file: &'a SourceFile,
53     should_compute_edit: bool,
54     assist: Assist,
55 }
56
57 impl<'a, DB> Clone for AssistCtx<'a, DB> {
58     fn clone(&self) -> Self {
59         AssistCtx {
60             db: self.db,
61             frange: self.frange,
62             source_file: self.source_file,
63             should_compute_edit: self.should_compute_edit,
64             assist: self.assist.clone(),
65         }
66     }
67 }
68
69 impl<'a, DB: HirDatabase> AssistCtx<'a, DB> {
70     pub(crate) fn with_ctx<F, T>(db: &DB, frange: FileRange, should_compute_edit: bool, f: F) -> T
71     where
72         F: FnOnce(AssistCtx<DB>) -> T,
73     {
74         let source_file = &db.parse(frange.file_id);
75         let assist =
76             if should_compute_edit { Assist::Resolved(vec![]) } else { Assist::Unresolved(vec![]) };
77
78         let ctx = AssistCtx { db, frange, source_file, should_compute_edit, assist };
79         f(ctx)
80     }
81
82     pub(crate) fn add_action(
83         &mut self,
84         label: impl Into<String>,
85         f: impl FnOnce(&mut AssistBuilder),
86     ) -> &mut Self {
87         let label = AssistLabel { label: label.into() };
88         match &mut self.assist {
89             Assist::Unresolved(labels) => labels.push(label),
90             Assist::Resolved(labels_actions) => {
91                 let action = {
92                     let mut edit = AssistBuilder::default();
93                     f(&mut edit);
94                     edit.build()
95                 };
96                 labels_actions.push((label, action));
97             }
98         }
99         self
100     }
101
102     pub(crate) fn build(self) -> Option<Assist> {
103         Some(self.assist)
104     }
105
106     pub(crate) fn leaf_at_offset(&self) -> LeafAtOffset<&'a SyntaxNode> {
107         find_leaf_at_offset(self.source_file.syntax(), self.frange.range.start())
108     }
109
110     pub(crate) fn node_at_offset<N: AstNode>(&self) -> Option<&'a N> {
111         find_node_at_offset(self.source_file.syntax(), self.frange.range.start())
112     }
113     pub(crate) fn covering_node(&self) -> &'a SyntaxNode {
114         find_covering_node(self.source_file.syntax(), self.frange.range)
115     }
116 }
117
118 #[derive(Default)]
119 pub(crate) struct AssistBuilder {
120     edit: TextEditBuilder,
121     cursor_position: Option<TextUnit>,
122     target: Option<TextRange>,
123 }
124
125 impl AssistBuilder {
126     pub(crate) fn replace(&mut self, range: TextRange, replace_with: impl Into<String>) {
127         self.edit.replace(range, replace_with.into())
128     }
129
130     pub(crate) fn replace_node_and_indent(
131         &mut self,
132         node: &SyntaxNode,
133         replace_with: impl Into<String>,
134     ) {
135         let mut replace_with = replace_with.into();
136         if let Some(indent) = leading_indent(node) {
137             replace_with = reindent(&replace_with, indent)
138         }
139         self.replace(node.range(), replace_with)
140     }
141
142     #[allow(unused)]
143     pub(crate) fn delete(&mut self, range: TextRange) {
144         self.edit.delete(range)
145     }
146
147     pub(crate) fn insert(&mut self, offset: TextUnit, text: impl Into<String>) {
148         self.edit.insert(offset, text.into())
149     }
150
151     pub(crate) fn set_cursor(&mut self, offset: TextUnit) {
152         self.cursor_position = Some(offset)
153     }
154
155     pub(crate) fn target(&mut self, target: TextRange) {
156         self.target = Some(target)
157     }
158
159     fn build(self) -> AssistAction {
160         AssistAction {
161             edit: self.edit.finish(),
162             cursor_position: self.cursor_position,
163             target: self.target,
164         }
165     }
166 }