aboutsummaryrefslogtreecommitdiff
path: root/crates/ra_ide_api_light
diff options
context:
space:
mode:
Diffstat (limited to 'crates/ra_ide_api_light')
-rw-r--r--crates/ra_ide_api_light/Cargo.toml7
-rw-r--r--crates/ra_ide_api_light/src/join_lines.rs589
-rw-r--r--crates/ra_ide_api_light/src/lib.rs144
-rw-r--r--crates/ra_ide_api_light/src/snapshots/tests__highlighting.snap32
-rw-r--r--crates/ra_ide_api_light/src/test_utils.rs24
-rw-r--r--crates/ra_ide_api_light/src/typing.rs407
6 files changed, 7 insertions, 1196 deletions
diff --git a/crates/ra_ide_api_light/Cargo.toml b/crates/ra_ide_api_light/Cargo.toml
index a30833dc3..4e69f5325 100644
--- a/crates/ra_ide_api_light/Cargo.toml
+++ b/crates/ra_ide_api_light/Cargo.toml
@@ -17,5 +17,10 @@ ra_fmt = { path = "../ra_fmt" }
17 17
18[dev-dependencies] 18[dev-dependencies]
19test_utils = { path = "../test_utils" } 19test_utils = { path = "../test_utils" }
20proptest = "0.9.0"
21insta = "0.7.0" 20insta = "0.7.0"
21
22[dev-dependencies.proptest]
23version = "0.9.0"
24# Disable `fork` feature to allow compiling on webassembly
25default-features = false
26features = ["std", "bit-set", "break-dead-code"]
diff --git a/crates/ra_ide_api_light/src/join_lines.rs b/crates/ra_ide_api_light/src/join_lines.rs
deleted file mode 100644
index b5bcd62fb..000000000
--- a/crates/ra_ide_api_light/src/join_lines.rs
+++ /dev/null
@@ -1,589 +0,0 @@
1use itertools::Itertools;
2use ra_syntax::{
3 SourceFile, TextRange, TextUnit, AstNode, SyntaxNode,
4 SyntaxKind::{self, WHITESPACE, COMMA, R_CURLY, R_PAREN, R_BRACK},
5 algo::{find_covering_node, non_trivia_sibling},
6 ast,
7 Direction,
8};
9use ra_fmt::{
10 compute_ws, extract_trivial_expression
11};
12use crate::{
13 LocalEdit, TextEditBuilder,
14};
15
16pub fn join_lines(file: &SourceFile, range: TextRange) -> LocalEdit {
17 let range = if range.is_empty() {
18 let syntax = file.syntax();
19 let text = syntax.text().slice(range.start()..);
20 let pos = match text.find('\n') {
21 None => {
22 return LocalEdit {
23 label: "join lines".to_string(),
24 edit: TextEditBuilder::default().finish(),
25 cursor_position: None,
26 };
27 }
28 Some(pos) => pos,
29 };
30 TextRange::offset_len(range.start() + pos, TextUnit::of_char('\n'))
31 } else {
32 range
33 };
34
35 let node = find_covering_node(file.syntax(), range);
36 let mut edit = TextEditBuilder::default();
37 for node in node.descendants() {
38 let text = match node.leaf_text() {
39 Some(text) => text,
40 None => continue,
41 };
42 let range = match range.intersection(&node.range()) {
43 Some(range) => range,
44 None => continue,
45 } - node.range().start();
46 for (pos, _) in text[range].bytes().enumerate().filter(|&(_, b)| b == b'\n') {
47 let pos: TextUnit = (pos as u32).into();
48 let off = node.range().start() + range.start() + pos;
49 if !edit.invalidates_offset(off) {
50 remove_newline(&mut edit, node, text.as_str(), off);
51 }
52 }
53 }
54
55 LocalEdit { label: "join lines".to_string(), edit: edit.finish(), cursor_position: None }
56}
57
58fn remove_newline(
59 edit: &mut TextEditBuilder,
60 node: &SyntaxNode,
61 node_text: &str,
62 offset: TextUnit,
63) {
64 if node.kind() != WHITESPACE || node_text.bytes().filter(|&b| b == b'\n').count() != 1 {
65 // The node is either the first or the last in the file
66 let suff = &node_text[TextRange::from_to(
67 offset - node.range().start() + TextUnit::of_char('\n'),
68 TextUnit::of_str(node_text),
69 )];
70 let spaces = suff.bytes().take_while(|&b| b == b' ').count();
71
72 edit.replace(TextRange::offset_len(offset, ((spaces + 1) as u32).into()), " ".to_string());
73 return;
74 }
75
76 // Special case that turns something like:
77 //
78 // ```
79 // my_function({<|>
80 // <some-expr>
81 // })
82 // ```
83 //
84 // into `my_function(<some-expr>)`
85 if join_single_expr_block(edit, node).is_some() {
86 return;
87 }
88 // ditto for
89 //
90 // ```
91 // use foo::{<|>
92 // bar
93 // };
94 // ```
95 if join_single_use_tree(edit, node).is_some() {
96 return;
97 }
98
99 // The node is between two other nodes
100 let prev = node.prev_sibling().unwrap();
101 let next = node.next_sibling().unwrap();
102 if is_trailing_comma(prev.kind(), next.kind()) {
103 // Removes: trailing comma, newline (incl. surrounding whitespace)
104 edit.delete(TextRange::from_to(prev.range().start(), node.range().end()));
105 } else if prev.kind() == COMMA && next.kind() == R_CURLY {
106 // Removes: comma, newline (incl. surrounding whitespace)
107 let space = if let Some(left) = prev.prev_sibling() { compute_ws(left, next) } else { " " };
108 edit.replace(
109 TextRange::from_to(prev.range().start(), node.range().end()),
110 space.to_string(),
111 );
112 } else if let (Some(_), Some(next)) = (ast::Comment::cast(prev), ast::Comment::cast(next)) {
113 // Removes: newline (incl. surrounding whitespace), start of the next comment
114 edit.delete(TextRange::from_to(
115 node.range().start(),
116 next.syntax().range().start() + TextUnit::of_str(next.prefix()),
117 ));
118 } else {
119 // Remove newline but add a computed amount of whitespace characters
120 edit.replace(node.range(), compute_ws(prev, next).to_string());
121 }
122}
123
124fn has_comma_after(node: &SyntaxNode) -> bool {
125 match non_trivia_sibling(node, Direction::Next) {
126 Some(n) => n.kind() == COMMA,
127 _ => false,
128 }
129}
130
131fn join_single_expr_block(edit: &mut TextEditBuilder, node: &SyntaxNode) -> Option<()> {
132 let block = ast::Block::cast(node.parent()?)?;
133 let block_expr = ast::BlockExpr::cast(block.syntax().parent()?)?;
134 let expr = extract_trivial_expression(block)?;
135
136 let block_range = block_expr.syntax().range();
137 let mut buf = expr.syntax().text().to_string();
138
139 // Match block needs to have a comma after the block
140 if let Some(match_arm) = block_expr.syntax().parent().and_then(ast::MatchArm::cast) {
141 if !has_comma_after(match_arm.syntax()) {
142 buf.push(',');
143 }
144 }
145
146 edit.replace(block_range, buf);
147
148 Some(())
149}
150
151fn join_single_use_tree(edit: &mut TextEditBuilder, node: &SyntaxNode) -> Option<()> {
152 let use_tree_list = ast::UseTreeList::cast(node.parent()?)?;
153 let (tree,) = use_tree_list.use_trees().collect_tuple()?;
154 edit.replace(use_tree_list.syntax().range(), tree.syntax().text().to_string());
155 Some(())
156}
157
158fn is_trailing_comma(left: SyntaxKind, right: SyntaxKind) -> bool {
159 match (left, right) {
160 (COMMA, R_PAREN) | (COMMA, R_BRACK) => true,
161 _ => false,
162 }
163}
164
165#[cfg(test)]
166mod tests {
167 use crate::test_utils::{assert_eq_text, check_action, extract_range};
168
169 use super::*;
170
171 fn check_join_lines(before: &str, after: &str) {
172 check_action(before, after, |file, offset| {
173 let range = TextRange::offset_len(offset, 0.into());
174 let res = join_lines(file, range);
175 Some(res)
176 })
177 }
178
179 #[test]
180 fn test_join_lines_comma() {
181 check_join_lines(
182 r"
183fn foo() {
184 <|>foo(1,
185 )
186}
187",
188 r"
189fn foo() {
190 <|>foo(1)
191}
192",
193 );
194 }
195
196 #[test]
197 fn test_join_lines_lambda_block() {
198 check_join_lines(
199 r"
200pub fn reparse(&self, edit: &AtomTextEdit) -> File {
201 <|>self.incremental_reparse(edit).unwrap_or_else(|| {
202 self.full_reparse(edit)
203 })
204}
205",
206 r"
207pub fn reparse(&self, edit: &AtomTextEdit) -> File {
208 <|>self.incremental_reparse(edit).unwrap_or_else(|| self.full_reparse(edit))
209}
210",
211 );
212 }
213
214 #[test]
215 fn test_join_lines_block() {
216 check_join_lines(
217 r"
218fn foo() {
219 foo(<|>{
220 92
221 })
222}",
223 r"
224fn foo() {
225 foo(<|>92)
226}",
227 );
228 }
229
230 #[test]
231 fn join_lines_adds_comma_for_block_in_match_arm() {
232 check_join_lines(
233 r"
234fn foo(e: Result<U, V>) {
235 match e {
236 Ok(u) => <|>{
237 u.foo()
238 }
239 Err(v) => v,
240 }
241}",
242 r"
243fn foo(e: Result<U, V>) {
244 match e {
245 Ok(u) => <|>u.foo(),
246 Err(v) => v,
247 }
248}",
249 );
250 }
251
252 #[test]
253 fn join_lines_keeps_comma_for_block_in_match_arm() {
254 // We already have a comma
255 check_join_lines(
256 r"
257fn foo(e: Result<U, V>) {
258 match e {
259 Ok(u) => <|>{
260 u.foo()
261 },
262 Err(v) => v,
263 }
264}",
265 r"
266fn foo(e: Result<U, V>) {
267 match e {
268 Ok(u) => <|>u.foo(),
269 Err(v) => v,
270 }
271}",
272 );
273
274 // comma with whitespace between brace and ,
275 check_join_lines(
276 r"
277fn foo(e: Result<U, V>) {
278 match e {
279 Ok(u) => <|>{
280 u.foo()
281 } ,
282 Err(v) => v,
283 }
284}",
285 r"
286fn foo(e: Result<U, V>) {
287 match e {
288 Ok(u) => <|>u.foo() ,
289 Err(v) => v,
290 }
291}",
292 );
293
294 // comma with newline between brace and ,
295 check_join_lines(
296 r"
297fn foo(e: Result<U, V>) {
298 match e {
299 Ok(u) => <|>{
300 u.foo()
301 }
302 ,
303 Err(v) => v,
304 }
305}",
306 r"
307fn foo(e: Result<U, V>) {
308 match e {
309 Ok(u) => <|>u.foo()
310 ,
311 Err(v) => v,
312 }
313}",
314 );
315 }
316
317 #[test]
318 fn join_lines_keeps_comma_with_single_arg_tuple() {
319 // A single arg tuple
320 check_join_lines(
321 r"
322fn foo() {
323 let x = (<|>{
324 4
325 },);
326}",
327 r"
328fn foo() {
329 let x = (<|>4,);
330}",
331 );
332
333 // single arg tuple with whitespace between brace and comma
334 check_join_lines(
335 r"
336fn foo() {
337 let x = (<|>{
338 4
339 } ,);
340}",
341 r"
342fn foo() {
343 let x = (<|>4 ,);
344}",
345 );
346
347 // single arg tuple with newline between brace and comma
348 check_join_lines(
349 r"
350fn foo() {
351 let x = (<|>{
352 4
353 }
354 ,);
355}",
356 r"
357fn foo() {
358 let x = (<|>4
359 ,);
360}",
361 );
362 }
363
364 #[test]
365 fn test_join_lines_use_items_left() {
366 // No space after the '{'
367 check_join_lines(
368 r"
369<|>use ra_syntax::{
370 TextUnit, TextRange,
371};",
372 r"
373<|>use ra_syntax::{TextUnit, TextRange,
374};",
375 );
376 }
377
378 #[test]
379 fn test_join_lines_use_items_right() {
380 // No space after the '}'
381 check_join_lines(
382 r"
383use ra_syntax::{
384<|> TextUnit, TextRange
385};",
386 r"
387use ra_syntax::{
388<|> TextUnit, TextRange};",
389 );
390 }
391
392 #[test]
393 fn test_join_lines_use_items_right_comma() {
394 // No space after the '}'
395 check_join_lines(
396 r"
397use ra_syntax::{
398<|> TextUnit, TextRange,
399};",
400 r"
401use ra_syntax::{
402<|> TextUnit, TextRange};",
403 );
404 }
405
406 #[test]
407 fn test_join_lines_use_tree() {
408 check_join_lines(
409 r"
410use ra_syntax::{
411 algo::<|>{
412 find_leaf_at_offset,
413 },
414 ast,
415};",
416 r"
417use ra_syntax::{
418 algo::<|>find_leaf_at_offset,
419 ast,
420};",
421 );
422 }
423
424 #[test]
425 fn test_join_lines_normal_comments() {
426 check_join_lines(
427 r"
428fn foo() {
429 // Hello<|>
430 // world!
431}
432",
433 r"
434fn foo() {
435 // Hello<|> world!
436}
437",
438 );
439 }
440
441 #[test]
442 fn test_join_lines_doc_comments() {
443 check_join_lines(
444 r"
445fn foo() {
446 /// Hello<|>
447 /// world!
448}
449",
450 r"
451fn foo() {
452 /// Hello<|> world!
453}
454",
455 );
456 }
457
458 #[test]
459 fn test_join_lines_mod_comments() {
460 check_join_lines(
461 r"
462fn foo() {
463 //! Hello<|>
464 //! world!
465}
466",
467 r"
468fn foo() {
469 //! Hello<|> world!
470}
471",
472 );
473 }
474
475 #[test]
476 fn test_join_lines_multiline_comments_1() {
477 check_join_lines(
478 r"
479fn foo() {
480 // Hello<|>
481 /* world! */
482}
483",
484 r"
485fn foo() {
486 // Hello<|> world! */
487}
488",
489 );
490 }
491
492 #[test]
493 fn test_join_lines_multiline_comments_2() {
494 check_join_lines(
495 r"
496fn foo() {
497 // The<|>
498 /* quick
499 brown
500 fox! */
501}
502",
503 r"
504fn foo() {
505 // The<|> quick
506 brown
507 fox! */
508}
509",
510 );
511 }
512
513 fn check_join_lines_sel(before: &str, after: &str) {
514 let (sel, before) = extract_range(before);
515 let file = SourceFile::parse(&before);
516 let result = join_lines(&file, sel);
517 let actual = result.edit.apply(&before);
518 assert_eq_text!(after, &actual);
519 }
520
521 #[test]
522 fn test_join_lines_selection_fn_args() {
523 check_join_lines_sel(
524 r"
525fn foo() {
526 <|>foo(1,
527 2,
528 3,
529 <|>)
530}
531 ",
532 r"
533fn foo() {
534 foo(1, 2, 3)
535}
536 ",
537 );
538 }
539
540 #[test]
541 fn test_join_lines_selection_struct() {
542 check_join_lines_sel(
543 r"
544struct Foo <|>{
545 f: u32,
546}<|>
547 ",
548 r"
549struct Foo { f: u32 }
550 ",
551 );
552 }
553
554 #[test]
555 fn test_join_lines_selection_dot_chain() {
556 check_join_lines_sel(
557 r"
558fn foo() {
559 join(<|>type_params.type_params()
560 .filter_map(|it| it.name())
561 .map(|it| it.text())<|>)
562}",
563 r"
564fn foo() {
565 join(type_params.type_params().filter_map(|it| it.name()).map(|it| it.text()))
566}",
567 );
568 }
569
570 #[test]
571 fn test_join_lines_selection_lambda_block_body() {
572 check_join_lines_sel(
573 r"
574pub fn handle_find_matching_brace() {
575 params.offsets
576 .map(|offset| <|>{
577 world.analysis().matching_brace(&file, offset).unwrap_or(offset)
578 }<|>)
579 .collect();
580}",
581 r"
582pub fn handle_find_matching_brace() {
583 params.offsets
584 .map(|offset| world.analysis().matching_brace(&file, offset).unwrap_or(offset))
585 .collect();
586}",
587 );
588 }
589}
diff --git a/crates/ra_ide_api_light/src/lib.rs b/crates/ra_ide_api_light/src/lib.rs
index 4036a598e..df7f144b6 100644
--- a/crates/ra_ide_api_light/src/lib.rs
+++ b/crates/ra_ide_api_light/src/lib.rs
@@ -4,151 +4,9 @@
4//! an edit or some auxiliary info. 4//! an edit or some auxiliary info.
5 5
6mod structure; 6mod structure;
7#[cfg(test)]
8mod test_utils;
9mod join_lines;
10mod typing;
11 7
12use rustc_hash::FxHashSet; 8use ra_syntax::TextRange;
13use ra_text_edit::TextEditBuilder;
14use ra_syntax::{
15 SourceFile, SyntaxNode, TextRange, TextUnit, Direction,
16 algo::find_leaf_at_offset,
17 SyntaxKind::{self, *},
18 ast::{self, AstNode},
19};
20 9
21pub use crate::{ 10pub use crate::{
22 structure::{file_structure, StructureNode}, 11 structure::{file_structure, StructureNode},
23 join_lines::join_lines,
24 typing::{on_enter, on_dot_typed, on_eq_typed},
25}; 12};
26
27#[derive(Debug)]
28pub struct LocalEdit {
29 pub label: String,
30 pub edit: ra_text_edit::TextEdit,
31 pub cursor_position: Option<TextUnit>,
32}
33
34#[derive(Debug)]
35pub struct HighlightedRange {
36 pub range: TextRange,
37 pub tag: &'static str,
38}
39
40#[derive(Debug, Copy, Clone)]
41pub enum Severity {
42 Error,
43 WeakWarning,
44}
45
46#[derive(Debug)]
47pub struct Diagnostic {
48 pub range: TextRange,
49 pub msg: String,
50 pub severity: Severity,
51 pub fix: Option<LocalEdit>,
52}
53
54pub fn matching_brace(file: &SourceFile, offset: TextUnit) -> Option<TextUnit> {
55 const BRACES: &[SyntaxKind] =
56 &[L_CURLY, R_CURLY, L_BRACK, R_BRACK, L_PAREN, R_PAREN, L_ANGLE, R_ANGLE];
57 let (brace_node, brace_idx) = find_leaf_at_offset(file.syntax(), offset)
58 .filter_map(|node| {
59 let idx = BRACES.iter().position(|&brace| brace == node.kind())?;
60 Some((node, idx))
61 })
62 .next()?;
63 let parent = brace_node.parent()?;
64 let matching_kind = BRACES[brace_idx ^ 1];
65 let matching_node = parent.children().find(|node| node.kind() == matching_kind)?;
66 Some(matching_node.range().start())
67}
68
69pub fn highlight(root: &SyntaxNode) -> Vec<HighlightedRange> {
70 // Visited nodes to handle highlighting priorities
71 let mut highlighted = FxHashSet::default();
72 let mut res = Vec::new();
73 for node in root.descendants() {
74 if highlighted.contains(&node) {
75 continue;
76 }
77 let tag = match node.kind() {
78 COMMENT => "comment",
79 STRING | RAW_STRING | RAW_BYTE_STRING | BYTE_STRING => "string",
80 ATTR => "attribute",
81 NAME_REF => "text",
82 NAME => "function",
83 INT_NUMBER | FLOAT_NUMBER | CHAR | BYTE => "literal",
84 LIFETIME => "parameter",
85 k if k.is_keyword() => "keyword",
86 _ => {
87 if let Some(macro_call) = ast::MacroCall::cast(node) {
88 if let Some(path) = macro_call.path() {
89 if let Some(segment) = path.segment() {
90 if let Some(name_ref) = segment.name_ref() {
91 highlighted.insert(name_ref.syntax());
92 let range_start = name_ref.syntax().range().start();
93 let mut range_end = name_ref.syntax().range().end();
94 for sibling in path.syntax().siblings(Direction::Next) {
95 match sibling.kind() {
96 EXCL | IDENT => range_end = sibling.range().end(),
97 _ => (),
98 }
99 }
100 res.push(HighlightedRange {
101 range: TextRange::from_to(range_start, range_end),
102 tag: "macro",
103 })
104 }
105 }
106 }
107 }
108 continue;
109 }
110 };
111 res.push(HighlightedRange { range: node.range(), tag })
112 }
113 res
114}
115
116#[cfg(test)]
117mod tests {
118 use ra_syntax::AstNode;
119 use insta::assert_debug_snapshot_matches;
120
121 use crate::test_utils::{add_cursor, assert_eq_text, extract_offset};
122
123 use super::*;
124
125 #[test]
126 fn test_highlighting() {
127 let file = SourceFile::parse(
128 r#"
129// comment
130fn main() {}
131 println!("Hello, {}!", 92);
132"#,
133 );
134 let hls = highlight(file.syntax());
135 assert_debug_snapshot_matches!("highlighting", hls);
136 }
137
138 #[test]
139 fn test_matching_brace() {
140 fn do_check(before: &str, after: &str) {
141 let (pos, before) = extract_offset(before);
142 let file = SourceFile::parse(&before);
143 let new_pos = match matching_brace(&file, pos) {
144 None => pos,
145 Some(pos) => pos,
146 };
147 let actual = add_cursor(&before, new_pos);
148 assert_eq_text!(after, &actual);
149 }
150
151 do_check("struct Foo { a: i32, }<|>", "struct Foo <|>{ a: i32, }");
152 }
153
154}
diff --git a/crates/ra_ide_api_light/src/snapshots/tests__highlighting.snap b/crates/ra_ide_api_light/src/snapshots/tests__highlighting.snap
deleted file mode 100644
index ef306a7a0..000000000
--- a/crates/ra_ide_api_light/src/snapshots/tests__highlighting.snap
+++ /dev/null
@@ -1,32 +0,0 @@
1---
2created: "2019-01-22T14:45:01.959724300+00:00"
3creator: [email protected]
4expression: hls
5source: "crates\\ra_ide_api_light\\src\\lib.rs"
6---
7[
8 HighlightedRange {
9 range: [1; 11),
10 tag: "comment"
11 },
12 HighlightedRange {
13 range: [12; 14),
14 tag: "keyword"
15 },
16 HighlightedRange {
17 range: [15; 19),
18 tag: "function"
19 },
20 HighlightedRange {
21 range: [29; 37),
22 tag: "macro"
23 },
24 HighlightedRange {
25 range: [38; 50),
26 tag: "string"
27 },
28 HighlightedRange {
29 range: [52; 54),
30 tag: "literal"
31 }
32]
diff --git a/crates/ra_ide_api_light/src/test_utils.rs b/crates/ra_ide_api_light/src/test_utils.rs
deleted file mode 100644
index bfac0fce3..000000000
--- a/crates/ra_ide_api_light/src/test_utils.rs
+++ /dev/null
@@ -1,24 +0,0 @@
1use ra_syntax::{SourceFile, TextUnit};
2
3use crate::LocalEdit;
4pub use test_utils::*;
5
6pub fn check_action<F: Fn(&SourceFile, TextUnit) -> Option<LocalEdit>>(
7 before: &str,
8 after: &str,
9 f: F,
10) {
11 let (before_cursor_pos, before) = extract_offset(before);
12 let file = SourceFile::parse(&before);
13 let result = f(&file, before_cursor_pos).expect("code action is not applicable");
14 let actual = result.edit.apply(&before);
15 let actual_cursor_pos = match result.cursor_position {
16 None => result
17 .edit
18 .apply_to_offset(before_cursor_pos)
19 .expect("cursor position is affected by the edit"),
20 Some(off) => off,
21 };
22 let actual = add_cursor(&actual, actual_cursor_pos);
23 assert_eq_text!(after, &actual);
24}
diff --git a/crates/ra_ide_api_light/src/typing.rs b/crates/ra_ide_api_light/src/typing.rs
deleted file mode 100644
index 9dd9f1c1d..000000000
--- a/crates/ra_ide_api_light/src/typing.rs
+++ /dev/null
@@ -1,407 +0,0 @@
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, TextEditBuilder};
9
10pub fn on_enter(file: &SourceFile, offset: TextUnit) -> Option<LocalEdit> {
11 let comment =
12 find_leaf_at_offset(file.syntax(), offset).left_biased().and_then(ast::Comment::cast)?;
13
14 if let ast::CommentFlavor::Multiline = comment.flavor() {
15 return None;
16 }
17
18 let prefix = comment.prefix();
19 if offset < comment.syntax().range().start() + TextUnit::of_str(prefix) + TextUnit::from(1) {
20 return None;
21 }
22
23 let indent = node_indent(file, comment.syntax())?;
24 let inserted = format!("\n{}{} ", indent, prefix);
25 let cursor_position = offset + TextUnit::of_str(&inserted);
26 let mut edit = TextEditBuilder::default();
27 edit.insert(offset, inserted);
28 Some(LocalEdit {
29 label: "on enter".to_string(),
30 edit: edit.finish(),
31 cursor_position: Some(cursor_position),
32 })
33}
34
35fn node_indent<'a>(file: &'a SourceFile, node: &SyntaxNode) -> Option<&'a str> {
36 let ws = match find_leaf_at_offset(file.syntax(), node.range().start()) {
37 LeafAtOffset::Between(l, r) => {
38 assert!(r == node);
39 l
40 }
41 LeafAtOffset::Single(n) => {
42 assert!(n == node);
43 return Some("");
44 }
45 LeafAtOffset::None => unreachable!(),
46 };
47 if ws.kind() != WHITESPACE {
48 return None;
49 }
50 let text = ws.leaf_text().unwrap();
51 let pos = text.as_str().rfind('\n').map(|it| it + 1).unwrap_or(0);
52 Some(&text[pos..])
53}
54
55pub fn on_eq_typed(file: &SourceFile, eq_offset: TextUnit) -> Option<LocalEdit> {
56 assert_eq!(file.syntax().text().char_at(eq_offset), Some('='));
57 let let_stmt: &ast::LetStmt = find_node_at_offset(file.syntax(), eq_offset)?;
58 if let_stmt.has_semi() {
59 return None;
60 }
61 if let Some(expr) = let_stmt.initializer() {
62 let expr_range = expr.syntax().range();
63 if expr_range.contains(eq_offset) && eq_offset != expr_range.start() {
64 return None;
65 }
66 if file.syntax().text().slice(eq_offset..expr_range.start()).contains('\n') {
67 return None;
68 }
69 } else {
70 return None;
71 }
72 let offset = let_stmt.syntax().range().end();
73 let mut edit = TextEditBuilder::default();
74 edit.insert(offset, ";".to_string());
75 Some(LocalEdit {
76 label: "add semicolon".to_string(),
77 edit: edit.finish(),
78 cursor_position: None,
79 })
80}
81
82pub fn on_dot_typed(file: &SourceFile, dot_offset: TextUnit) -> Option<LocalEdit> {
83 assert_eq!(file.syntax().text().char_at(dot_offset), Some('.'));
84
85 let whitespace = find_leaf_at_offset(file.syntax(), dot_offset)
86 .left_biased()
87 .and_then(ast::Whitespace::cast)?;
88
89 let current_indent = {
90 let text = whitespace.text();
91 let newline = text.rfind('\n')?;
92 &text[newline + 1..]
93 };
94 let current_indent_len = TextUnit::of_str(current_indent);
95
96 // Make sure dot is a part of call chain
97 let field_expr = whitespace.syntax().parent().and_then(ast::FieldExpr::cast)?;
98 let prev_indent = leading_indent(field_expr.syntax())?;
99 let target_indent = format!(" {}", prev_indent);
100 let target_indent_len = TextUnit::of_str(&target_indent);
101 if current_indent_len == target_indent_len {
102 return None;
103 }
104 let mut edit = TextEditBuilder::default();
105 edit.replace(
106 TextRange::from_to(dot_offset - current_indent_len, dot_offset),
107 target_indent.into(),
108 );
109 let res = LocalEdit {
110 label: "reindent dot".to_string(),
111 edit: edit.finish(),
112 cursor_position: Some(
113 dot_offset + target_indent_len - current_indent_len + TextUnit::of_char('.'),
114 ),
115 };
116 Some(res)
117}
118
119#[cfg(test)]
120mod tests {
121 use crate::test_utils::{add_cursor, assert_eq_text, extract_offset};
122
123 use super::*;
124
125 #[test]
126 fn test_on_eq_typed() {
127 fn type_eq(before: &str, after: &str) {
128 let (offset, before) = extract_offset(before);
129 let mut edit = TextEditBuilder::default();
130 edit.insert(offset, "=".to_string());
131 let before = edit.finish().apply(&before);
132 let file = SourceFile::parse(&before);
133 if let Some(result) = on_eq_typed(&file, offset) {
134 let actual = result.edit.apply(&before);
135 assert_eq_text!(after, &actual);
136 } else {
137 assert_eq_text!(&before, after)
138 };
139 }
140
141 // do_check(r"
142 // fn foo() {
143 // let foo =<|>
144 // }
145 // ", r"
146 // fn foo() {
147 // let foo =;
148 // }
149 // ");
150 type_eq(
151 r"
152fn foo() {
153 let foo <|> 1 + 1
154}
155",
156 r"
157fn foo() {
158 let foo = 1 + 1;
159}
160",
161 );
162 // do_check(r"
163 // fn foo() {
164 // let foo =<|>
165 // let bar = 1;
166 // }
167 // ", r"
168 // fn foo() {
169 // let foo =;
170 // let bar = 1;
171 // }
172 // ");
173 }
174
175 fn type_dot(before: &str, after: &str) {
176 let (offset, before) = extract_offset(before);
177 let mut edit = TextEditBuilder::default();
178 edit.insert(offset, ".".to_string());
179 let before = edit.finish().apply(&before);
180 let file = SourceFile::parse(&before);
181 if let Some(result) = on_dot_typed(&file, offset) {
182 let actual = result.edit.apply(&before);
183 assert_eq_text!(after, &actual);
184 } else {
185 assert_eq_text!(&before, after)
186 };
187 }
188
189 #[test]
190 fn indents_new_chain_call() {
191 type_dot(
192 r"
193 pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
194 self.child_impl(db, name)
195 <|>
196 }
197 ",
198 r"
199 pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
200 self.child_impl(db, name)
201 .
202 }
203 ",
204 );
205 type_dot(
206 r"
207 pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
208 self.child_impl(db, name)
209 <|>
210 }
211 ",
212 r"
213 pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
214 self.child_impl(db, name)
215 .
216 }
217 ",
218 )
219 }
220
221 #[test]
222 fn indents_new_chain_call_with_semi() {
223 type_dot(
224 r"
225 pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
226 self.child_impl(db, name)
227 <|>;
228 }
229 ",
230 r"
231 pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
232 self.child_impl(db, name)
233 .;
234 }
235 ",
236 );
237 type_dot(
238 r"
239 pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
240 self.child_impl(db, name)
241 <|>;
242 }
243 ",
244 r"
245 pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
246 self.child_impl(db, name)
247 .;
248 }
249 ",
250 )
251 }
252
253 #[test]
254 fn indents_continued_chain_call() {
255 type_dot(
256 r"
257 pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
258 self.child_impl(db, name)
259 .first()
260 <|>
261 }
262 ",
263 r"
264 pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
265 self.child_impl(db, name)
266 .first()
267 .
268 }
269 ",
270 );
271 type_dot(
272 r"
273 pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
274 self.child_impl(db, name)
275 .first()
276 <|>
277 }
278 ",
279 r"
280 pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
281 self.child_impl(db, name)
282 .first()
283 .
284 }
285 ",
286 );
287 }
288
289 #[test]
290 fn indents_middle_of_chain_call() {
291 type_dot(
292 r"
293 fn source_impl() {
294 let var = enum_defvariant_list().unwrap()
295 <|>
296 .nth(92)
297 .unwrap();
298 }
299 ",
300 r"
301 fn source_impl() {
302 let var = enum_defvariant_list().unwrap()
303 .
304 .nth(92)
305 .unwrap();
306 }
307 ",
308 );
309 type_dot(
310 r"
311 fn source_impl() {
312 let var = enum_defvariant_list().unwrap()
313 <|>
314 .nth(92)
315 .unwrap();
316 }
317 ",
318 r"
319 fn source_impl() {
320 let var = enum_defvariant_list().unwrap()
321 .
322 .nth(92)
323 .unwrap();
324 }
325 ",
326 );
327 }
328
329 #[test]
330 fn dont_indent_freestanding_dot() {
331 type_dot(
332 r"
333 pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
334 <|>
335 }
336 ",
337 r"
338 pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
339 .
340 }
341 ",
342 );
343 type_dot(
344 r"
345 pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
346 <|>
347 }
348 ",
349 r"
350 pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
351 .
352 }
353 ",
354 );
355 }
356
357 #[test]
358 fn test_on_enter() {
359 fn apply_on_enter(before: &str) -> Option<String> {
360 let (offset, before) = extract_offset(before);
361 let file = SourceFile::parse(&before);
362 let result = on_enter(&file, offset)?;
363 let actual = result.edit.apply(&before);
364 let actual = add_cursor(&actual, result.cursor_position.unwrap());
365 Some(actual)
366 }
367
368 fn do_check(before: &str, after: &str) {
369 let actual = apply_on_enter(before).unwrap();
370 assert_eq_text!(after, &actual);
371 }
372
373 fn do_check_noop(text: &str) {
374 assert!(apply_on_enter(text).is_none())
375 }
376
377 do_check(
378 r"
379/// Some docs<|>
380fn foo() {
381}
382",
383 r"
384/// Some docs
385/// <|>
386fn foo() {
387}
388",
389 );
390 do_check(
391 r"
392impl S {
393 /// Some<|> docs.
394 fn foo() {}
395}
396",
397 r"
398impl S {
399 /// Some
400 /// <|> docs.
401 fn foo() {}
402}
403",
404 );
405 do_check_noop(r"<|>//! docz");
406 }
407}