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