diff options
Diffstat (limited to 'crates/ide')
-rw-r--r-- | crates/ide/src/diagnostics.rs | 16 | ||||
-rw-r--r-- | crates/ide/src/expand_macro.rs | 44 | ||||
-rw-r--r-- | crates/ide/src/folding_ranges.rs | 17 | ||||
-rw-r--r-- | crates/ide/src/syntax_highlighting/highlight.rs | 14 | ||||
-rw-r--r-- | crates/ide/src/syntax_highlighting/tests.rs | 96 | ||||
-rw-r--r-- | crates/ide/src/typing.rs | 168 | ||||
-rw-r--r-- | crates/ide/src/typing/on_enter.rs | 20 |
7 files changed, 339 insertions, 36 deletions
diff --git a/crates/ide/src/diagnostics.rs b/crates/ide/src/diagnostics.rs index dd42116a7..0ace80a1e 100644 --- a/crates/ide/src/diagnostics.rs +++ b/crates/ide/src/diagnostics.rs | |||
@@ -20,7 +20,7 @@ use itertools::Itertools; | |||
20 | use rustc_hash::FxHashSet; | 20 | use rustc_hash::FxHashSet; |
21 | use syntax::{ | 21 | use syntax::{ |
22 | ast::{self, AstNode}, | 22 | ast::{self, AstNode}, |
23 | SyntaxNode, SyntaxNodePtr, TextRange, | 23 | SyntaxNode, SyntaxNodePtr, TextRange, TextSize, |
24 | }; | 24 | }; |
25 | use text_edit::TextEdit; | 25 | use text_edit::TextEdit; |
26 | use unlinked_file::UnlinkedFile; | 26 | use unlinked_file::UnlinkedFile; |
@@ -159,14 +159,16 @@ pub(crate) fn diagnostics( | |||
159 | ); | 159 | ); |
160 | }) | 160 | }) |
161 | .on::<UnlinkedFile, _>(|d| { | 161 | .on::<UnlinkedFile, _>(|d| { |
162 | // Limit diagnostic to the first few characters in the file. This matches how VS Code | ||
163 | // renders it with the full span, but on other editors, and is less invasive. | ||
164 | let range = sema.diagnostics_display_range(d.display_source()).range; | ||
165 | let range = range.intersect(TextRange::up_to(TextSize::of("..."))).unwrap_or(range); | ||
166 | |||
162 | // Override severity and mark as unused. | 167 | // Override severity and mark as unused. |
163 | res.borrow_mut().push( | 168 | res.borrow_mut().push( |
164 | Diagnostic::hint( | 169 | Diagnostic::hint(range, d.message()) |
165 | sema.diagnostics_display_range(d.display_source()).range, | 170 | .with_fix(d.fix(&sema)) |
166 | d.message(), | 171 | .with_code(Some(d.code())), |
167 | ) | ||
168 | .with_fix(d.fix(&sema)) | ||
169 | .with_code(Some(d.code())), | ||
170 | ); | 172 | ); |
171 | }) | 173 | }) |
172 | .on::<hir::diagnostics::UnresolvedProcMacro, _>(|d| { | 174 | .on::<hir::diagnostics::UnresolvedProcMacro, _>(|d| { |
diff --git a/crates/ide/src/expand_macro.rs b/crates/ide/src/expand_macro.rs index 9eeabbeda..be0ee03bf 100644 --- a/crates/ide/src/expand_macro.rs +++ b/crates/ide/src/expand_macro.rs | |||
@@ -1,3 +1,5 @@ | |||
1 | use std::iter; | ||
2 | |||
1 | use hir::Semantics; | 3 | use hir::Semantics; |
2 | use ide_db::RootDatabase; | 4 | use ide_db::RootDatabase; |
3 | use syntax::{ | 5 | use syntax::{ |
@@ -91,24 +93,42 @@ fn insert_whitespaces(syn: SyntaxNode) -> String { | |||
91 | let is_last = | 93 | let is_last = |
92 | |f: fn(SyntaxKind) -> bool, default| -> bool { last.map(f).unwrap_or(default) }; | 94 | |f: fn(SyntaxKind) -> bool, default| -> bool { last.map(f).unwrap_or(default) }; |
93 | 95 | ||
94 | res += &match token.kind() { | 96 | match token.kind() { |
95 | k if is_text(k) && is_next(|it| !it.is_punct(), true) => token.text().to_string() + " ", | 97 | k if is_text(k) && is_next(|it| !it.is_punct(), true) => { |
98 | res.push_str(token.text()); | ||
99 | res.push(' '); | ||
100 | } | ||
96 | L_CURLY if is_next(|it| it != R_CURLY, true) => { | 101 | L_CURLY if is_next(|it| it != R_CURLY, true) => { |
97 | indent += 1; | 102 | indent += 1; |
98 | let leading_space = if is_last(is_text, false) { " " } else { "" }; | 103 | if is_last(is_text, false) { |
99 | format!("{}{{\n{}", leading_space, " ".repeat(indent)) | 104 | res.push(' '); |
105 | } | ||
106 | res.push_str("{\n"); | ||
107 | res.extend(iter::repeat(" ").take(2 * indent)); | ||
100 | } | 108 | } |
101 | R_CURLY if is_last(|it| it != L_CURLY, true) => { | 109 | R_CURLY if is_last(|it| it != L_CURLY, true) => { |
102 | indent = indent.saturating_sub(1); | 110 | indent = indent.saturating_sub(1); |
103 | format!("\n{}}}", " ".repeat(indent)) | 111 | res.push('\n'); |
112 | res.extend(iter::repeat(" ").take(2 * indent)); | ||
113 | res.push_str("}"); | ||
104 | } | 114 | } |
105 | R_CURLY => format!("}}\n{}", " ".repeat(indent)), | 115 | R_CURLY => { |
106 | T![;] => format!(";\n{}", " ".repeat(indent)), | 116 | res.push_str("}\n"); |
107 | T![->] => " -> ".to_string(), | 117 | res.extend(iter::repeat(" ").take(2 * indent)); |
108 | T![=] => " = ".to_string(), | 118 | } |
109 | T![=>] => " => ".to_string(), | 119 | LIFETIME_IDENT if is_next(|it| it == IDENT, true) => { |
110 | _ => token.text().to_string(), | 120 | res.push_str(token.text()); |
111 | }; | 121 | res.push(' '); |
122 | } | ||
123 | T![;] => { | ||
124 | res.push_str(";\n"); | ||
125 | res.extend(iter::repeat(" ").take(2 * indent)); | ||
126 | } | ||
127 | T![->] => res.push_str(" -> "), | ||
128 | T![=] => res.push_str(" = "), | ||
129 | T![=>] => res.push_str(" => "), | ||
130 | _ => res.push_str(token.text()), | ||
131 | } | ||
112 | 132 | ||
113 | last = Some(token.kind()); | 133 | last = Some(token.kind()); |
114 | } | 134 | } |
diff --git a/crates/ide/src/folding_ranges.rs b/crates/ide/src/folding_ranges.rs index 153726ce8..2b9ed123c 100644 --- a/crates/ide/src/folding_ranges.rs +++ b/crates/ide/src/folding_ranges.rs | |||
@@ -19,6 +19,7 @@ pub enum FoldKind { | |||
19 | Region, | 19 | Region, |
20 | Consts, | 20 | Consts, |
21 | Statics, | 21 | Statics, |
22 | Array, | ||
22 | } | 23 | } |
23 | 24 | ||
24 | #[derive(Debug)] | 25 | #[derive(Debug)] |
@@ -119,6 +120,7 @@ fn fold_kind(kind: SyntaxKind) -> Option<FoldKind> { | |||
119 | match kind { | 120 | match kind { |
120 | COMMENT => Some(FoldKind::Comment), | 121 | COMMENT => Some(FoldKind::Comment), |
121 | ARG_LIST | PARAM_LIST => Some(FoldKind::ArgList), | 122 | ARG_LIST | PARAM_LIST => Some(FoldKind::ArgList), |
123 | ARRAY_EXPR => Some(FoldKind::Array), | ||
122 | ASSOC_ITEM_LIST | 124 | ASSOC_ITEM_LIST |
123 | | RECORD_FIELD_LIST | 125 | | RECORD_FIELD_LIST |
124 | | RECORD_PAT_FIELD_LIST | 126 | | RECORD_PAT_FIELD_LIST |
@@ -269,6 +271,7 @@ mod tests { | |||
269 | FoldKind::Region => "region", | 271 | FoldKind::Region => "region", |
270 | FoldKind::Consts => "consts", | 272 | FoldKind::Consts => "consts", |
271 | FoldKind::Statics => "statics", | 273 | FoldKind::Statics => "statics", |
274 | FoldKind::Array => "array", | ||
272 | }; | 275 | }; |
273 | assert_eq!(kind, &attr.unwrap()); | 276 | assert_eq!(kind, &attr.unwrap()); |
274 | } | 277 | } |
@@ -465,6 +468,20 @@ fn foo<fold arglist>( | |||
465 | } | 468 | } |
466 | 469 | ||
467 | #[test] | 470 | #[test] |
471 | fn fold_multiline_array() { | ||
472 | check( | ||
473 | r#" | ||
474 | const FOO: [usize; 4] = <fold array>[ | ||
475 | 1, | ||
476 | 2, | ||
477 | 3, | ||
478 | 4, | ||
479 | ]</fold>; | ||
480 | "#, | ||
481 | ) | ||
482 | } | ||
483 | |||
484 | #[test] | ||
468 | fn fold_region() { | 485 | fn fold_region() { |
469 | check( | 486 | check( |
470 | r#" | 487 | r#" |
diff --git a/crates/ide/src/syntax_highlighting/highlight.rs b/crates/ide/src/syntax_highlighting/highlight.rs index 5ccb84714..e921784bf 100644 --- a/crates/ide/src/syntax_highlighting/highlight.rs +++ b/crates/ide/src/syntax_highlighting/highlight.rs | |||
@@ -323,8 +323,18 @@ fn highlight_def(db: &RootDatabase, def: Definition) -> Highlight { | |||
323 | hir::ModuleDef::Trait(_) => HlTag::Symbol(SymbolKind::Trait), | 323 | hir::ModuleDef::Trait(_) => HlTag::Symbol(SymbolKind::Trait), |
324 | hir::ModuleDef::TypeAlias(type_) => { | 324 | hir::ModuleDef::TypeAlias(type_) => { |
325 | let mut h = Highlight::new(HlTag::Symbol(SymbolKind::TypeAlias)); | 325 | let mut h = Highlight::new(HlTag::Symbol(SymbolKind::TypeAlias)); |
326 | if type_.as_assoc_item(db).is_some() { | 326 | if let Some(item) = type_.as_assoc_item(db) { |
327 | h |= HlMod::Associated | 327 | h |= HlMod::Associated; |
328 | match item.container(db) { | ||
329 | AssocItemContainer::Impl(i) => { | ||
330 | if i.trait_(db).is_some() { | ||
331 | h |= HlMod::Trait; | ||
332 | } | ||
333 | } | ||
334 | AssocItemContainer::Trait(_t) => { | ||
335 | h |= HlMod::Trait; | ||
336 | } | ||
337 | } | ||
328 | } | 338 | } |
329 | return h; | 339 | return h; |
330 | } | 340 | } |
diff --git a/crates/ide/src/syntax_highlighting/tests.rs b/crates/ide/src/syntax_highlighting/tests.rs index 1b02857ec..de2d22ac7 100644 --- a/crates/ide/src/syntax_highlighting/tests.rs +++ b/crates/ide/src/syntax_highlighting/tests.rs | |||
@@ -1,5 +1,8 @@ | |||
1 | use std::time::Instant; | ||
2 | |||
1 | use expect_test::{expect_file, ExpectFile}; | 3 | use expect_test::{expect_file, ExpectFile}; |
2 | use ide_db::SymbolKind; | 4 | use ide_db::SymbolKind; |
5 | use stdx::format_to; | ||
3 | use test_utils::{bench, bench_fixture, skip_slow_tests}; | 6 | use test_utils::{bench, bench_fixture, skip_slow_tests}; |
4 | 7 | ||
5 | use crate::{fixture, FileRange, HlTag, TextRange}; | 8 | use crate::{fixture, FileRange, HlTag, TextRange}; |
@@ -258,6 +261,99 @@ fn benchmark_syntax_highlighting_long_struct() { | |||
258 | } | 261 | } |
259 | 262 | ||
260 | #[test] | 263 | #[test] |
264 | fn syntax_highlighting_not_quadratic() { | ||
265 | if skip_slow_tests() { | ||
266 | return; | ||
267 | } | ||
268 | |||
269 | let mut measures = Vec::new(); | ||
270 | for i in 6..=10 { | ||
271 | let n = 1 << i; | ||
272 | let fixture = bench_fixture::big_struct_n(n); | ||
273 | let (analysis, file_id) = fixture::file(&fixture); | ||
274 | |||
275 | let time = Instant::now(); | ||
276 | |||
277 | let hash = analysis | ||
278 | .highlight(file_id) | ||
279 | .unwrap() | ||
280 | .iter() | ||
281 | .filter(|it| it.highlight.tag == HlTag::Symbol(SymbolKind::Struct)) | ||
282 | .count(); | ||
283 | assert!(hash > n as usize); | ||
284 | |||
285 | let elapsed = time.elapsed(); | ||
286 | measures.push((n as f64, elapsed.as_millis() as f64)) | ||
287 | } | ||
288 | |||
289 | assert_linear(&measures) | ||
290 | } | ||
291 | |||
292 | /// Checks that a set of measurements looks like a liner function rather than | ||
293 | /// like a quadratic function. Algorithm: | ||
294 | /// | ||
295 | /// 1. Linearly scale input to be in [0; 1) | ||
296 | /// 2. Using linear regression, compute the best linear function approximating | ||
297 | /// the input. | ||
298 | /// 3. Compute RMSE and maximal absolute error. | ||
299 | /// 4. Check that errors are within tolerances and that the constant term is not | ||
300 | /// too negative. | ||
301 | /// | ||
302 | /// Ideally, we should use a proper "model selection" to directly compare | ||
303 | /// quadratic and linear models, but that sounds rather complicated: | ||
304 | /// | ||
305 | /// https://stats.stackexchange.com/questions/21844/selecting-best-model-based-on-linear-quadratic-and-cubic-fit-of-data | ||
306 | fn assert_linear(xy: &[(f64, f64)]) { | ||
307 | let (mut xs, mut ys): (Vec<_>, Vec<_>) = xy.iter().copied().unzip(); | ||
308 | normalize(&mut xs); | ||
309 | normalize(&mut ys); | ||
310 | let xy = xs.iter().copied().zip(ys.iter().copied()); | ||
311 | |||
312 | // Linear regression: finding a and b to fit y = a + b*x. | ||
313 | |||
314 | let mean_x = mean(&xs); | ||
315 | let mean_y = mean(&ys); | ||
316 | |||
317 | let b = { | ||
318 | let mut num = 0.0; | ||
319 | let mut denom = 0.0; | ||
320 | for (x, y) in xy.clone() { | ||
321 | num += (x - mean_x) * (y - mean_y); | ||
322 | denom += (x - mean_x).powi(2); | ||
323 | } | ||
324 | num / denom | ||
325 | }; | ||
326 | |||
327 | let a = mean_y - b * mean_x; | ||
328 | |||
329 | let mut plot = format!("y_pred = {:.3} + {:.3} * x\n\nx y y_pred\n", a, b); | ||
330 | |||
331 | let mut se = 0.0; | ||
332 | let mut max_error = 0.0f64; | ||
333 | for (x, y) in xy { | ||
334 | let y_pred = a + b * x; | ||
335 | se += (y - y_pred).powi(2); | ||
336 | max_error = max_error.max((y_pred - y).abs()); | ||
337 | |||
338 | format_to!(plot, "{:.3} {:.3} {:.3}\n", x, y, y_pred); | ||
339 | } | ||
340 | |||
341 | let rmse = (se / xs.len() as f64).sqrt(); | ||
342 | format_to!(plot, "\nrmse = {:.3} max error = {:.3}", rmse, max_error); | ||
343 | |||
344 | assert!(rmse < 0.05 && max_error < 0.1 && a > -0.1, "\nLooks quadratic\n{}", plot); | ||
345 | |||
346 | fn normalize(xs: &mut Vec<f64>) { | ||
347 | let max = xs.iter().copied().max_by(|a, b| a.partial_cmp(b).unwrap()).unwrap(); | ||
348 | xs.iter_mut().for_each(|it| *it /= max); | ||
349 | } | ||
350 | |||
351 | fn mean(xs: &[f64]) -> f64 { | ||
352 | xs.iter().copied().sum::<f64>() / (xs.len() as f64) | ||
353 | } | ||
354 | } | ||
355 | |||
356 | #[test] | ||
261 | fn benchmark_syntax_highlighting_parser() { | 357 | fn benchmark_syntax_highlighting_parser() { |
262 | if skip_slow_tests() { | 358 | if skip_slow_tests() { |
263 | return; | 359 | return; |
diff --git a/crates/ide/src/typing.rs b/crates/ide/src/typing.rs index 11408d445..1378048e5 100644 --- a/crates/ide/src/typing.rs +++ b/crates/ide/src/typing.rs | |||
@@ -22,18 +22,19 @@ use ide_db::{ | |||
22 | use syntax::{ | 22 | use syntax::{ |
23 | algo::find_node_at_offset, | 23 | algo::find_node_at_offset, |
24 | ast::{self, edit::IndentLevel, AstToken}, | 24 | ast::{self, edit::IndentLevel, AstToken}, |
25 | AstNode, SourceFile, | 25 | AstNode, Parse, SourceFile, |
26 | SyntaxKind::{FIELD_EXPR, METHOD_CALL_EXPR}, | 26 | SyntaxKind::{FIELD_EXPR, METHOD_CALL_EXPR}, |
27 | TextRange, TextSize, | 27 | TextRange, TextSize, |
28 | }; | 28 | }; |
29 | 29 | ||
30 | use text_edit::TextEdit; | 30 | use text_edit::{Indel, TextEdit}; |
31 | 31 | ||
32 | use crate::SourceChange; | 32 | use crate::SourceChange; |
33 | 33 | ||
34 | pub(crate) use on_enter::on_enter; | 34 | pub(crate) use on_enter::on_enter; |
35 | 35 | ||
36 | pub(crate) const TRIGGER_CHARS: &str = ".=>"; | 36 | // Don't forget to add new trigger characters to `server_capabilities` in `caps.rs`. |
37 | pub(crate) const TRIGGER_CHARS: &str = ".=>{"; | ||
37 | 38 | ||
38 | // Feature: On Typing Assists | 39 | // Feature: On Typing Assists |
39 | // | 40 | // |
@@ -41,6 +42,7 @@ pub(crate) const TRIGGER_CHARS: &str = ".=>"; | |||
41 | // | 42 | // |
42 | // - typing `let =` tries to smartly add `;` if `=` is followed by an existing expression | 43 | // - typing `let =` tries to smartly add `;` if `=` is followed by an existing expression |
43 | // - typing `.` in a chain method call auto-indents | 44 | // - typing `.` in a chain method call auto-indents |
45 | // - typing `{` in front of an expression inserts a closing `}` after the expression | ||
44 | // | 46 | // |
45 | // VS Code:: | 47 | // VS Code:: |
46 | // | 48 | // |
@@ -57,28 +59,79 @@ pub(crate) fn on_char_typed( | |||
57 | position: FilePosition, | 59 | position: FilePosition, |
58 | char_typed: char, | 60 | char_typed: char, |
59 | ) -> Option<SourceChange> { | 61 | ) -> Option<SourceChange> { |
60 | assert!(TRIGGER_CHARS.contains(char_typed)); | 62 | if !stdx::always!(TRIGGER_CHARS.contains(char_typed)) { |
61 | let file = &db.parse(position.file_id).tree(); | 63 | return None; |
62 | assert_eq!(file.syntax().text().char_at(position.offset), Some(char_typed)); | 64 | } |
65 | let file = &db.parse(position.file_id); | ||
66 | if !stdx::always!(file.tree().syntax().text().char_at(position.offset) == Some(char_typed)) { | ||
67 | return None; | ||
68 | } | ||
63 | let edit = on_char_typed_inner(file, position.offset, char_typed)?; | 69 | let edit = on_char_typed_inner(file, position.offset, char_typed)?; |
64 | Some(SourceChange::from_text_edit(position.file_id, edit)) | 70 | Some(SourceChange::from_text_edit(position.file_id, edit)) |
65 | } | 71 | } |
66 | 72 | ||
67 | fn on_char_typed_inner(file: &SourceFile, offset: TextSize, char_typed: char) -> Option<TextEdit> { | 73 | fn on_char_typed_inner( |
68 | assert!(TRIGGER_CHARS.contains(char_typed)); | 74 | file: &Parse<SourceFile>, |
75 | offset: TextSize, | ||
76 | char_typed: char, | ||
77 | ) -> Option<TextEdit> { | ||
78 | if !stdx::always!(TRIGGER_CHARS.contains(char_typed)) { | ||
79 | return None; | ||
80 | } | ||
69 | match char_typed { | 81 | match char_typed { |
70 | '.' => on_dot_typed(file, offset), | 82 | '.' => on_dot_typed(&file.tree(), offset), |
71 | '=' => on_eq_typed(file, offset), | 83 | '=' => on_eq_typed(&file.tree(), offset), |
72 | '>' => on_arrow_typed(file, offset), | 84 | '>' => on_arrow_typed(&file.tree(), offset), |
85 | '{' => on_opening_brace_typed(file, offset), | ||
73 | _ => unreachable!(), | 86 | _ => unreachable!(), |
74 | } | 87 | } |
75 | } | 88 | } |
76 | 89 | ||
90 | /// Inserts a closing `}` when the user types an opening `{`, wrapping an existing expression in a | ||
91 | /// block. | ||
92 | fn on_opening_brace_typed(file: &Parse<SourceFile>, offset: TextSize) -> Option<TextEdit> { | ||
93 | if !stdx::always!(file.tree().syntax().text().char_at(offset) == Some('{')) { | ||
94 | return None; | ||
95 | } | ||
96 | |||
97 | let brace_token = file.tree().syntax().token_at_offset(offset).right_biased()?; | ||
98 | |||
99 | // Remove the `{` to get a better parse tree, and reparse | ||
100 | let file = file.reparse(&Indel::delete(brace_token.text_range())); | ||
101 | |||
102 | let mut expr: ast::Expr = find_node_at_offset(file.tree().syntax(), offset)?; | ||
103 | if expr.syntax().text_range().start() != offset { | ||
104 | return None; | ||
105 | } | ||
106 | |||
107 | // Enclose the outermost expression starting at `offset` | ||
108 | while let Some(parent) = expr.syntax().parent() { | ||
109 | if parent.text_range().start() != expr.syntax().text_range().start() { | ||
110 | break; | ||
111 | } | ||
112 | |||
113 | match ast::Expr::cast(parent) { | ||
114 | Some(parent) => expr = parent, | ||
115 | None => break, | ||
116 | } | ||
117 | } | ||
118 | |||
119 | // If it's a statement in a block, we don't know how many statements should be included | ||
120 | if ast::ExprStmt::can_cast(expr.syntax().parent()?.kind()) { | ||
121 | return None; | ||
122 | } | ||
123 | |||
124 | // Insert `}` right after the expression. | ||
125 | Some(TextEdit::insert(expr.syntax().text_range().end() + TextSize::of("{"), "}".to_string())) | ||
126 | } | ||
127 | |||
77 | /// Returns an edit which should be applied after `=` was typed. Primarily, | 128 | /// Returns an edit which should be applied after `=` was typed. Primarily, |
78 | /// this works when adding `let =`. | 129 | /// this works when adding `let =`. |
79 | // FIXME: use a snippet completion instead of this hack here. | 130 | // FIXME: use a snippet completion instead of this hack here. |
80 | fn on_eq_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> { | 131 | fn on_eq_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> { |
81 | assert_eq!(file.syntax().text().char_at(offset), Some('=')); | 132 | if !stdx::always!(file.syntax().text().char_at(offset) == Some('=')) { |
133 | return None; | ||
134 | } | ||
82 | let let_stmt: ast::LetStmt = find_node_at_offset(file.syntax(), offset)?; | 135 | let let_stmt: ast::LetStmt = find_node_at_offset(file.syntax(), offset)?; |
83 | if let_stmt.semicolon_token().is_some() { | 136 | if let_stmt.semicolon_token().is_some() { |
84 | return None; | 137 | return None; |
@@ -100,7 +153,9 @@ fn on_eq_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> { | |||
100 | 153 | ||
101 | /// Returns an edit which should be applied when a dot ('.') is typed on a blank line, indenting the line appropriately. | 154 | /// Returns an edit which should be applied when a dot ('.') is typed on a blank line, indenting the line appropriately. |
102 | fn on_dot_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> { | 155 | fn on_dot_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> { |
103 | assert_eq!(file.syntax().text().char_at(offset), Some('.')); | 156 | if !stdx::always!(file.syntax().text().char_at(offset) == Some('.')) { |
157 | return None; | ||
158 | } | ||
104 | let whitespace = | 159 | let whitespace = |
105 | file.syntax().token_at_offset(offset).left_biased().and_then(ast::Whitespace::cast)?; | 160 | file.syntax().token_at_offset(offset).left_biased().and_then(ast::Whitespace::cast)?; |
106 | 161 | ||
@@ -129,7 +184,9 @@ fn on_dot_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> { | |||
129 | /// Adds a space after an arrow when `fn foo() { ... }` is turned into `fn foo() -> { ... }` | 184 | /// Adds a space after an arrow when `fn foo() { ... }` is turned into `fn foo() -> { ... }` |
130 | fn on_arrow_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> { | 185 | fn on_arrow_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> { |
131 | let file_text = file.syntax().text(); | 186 | let file_text = file.syntax().text(); |
132 | assert_eq!(file_text.char_at(offset), Some('>')); | 187 | if !stdx::always!(file_text.char_at(offset) == Some('>')) { |
188 | return None; | ||
189 | } | ||
133 | let after_arrow = offset + TextSize::of('>'); | 190 | let after_arrow = offset + TextSize::of('>'); |
134 | if file_text.char_at(after_arrow) != Some('{') { | 191 | if file_text.char_at(after_arrow) != Some('{') { |
135 | return None; | 192 | return None; |
@@ -152,7 +209,7 @@ mod tests { | |||
152 | let edit = TextEdit::insert(offset, char_typed.to_string()); | 209 | let edit = TextEdit::insert(offset, char_typed.to_string()); |
153 | edit.apply(&mut before); | 210 | edit.apply(&mut before); |
154 | let parse = SourceFile::parse(&before); | 211 | let parse = SourceFile::parse(&before); |
155 | on_char_typed_inner(&parse.tree(), offset, char_typed).map(|it| { | 212 | on_char_typed_inner(&parse, offset, char_typed).map(|it| { |
156 | it.apply(&mut before); | 213 | it.apply(&mut before); |
157 | before.to_string() | 214 | before.to_string() |
158 | }) | 215 | }) |
@@ -373,4 +430,85 @@ fn main() { | |||
373 | fn adds_space_after_return_type() { | 430 | fn adds_space_after_return_type() { |
374 | type_char('>', "fn foo() -$0{ 92 }", "fn foo() -> { 92 }") | 431 | type_char('>', "fn foo() -$0{ 92 }", "fn foo() -> { 92 }") |
375 | } | 432 | } |
433 | |||
434 | #[test] | ||
435 | fn adds_closing_brace() { | ||
436 | type_char( | ||
437 | '{', | ||
438 | r#" | ||
439 | fn f() { match () { _ => $0() } } | ||
440 | "#, | ||
441 | r#" | ||
442 | fn f() { match () { _ => {()} } } | ||
443 | "#, | ||
444 | ); | ||
445 | type_char( | ||
446 | '{', | ||
447 | r#" | ||
448 | fn f() { $0() } | ||
449 | "#, | ||
450 | r#" | ||
451 | fn f() { {()} } | ||
452 | "#, | ||
453 | ); | ||
454 | type_char( | ||
455 | '{', | ||
456 | r#" | ||
457 | fn f() { let x = $0(); } | ||
458 | "#, | ||
459 | r#" | ||
460 | fn f() { let x = {()}; } | ||
461 | "#, | ||
462 | ); | ||
463 | type_char( | ||
464 | '{', | ||
465 | r#" | ||
466 | fn f() { let x = $0a.b(); } | ||
467 | "#, | ||
468 | r#" | ||
469 | fn f() { let x = {a.b()}; } | ||
470 | "#, | ||
471 | ); | ||
472 | type_char( | ||
473 | '{', | ||
474 | r#" | ||
475 | const S: () = $0(); | ||
476 | fn f() {} | ||
477 | "#, | ||
478 | r#" | ||
479 | const S: () = {()}; | ||
480 | fn f() {} | ||
481 | "#, | ||
482 | ); | ||
483 | type_char( | ||
484 | '{', | ||
485 | r#" | ||
486 | const S: () = $0a.b(); | ||
487 | fn f() {} | ||
488 | "#, | ||
489 | r#" | ||
490 | const S: () = {a.b()}; | ||
491 | fn f() {} | ||
492 | "#, | ||
493 | ); | ||
494 | type_char( | ||
495 | '{', | ||
496 | r#" | ||
497 | fn f() { | ||
498 | match x { | ||
499 | 0 => $0(), | ||
500 | 1 => (), | ||
501 | } | ||
502 | } | ||
503 | "#, | ||
504 | r#" | ||
505 | fn f() { | ||
506 | match x { | ||
507 | 0 => {()}, | ||
508 | 1 => (), | ||
509 | } | ||
510 | } | ||
511 | "#, | ||
512 | ); | ||
513 | } | ||
376 | } | 514 | } |
diff --git a/crates/ide/src/typing/on_enter.rs b/crates/ide/src/typing/on_enter.rs index 9144681bf..6f1ce3689 100644 --- a/crates/ide/src/typing/on_enter.rs +++ b/crates/ide/src/typing/on_enter.rs | |||
@@ -18,6 +18,7 @@ use text_edit::TextEdit; | |||
18 | // | 18 | // |
19 | // - kbd:[Enter] inside triple-slash comments automatically inserts `///` | 19 | // - kbd:[Enter] inside triple-slash comments automatically inserts `///` |
20 | // - kbd:[Enter] in the middle or after a trailing space in `//` inserts `//` | 20 | // - kbd:[Enter] in the middle or after a trailing space in `//` inserts `//` |
21 | // - kbd:[Enter] inside `//!` doc comments automatically inserts `//!` | ||
21 | // | 22 | // |
22 | // This action needs to be assigned to shortcut explicitly. | 23 | // This action needs to be assigned to shortcut explicitly. |
23 | // | 24 | // |
@@ -187,6 +188,25 @@ fn foo() { | |||
187 | } | 188 | } |
188 | 189 | ||
189 | #[test] | 190 | #[test] |
191 | fn continues_another_doc_comment() { | ||
192 | do_check( | ||
193 | r#" | ||
194 | fn main() { | ||
195 | //! Documentation for$0 on enter | ||
196 | let x = 1 + 1; | ||
197 | } | ||
198 | "#, | ||
199 | r#" | ||
200 | fn main() { | ||
201 | //! Documentation for | ||
202 | //! $0 on enter | ||
203 | let x = 1 + 1; | ||
204 | } | ||
205 | "#, | ||
206 | ); | ||
207 | } | ||
208 | |||
209 | #[test] | ||
190 | fn continues_code_comment_in_the_middle_of_line() { | 210 | fn continues_code_comment_in_the_middle_of_line() { |
191 | do_check( | 211 | do_check( |
192 | r" | 212 | r" |