aboutsummaryrefslogtreecommitdiff
path: root/crates/ra_ide_api/src
diff options
context:
space:
mode:
Diffstat (limited to 'crates/ra_ide_api/src')
-rw-r--r--crates/ra_ide_api/src/lib.rs7
-rw-r--r--crates/ra_ide_api/src/typing.rs408
2 files changed, 412 insertions, 3 deletions
diff --git a/crates/ra_ide_api/src/lib.rs b/crates/ra_ide_api/src/lib.rs
index a838c30da..c2ef61ae2 100644
--- a/crates/ra_ide_api/src/lib.rs
+++ b/crates/ra_ide_api/src/lib.rs
@@ -37,6 +37,7 @@ mod line_index;
37mod folding_ranges; 37mod folding_ranges;
38mod line_index_utils; 38mod line_index_utils;
39mod join_lines; 39mod join_lines;
40mod typing;
40 41
41#[cfg(test)] 42#[cfg(test)]
42mod marks; 43mod marks;
@@ -295,7 +296,7 @@ impl Analysis {
295 /// up minor stuff like continuing the comment. 296 /// up minor stuff like continuing the comment.
296 pub fn on_enter(&self, position: FilePosition) -> Option<SourceChange> { 297 pub fn on_enter(&self, position: FilePosition) -> Option<SourceChange> {
297 let file = self.db.parse(position.file_id); 298 let file = self.db.parse(position.file_id);
298 let edit = ra_ide_api_light::on_enter(&file, position.offset)?; 299 let edit = typing::on_enter(&file, position.offset)?;
299 Some(SourceChange::from_local_edit(position.file_id, edit)) 300 Some(SourceChange::from_local_edit(position.file_id, edit))
300 } 301 }
301 302
@@ -304,14 +305,14 @@ impl Analysis {
304 // FIXME: use a snippet completion instead of this hack here. 305 // FIXME: use a snippet completion instead of this hack here.
305 pub fn on_eq_typed(&self, position: FilePosition) -> Option<SourceChange> { 306 pub fn on_eq_typed(&self, position: FilePosition) -> Option<SourceChange> {
306 let file = self.db.parse(position.file_id); 307 let file = self.db.parse(position.file_id);
307 let edit = ra_ide_api_light::on_eq_typed(&file, position.offset)?; 308 let edit = typing::on_eq_typed(&file, position.offset)?;
308 Some(SourceChange::from_local_edit(position.file_id, edit)) 309 Some(SourceChange::from_local_edit(position.file_id, edit))
309 } 310 }
310 311
311 /// Returns an edit which should be applied when a dot ('.') is typed on a blank line, indenting the line appropriately. 312 /// Returns an edit which should be applied when a dot ('.') is typed on a blank line, indenting the line appropriately.
312 pub fn on_dot_typed(&self, position: FilePosition) -> Option<SourceChange> { 313 pub fn on_dot_typed(&self, position: FilePosition) -> Option<SourceChange> {
313 let file = self.db.parse(position.file_id); 314 let file = self.db.parse(position.file_id);
314 let edit = ra_ide_api_light::on_dot_typed(&file, position.offset)?; 315 let edit = typing::on_dot_typed(&file, position.offset)?;
315 Some(SourceChange::from_local_edit(position.file_id, edit)) 316 Some(SourceChange::from_local_edit(position.file_id, edit))
316 } 317 }
317 318
diff --git a/crates/ra_ide_api/src/typing.rs b/crates/ra_ide_api/src/typing.rs
new file mode 100644
index 000000000..b7e023d60
--- /dev/null
+++ b/crates/ra_ide_api/src/typing.rs
@@ -0,0 +1,408 @@
1use ra_syntax::{
2 AstNode, SourceFile, SyntaxKind::*,
3 SyntaxNode, TextUnit, TextRange,
4 algo::{find_node_at_offset, find_leaf_at_offset, LeafAtOffset},
5 ast::{self, AstToken},
6};
7use ra_fmt::leading_indent;
8use crate::LocalEdit;
9use ra_text_edit::TextEditBuilder;
10
11pub fn on_enter(file: &SourceFile, offset: TextUnit) -> Option<LocalEdit> {
12 let comment =
13 find_leaf_at_offset(file.syntax(), offset).left_biased().and_then(ast::Comment::cast)?;
14
15 if let ast::CommentFlavor::Multiline = comment.flavor() {
16 return None;
17 }
18
19 let prefix = comment.prefix();
20 if offset < comment.syntax().range().start() + TextUnit::of_str(prefix) + TextUnit::from(1) {
21 return None;
22 }
23
24 let indent = node_indent(file, comment.syntax())?;
25 let inserted = format!("\n{}{} ", indent, prefix);
26 let cursor_position = offset + TextUnit::of_str(&inserted);
27 let mut edit = TextEditBuilder::default();
28 edit.insert(offset, inserted);
29 Some(LocalEdit {
30 label: "on enter".to_string(),
31 edit: edit.finish(),
32 cursor_position: Some(cursor_position),
33 })
34}
35
36fn node_indent<'a>(file: &'a SourceFile, node: &SyntaxNode) -> Option<&'a str> {
37 let ws = match find_leaf_at_offset(file.syntax(), node.range().start()) {
38 LeafAtOffset::Between(l, r) => {
39 assert!(r == node);
40 l
41 }
42 LeafAtOffset::Single(n) => {
43 assert!(n == node);
44 return Some("");
45 }
46 LeafAtOffset::None => unreachable!(),
47 };
48 if ws.kind() != WHITESPACE {
49 return None;
50 }
51 let text = ws.leaf_text().unwrap();
52 let pos = text.as_str().rfind('\n').map(|it| it + 1).unwrap_or(0);
53 Some(&text[pos..])
54}
55
56pub fn on_eq_typed(file: &SourceFile, eq_offset: TextUnit) -> Option<LocalEdit> {
57 assert_eq!(file.syntax().text().char_at(eq_offset), Some('='));
58 let let_stmt: &ast::LetStmt = find_node_at_offset(file.syntax(), eq_offset)?;
59 if let_stmt.has_semi() {
60 return None;
61 }
62 if let Some(expr) = let_stmt.initializer() {
63 let expr_range = expr.syntax().range();
64 if expr_range.contains(eq_offset) && eq_offset != expr_range.start() {
65 return None;
66 }
67 if file.syntax().text().slice(eq_offset..expr_range.start()).contains('\n') {
68 return None;
69 }
70 } else {
71 return None;
72 }
73 let offset = let_stmt.syntax().range().end();
74 let mut edit = TextEditBuilder::default();
75 edit.insert(offset, ";".to_string());
76 Some(LocalEdit {
77 label: "add semicolon".to_string(),
78 edit: edit.finish(),
79 cursor_position: None,
80 })
81}
82
83pub fn on_dot_typed(file: &SourceFile, dot_offset: TextUnit) -> Option<LocalEdit> {
84 assert_eq!(file.syntax().text().char_at(dot_offset), Some('.'));
85
86 let whitespace = find_leaf_at_offset(file.syntax(), dot_offset)
87 .left_biased()
88 .and_then(ast::Whitespace::cast)?;
89
90 let current_indent = {
91 let text = whitespace.text();
92 let newline = text.rfind('\n')?;
93 &text[newline + 1..]
94 };
95 let current_indent_len = TextUnit::of_str(current_indent);
96
97 // Make sure dot is a part of call chain
98 let field_expr = whitespace.syntax().parent().and_then(ast::FieldExpr::cast)?;
99 let prev_indent = leading_indent(field_expr.syntax())?;
100 let target_indent = format!(" {}", prev_indent);
101 let target_indent_len = TextUnit::of_str(&target_indent);
102 if current_indent_len == target_indent_len {
103 return None;
104 }
105 let mut edit = TextEditBuilder::default();
106 edit.replace(
107 TextRange::from_to(dot_offset - current_indent_len, dot_offset),
108 target_indent.into(),
109 );
110 let res = LocalEdit {
111 label: "reindent dot".to_string(),
112 edit: edit.finish(),
113 cursor_position: Some(
114 dot_offset + target_indent_len - current_indent_len + TextUnit::of_char('.'),
115 ),
116 };
117 Some(res)
118}
119
120#[cfg(test)]
121mod tests {
122 use test_utils::{add_cursor, assert_eq_text, extract_offset};
123
124 use super::*;
125
126 #[test]
127 fn test_on_eq_typed() {
128 fn type_eq(before: &str, after: &str) {
129 let (offset, before) = extract_offset(before);
130 let mut edit = TextEditBuilder::default();
131 edit.insert(offset, "=".to_string());
132 let before = edit.finish().apply(&before);
133 let file = SourceFile::parse(&before);
134 if let Some(result) = on_eq_typed(&file, offset) {
135 let actual = result.edit.apply(&before);
136 assert_eq_text!(after, &actual);
137 } else {
138 assert_eq_text!(&before, after)
139 };
140 }
141
142 // do_check(r"
143 // fn foo() {
144 // let foo =<|>
145 // }
146 // ", r"
147 // fn foo() {
148 // let foo =;
149 // }
150 // ");
151 type_eq(
152 r"
153fn foo() {
154 let foo <|> 1 + 1
155}
156",
157 r"
158fn foo() {
159 let foo = 1 + 1;
160}
161",
162 );
163 // do_check(r"
164 // fn foo() {
165 // let foo =<|>
166 // let bar = 1;
167 // }
168 // ", r"
169 // fn foo() {
170 // let foo =;
171 // let bar = 1;
172 // }
173 // ");
174 }
175
176 fn type_dot(before: &str, after: &str) {
177 let (offset, before) = extract_offset(before);
178 let mut edit = TextEditBuilder::default();
179 edit.insert(offset, ".".to_string());
180 let before = edit.finish().apply(&before);
181 let file = SourceFile::parse(&before);
182 if let Some(result) = on_dot_typed(&file, offset) {
183 let actual = result.edit.apply(&before);
184 assert_eq_text!(after, &actual);
185 } else {
186 assert_eq_text!(&before, after)
187 };
188 }
189
190 #[test]
191 fn indents_new_chain_call() {
192 type_dot(
193 r"
194 pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
195 self.child_impl(db, name)
196 <|>
197 }
198 ",
199 r"
200 pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
201 self.child_impl(db, name)
202 .
203 }
204 ",
205 );
206 type_dot(
207 r"
208 pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
209 self.child_impl(db, name)
210 <|>
211 }
212 ",
213 r"
214 pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
215 self.child_impl(db, name)
216 .
217 }
218 ",
219 )
220 }
221
222 #[test]
223 fn indents_new_chain_call_with_semi() {
224 type_dot(
225 r"
226 pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
227 self.child_impl(db, name)
228 <|>;
229 }
230 ",
231 r"
232 pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
233 self.child_impl(db, name)
234 .;
235 }
236 ",
237 );
238 type_dot(
239 r"
240 pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
241 self.child_impl(db, name)
242 <|>;
243 }
244 ",
245 r"
246 pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
247 self.child_impl(db, name)
248 .;
249 }
250 ",
251 )
252 }
253
254 #[test]
255 fn indents_continued_chain_call() {
256 type_dot(
257 r"
258 pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
259 self.child_impl(db, name)
260 .first()
261 <|>
262 }
263 ",
264 r"
265 pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
266 self.child_impl(db, name)
267 .first()
268 .
269 }
270 ",
271 );
272 type_dot(
273 r"
274 pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
275 self.child_impl(db, name)
276 .first()
277 <|>
278 }
279 ",
280 r"
281 pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
282 self.child_impl(db, name)
283 .first()
284 .
285 }
286 ",
287 );
288 }
289
290 #[test]
291 fn indents_middle_of_chain_call() {
292 type_dot(
293 r"
294 fn source_impl() {
295 let var = enum_defvariant_list().unwrap()
296 <|>
297 .nth(92)
298 .unwrap();
299 }
300 ",
301 r"
302 fn source_impl() {
303 let var = enum_defvariant_list().unwrap()
304 .
305 .nth(92)
306 .unwrap();
307 }
308 ",
309 );
310 type_dot(
311 r"
312 fn source_impl() {
313 let var = enum_defvariant_list().unwrap()
314 <|>
315 .nth(92)
316 .unwrap();
317 }
318 ",
319 r"
320 fn source_impl() {
321 let var = enum_defvariant_list().unwrap()
322 .
323 .nth(92)
324 .unwrap();
325 }
326 ",
327 );
328 }
329
330 #[test]
331 fn dont_indent_freestanding_dot() {
332 type_dot(
333 r"
334 pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
335 <|>
336 }
337 ",
338 r"
339 pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
340 .
341 }
342 ",
343 );
344 type_dot(
345 r"
346 pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
347 <|>
348 }
349 ",
350 r"
351 pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
352 .
353 }
354 ",
355 );
356 }
357
358 #[test]
359 fn test_on_enter() {
360 fn apply_on_enter(before: &str) -> Option<String> {
361 let (offset, before) = extract_offset(before);
362 let file = SourceFile::parse(&before);
363 let result = on_enter(&file, offset)?;
364 let actual = result.edit.apply(&before);
365 let actual = add_cursor(&actual, result.cursor_position.unwrap());
366 Some(actual)
367 }
368
369 fn do_check(before: &str, after: &str) {
370 let actual = apply_on_enter(before).unwrap();
371 assert_eq_text!(after, &actual);
372 }
373
374 fn do_check_noop(text: &str) {
375 assert!(apply_on_enter(text).is_none())
376 }
377
378 do_check(
379 r"
380/// Some docs<|>
381fn foo() {
382}
383",
384 r"
385/// Some docs
386/// <|>
387fn foo() {
388}
389",
390 );
391 do_check(
392 r"
393impl S {
394 /// Some<|> docs.
395 fn foo() {}
396}
397",
398 r"
399impl S {
400 /// Some
401 /// <|> docs.
402 fn foo() {}
403}
404",
405 );
406 do_check_noop(r"<|>//! docz");
407 }
408}