diff options
author | bors[bot] <26634292+bors[bot]@users.noreply.github.com> | 2019-10-25 12:24:43 +0100 |
---|---|---|
committer | GitHub <[email protected]> | 2019-10-25 12:24:43 +0100 |
commit | 5f779f6c46f29c63483c0e2be732377b1b87e685 (patch) | |
tree | 10e0a683d2426b7fba25bccc50f3808371202e56 /crates | |
parent | 4ab384c19d30e2ea1ead5886dfc1efc05521f075 (diff) | |
parent | d5cd8b5be2a146abe75c0aa322f2313240c8f23c (diff) |
Merge #2066
2066: insert space after `->` r=matklad a=matklad
Co-authored-by: Aleksey Kladov <[email protected]>
Diffstat (limited to 'crates')
-rw-r--r-- | crates/ra_ide_api/src/lib.rs | 129 | ||||
-rw-r--r-- | crates/ra_ide_api/src/source_change.rs | 119 | ||||
-rw-r--r-- | crates/ra_ide_api/src/typing.rs | 352 | ||||
-rw-r--r-- | crates/ra_lsp_server/src/caps.rs | 2 | ||||
-rw-r--r-- | crates/ra_lsp_server/src/main_loop/handlers.rs | 16 | ||||
-rw-r--r-- | crates/ra_text_edit/src/text_edit.rs | 18 |
6 files changed, 358 insertions, 278 deletions
diff --git a/crates/ra_ide_api/src/lib.rs b/crates/ra_ide_api/src/lib.rs index 0832229fd..d0188da44 100644 --- a/crates/ra_ide_api/src/lib.rs +++ b/crates/ra_ide_api/src/lib.rs | |||
@@ -14,6 +14,7 @@ mod db; | |||
14 | pub mod mock_analysis; | 14 | pub mod mock_analysis; |
15 | mod symbol_index; | 15 | mod symbol_index; |
16 | mod change; | 16 | mod change; |
17 | mod source_change; | ||
17 | mod feature_flags; | 18 | mod feature_flags; |
18 | 19 | ||
19 | mod status; | 20 | mod status; |
@@ -54,8 +55,6 @@ use ra_db::{ | |||
54 | CheckCanceled, FileLoader, SourceDatabase, | 55 | CheckCanceled, FileLoader, SourceDatabase, |
55 | }; | 56 | }; |
56 | use ra_syntax::{SourceFile, TextRange, TextUnit}; | 57 | use ra_syntax::{SourceFile, TextRange, TextUnit}; |
57 | use ra_text_edit::TextEdit; | ||
58 | use relative_path::RelativePathBuf; | ||
59 | 58 | ||
60 | use crate::{db::LineIndexDatabase, symbol_index::FileSymbol}; | 59 | use crate::{db::LineIndexDatabase, symbol_index::FileSymbol}; |
61 | 60 | ||
@@ -73,6 +72,7 @@ pub use crate::{ | |||
73 | line_index_utils::translate_offset_with_edit, | 72 | line_index_utils::translate_offset_with_edit, |
74 | references::{ReferenceSearchResult, SearchScope}, | 73 | references::{ReferenceSearchResult, SearchScope}, |
75 | runnables::{Runnable, RunnableKind}, | 74 | runnables::{Runnable, RunnableKind}, |
75 | source_change::{FileSystemEdit, SourceChange, SourceFileEdit}, | ||
76 | syntax_highlighting::HighlightedRange, | 76 | syntax_highlighting::HighlightedRange, |
77 | }; | 77 | }; |
78 | 78 | ||
@@ -84,99 +84,6 @@ pub use ra_db::{ | |||
84 | pub type Cancelable<T> = Result<T, Canceled>; | 84 | pub type Cancelable<T> = Result<T, Canceled>; |
85 | 85 | ||
86 | #[derive(Debug)] | 86 | #[derive(Debug)] |
87 | pub struct SourceChange { | ||
88 | pub label: String, | ||
89 | pub source_file_edits: Vec<SourceFileEdit>, | ||
90 | pub file_system_edits: Vec<FileSystemEdit>, | ||
91 | pub cursor_position: Option<FilePosition>, | ||
92 | } | ||
93 | |||
94 | impl SourceChange { | ||
95 | /// Creates a new SourceChange with the given label | ||
96 | /// from the edits. | ||
97 | pub(crate) fn from_edits<L: Into<String>>( | ||
98 | label: L, | ||
99 | source_file_edits: Vec<SourceFileEdit>, | ||
100 | file_system_edits: Vec<FileSystemEdit>, | ||
101 | ) -> Self { | ||
102 | SourceChange { | ||
103 | label: label.into(), | ||
104 | source_file_edits, | ||
105 | file_system_edits, | ||
106 | cursor_position: None, | ||
107 | } | ||
108 | } | ||
109 | |||
110 | /// Creates a new SourceChange with the given label, | ||
111 | /// containing only the given `SourceFileEdits`. | ||
112 | pub(crate) fn source_file_edits<L: Into<String>>(label: L, edits: Vec<SourceFileEdit>) -> Self { | ||
113 | SourceChange { | ||
114 | label: label.into(), | ||
115 | source_file_edits: edits, | ||
116 | file_system_edits: vec![], | ||
117 | cursor_position: None, | ||
118 | } | ||
119 | } | ||
120 | |||
121 | /// Creates a new SourceChange with the given label, | ||
122 | /// containing only the given `FileSystemEdits`. | ||
123 | pub(crate) fn file_system_edits<L: Into<String>>(label: L, edits: Vec<FileSystemEdit>) -> Self { | ||
124 | SourceChange { | ||
125 | label: label.into(), | ||
126 | source_file_edits: vec![], | ||
127 | file_system_edits: edits, | ||
128 | cursor_position: None, | ||
129 | } | ||
130 | } | ||
131 | |||
132 | /// Creates a new SourceChange with the given label, | ||
133 | /// containing only a single `SourceFileEdit`. | ||
134 | pub(crate) fn source_file_edit<L: Into<String>>(label: L, edit: SourceFileEdit) -> Self { | ||
135 | SourceChange::source_file_edits(label, vec![edit]) | ||
136 | } | ||
137 | |||
138 | /// Creates a new SourceChange with the given label | ||
139 | /// from the given `FileId` and `TextEdit` | ||
140 | pub(crate) fn source_file_edit_from<L: Into<String>>( | ||
141 | label: L, | ||
142 | file_id: FileId, | ||
143 | edit: TextEdit, | ||
144 | ) -> Self { | ||
145 | SourceChange::source_file_edit(label, SourceFileEdit { file_id, edit }) | ||
146 | } | ||
147 | |||
148 | /// Creates a new SourceChange with the given label | ||
149 | /// from the given `FileId` and `TextEdit` | ||
150 | pub(crate) fn file_system_edit<L: Into<String>>(label: L, edit: FileSystemEdit) -> Self { | ||
151 | SourceChange::file_system_edits(label, vec![edit]) | ||
152 | } | ||
153 | |||
154 | /// Sets the cursor position to the given `FilePosition` | ||
155 | pub(crate) fn with_cursor(mut self, cursor_position: FilePosition) -> Self { | ||
156 | self.cursor_position = Some(cursor_position); | ||
157 | self | ||
158 | } | ||
159 | |||
160 | /// Sets the cursor position to the given `FilePosition` | ||
161 | pub(crate) fn with_cursor_opt(mut self, cursor_position: Option<FilePosition>) -> Self { | ||
162 | self.cursor_position = cursor_position; | ||
163 | self | ||
164 | } | ||
165 | } | ||
166 | |||
167 | #[derive(Debug)] | ||
168 | pub struct SourceFileEdit { | ||
169 | pub file_id: FileId, | ||
170 | pub edit: TextEdit, | ||
171 | } | ||
172 | |||
173 | #[derive(Debug)] | ||
174 | pub enum FileSystemEdit { | ||
175 | CreateFile { source_root: SourceRootId, path: RelativePathBuf }, | ||
176 | MoveFile { src: FileId, dst_source_root: SourceRootId, dst_path: RelativePathBuf }, | ||
177 | } | ||
178 | |||
179 | #[derive(Debug)] | ||
180 | pub struct Diagnostic { | 87 | pub struct Diagnostic { |
181 | pub message: String, | 88 | pub message: String, |
182 | pub range: TextRange, | 89 | pub range: TextRange, |
@@ -407,24 +314,20 @@ impl Analysis { | |||
407 | self.with_db(|db| typing::on_enter(&db, position)) | 314 | self.with_db(|db| typing::on_enter(&db, position)) |
408 | } | 315 | } |
409 | 316 | ||
410 | /// Returns an edit which should be applied after `=` was typed. Primarily, | 317 | /// Returns an edit which should be applied after a character was typed. |
411 | /// this works when adding `let =`. | 318 | /// |
412 | // FIXME: use a snippet completion instead of this hack here. | 319 | /// This is useful for some on-the-fly fixups, like adding `;` to `let =` |
413 | pub fn on_eq_typed(&self, position: FilePosition) -> Cancelable<Option<SourceChange>> { | 320 | /// automatically. |
414 | self.with_db(|db| { | 321 | pub fn on_char_typed( |
415 | let parse = db.parse(position.file_id); | 322 | &self, |
416 | let file = parse.tree(); | 323 | position: FilePosition, |
417 | let edit = typing::on_eq_typed(&file, position.offset)?; | 324 | char_typed: char, |
418 | Some(SourceChange::source_file_edit( | 325 | ) -> Cancelable<Option<SourceChange>> { |
419 | "add semicolon", | 326 | // Fast path to not even parse the file. |
420 | SourceFileEdit { edit, file_id: position.file_id }, | 327 | if !typing::TRIGGER_CHARS.contains(char_typed) { |
421 | )) | 328 | return Ok(None); |
422 | }) | 329 | } |
423 | } | 330 | self.with_db(|db| typing::on_char_typed(&db, position, char_typed)) |
424 | |||
425 | /// Returns an edit which should be applied when a dot ('.') is typed on a blank line, indenting the line appropriately. | ||
426 | pub fn on_dot_typed(&self, position: FilePosition) -> Cancelable<Option<SourceChange>> { | ||
427 | self.with_db(|db| typing::on_dot_typed(&db, position)) | ||
428 | } | 331 | } |
429 | 332 | ||
430 | /// Returns a tree representation of symbols in the file. Useful to draw a | 333 | /// Returns a tree representation of symbols in the file. Useful to draw a |
diff --git a/crates/ra_ide_api/src/source_change.rs b/crates/ra_ide_api/src/source_change.rs new file mode 100644 index 000000000..4e63bbf6f --- /dev/null +++ b/crates/ra_ide_api/src/source_change.rs | |||
@@ -0,0 +1,119 @@ | |||
1 | //! This modules defines type to represent changes to the source code, that flow | ||
2 | //! from the server to the client. | ||
3 | //! | ||
4 | //! It can be viewed as a dual for `AnalysisChange`. | ||
5 | |||
6 | use ra_text_edit::TextEdit; | ||
7 | use relative_path::RelativePathBuf; | ||
8 | |||
9 | use crate::{FileId, FilePosition, SourceRootId, TextUnit}; | ||
10 | |||
11 | #[derive(Debug)] | ||
12 | pub struct SourceChange { | ||
13 | pub label: String, | ||
14 | pub source_file_edits: Vec<SourceFileEdit>, | ||
15 | pub file_system_edits: Vec<FileSystemEdit>, | ||
16 | pub cursor_position: Option<FilePosition>, | ||
17 | } | ||
18 | |||
19 | impl SourceChange { | ||
20 | /// Creates a new SourceChange with the given label | ||
21 | /// from the edits. | ||
22 | pub(crate) fn from_edits<L: Into<String>>( | ||
23 | label: L, | ||
24 | source_file_edits: Vec<SourceFileEdit>, | ||
25 | file_system_edits: Vec<FileSystemEdit>, | ||
26 | ) -> Self { | ||
27 | SourceChange { | ||
28 | label: label.into(), | ||
29 | source_file_edits, | ||
30 | file_system_edits, | ||
31 | cursor_position: None, | ||
32 | } | ||
33 | } | ||
34 | |||
35 | /// Creates a new SourceChange with the given label, | ||
36 | /// containing only the given `SourceFileEdits`. | ||
37 | pub(crate) fn source_file_edits<L: Into<String>>(label: L, edits: Vec<SourceFileEdit>) -> Self { | ||
38 | SourceChange { | ||
39 | label: label.into(), | ||
40 | source_file_edits: edits, | ||
41 | file_system_edits: vec![], | ||
42 | cursor_position: None, | ||
43 | } | ||
44 | } | ||
45 | |||
46 | /// Creates a new SourceChange with the given label, | ||
47 | /// containing only the given `FileSystemEdits`. | ||
48 | pub(crate) fn file_system_edits<L: Into<String>>(label: L, edits: Vec<FileSystemEdit>) -> Self { | ||
49 | SourceChange { | ||
50 | label: label.into(), | ||
51 | source_file_edits: vec![], | ||
52 | file_system_edits: edits, | ||
53 | cursor_position: None, | ||
54 | } | ||
55 | } | ||
56 | |||
57 | /// Creates a new SourceChange with the given label, | ||
58 | /// containing only a single `SourceFileEdit`. | ||
59 | pub(crate) fn source_file_edit<L: Into<String>>(label: L, edit: SourceFileEdit) -> Self { | ||
60 | SourceChange::source_file_edits(label, vec![edit]) | ||
61 | } | ||
62 | |||
63 | /// Creates a new SourceChange with the given label | ||
64 | /// from the given `FileId` and `TextEdit` | ||
65 | pub(crate) fn source_file_edit_from<L: Into<String>>( | ||
66 | label: L, | ||
67 | file_id: FileId, | ||
68 | edit: TextEdit, | ||
69 | ) -> Self { | ||
70 | SourceChange::source_file_edit(label, SourceFileEdit { file_id, edit }) | ||
71 | } | ||
72 | |||
73 | /// Creates a new SourceChange with the given label | ||
74 | /// from the given `FileId` and `TextEdit` | ||
75 | pub(crate) fn file_system_edit<L: Into<String>>(label: L, edit: FileSystemEdit) -> Self { | ||
76 | SourceChange::file_system_edits(label, vec![edit]) | ||
77 | } | ||
78 | |||
79 | /// Sets the cursor position to the given `FilePosition` | ||
80 | pub(crate) fn with_cursor(mut self, cursor_position: FilePosition) -> Self { | ||
81 | self.cursor_position = Some(cursor_position); | ||
82 | self | ||
83 | } | ||
84 | |||
85 | /// Sets the cursor position to the given `FilePosition` | ||
86 | pub(crate) fn with_cursor_opt(mut self, cursor_position: Option<FilePosition>) -> Self { | ||
87 | self.cursor_position = cursor_position; | ||
88 | self | ||
89 | } | ||
90 | } | ||
91 | |||
92 | #[derive(Debug)] | ||
93 | pub struct SourceFileEdit { | ||
94 | pub file_id: FileId, | ||
95 | pub edit: TextEdit, | ||
96 | } | ||
97 | |||
98 | #[derive(Debug)] | ||
99 | pub enum FileSystemEdit { | ||
100 | CreateFile { source_root: SourceRootId, path: RelativePathBuf }, | ||
101 | MoveFile { src: FileId, dst_source_root: SourceRootId, dst_path: RelativePathBuf }, | ||
102 | } | ||
103 | |||
104 | pub(crate) struct SingleFileChange { | ||
105 | pub label: String, | ||
106 | pub edit: TextEdit, | ||
107 | pub cursor_position: Option<TextUnit>, | ||
108 | } | ||
109 | |||
110 | impl SingleFileChange { | ||
111 | pub(crate) fn into_source_change(self, file_id: FileId) -> SourceChange { | ||
112 | SourceChange { | ||
113 | label: self.label, | ||
114 | source_file_edits: vec![SourceFileEdit { file_id, edit: self.edit }], | ||
115 | file_system_edits: Vec::new(), | ||
116 | cursor_position: self.cursor_position.map(|offset| FilePosition { file_id, offset }), | ||
117 | } | ||
118 | } | ||
119 | } | ||
diff --git a/crates/ra_ide_api/src/typing.rs b/crates/ra_ide_api/src/typing.rs index 2f5782012..26a3111fd 100644 --- a/crates/ra_ide_api/src/typing.rs +++ b/crates/ra_ide_api/src/typing.rs | |||
@@ -1,4 +1,17 @@ | |||
1 | //! FIXME: write short doc here | 1 | //! This module handles auto-magic editing actions applied together with users |
2 | //! edits. For example, if the user typed | ||
3 | //! | ||
4 | //! ```text | ||
5 | //! foo | ||
6 | //! .bar() | ||
7 | //! .baz() | ||
8 | //! | // <- cursor is here | ||
9 | //! ``` | ||
10 | //! | ||
11 | //! and types `.` next, we want to indent the dot. | ||
12 | //! | ||
13 | //! Language server executes such typing assists synchronously. That is, they | ||
14 | //! block user's typing and should be pretty fast for this reason! | ||
2 | 15 | ||
3 | use ra_db::{FilePosition, SourceDatabase}; | 16 | use ra_db::{FilePosition, SourceDatabase}; |
4 | use ra_fmt::leading_indent; | 17 | use ra_fmt::leading_indent; |
@@ -11,7 +24,7 @@ use ra_syntax::{ | |||
11 | }; | 24 | }; |
12 | use ra_text_edit::{TextEdit, TextEditBuilder}; | 25 | use ra_text_edit::{TextEdit, TextEditBuilder}; |
13 | 26 | ||
14 | use crate::{db::RootDatabase, SourceChange, SourceFileEdit}; | 27 | use crate::{db::RootDatabase, source_change::SingleFileChange, SourceChange, SourceFileEdit}; |
15 | 28 | ||
16 | pub(crate) fn on_enter(db: &RootDatabase, position: FilePosition) -> Option<SourceChange> { | 29 | pub(crate) fn on_enter(db: &RootDatabase, position: FilePosition) -> Option<SourceChange> { |
17 | let parse = db.parse(position.file_id); | 30 | let parse = db.parse(position.file_id); |
@@ -68,39 +81,67 @@ fn node_indent(file: &SourceFile, token: &SyntaxToken) -> Option<SmolStr> { | |||
68 | Some(text[pos..].into()) | 81 | Some(text[pos..].into()) |
69 | } | 82 | } |
70 | 83 | ||
71 | pub fn on_eq_typed(file: &SourceFile, eq_offset: TextUnit) -> Option<TextEdit> { | 84 | pub(crate) const TRIGGER_CHARS: &str = ".=>"; |
72 | assert_eq!(file.syntax().text().char_at(eq_offset), Some('=')); | 85 | |
73 | let let_stmt: ast::LetStmt = find_node_at_offset(file.syntax(), eq_offset)?; | 86 | pub(crate) fn on_char_typed( |
87 | db: &RootDatabase, | ||
88 | position: FilePosition, | ||
89 | char_typed: char, | ||
90 | ) -> Option<SourceChange> { | ||
91 | assert!(TRIGGER_CHARS.contains(char_typed)); | ||
92 | let file = &db.parse(position.file_id).tree(); | ||
93 | assert_eq!(file.syntax().text().char_at(position.offset), Some(char_typed)); | ||
94 | let single_file_change = on_char_typed_inner(file, position.offset, char_typed)?; | ||
95 | Some(single_file_change.into_source_change(position.file_id)) | ||
96 | } | ||
97 | |||
98 | fn on_char_typed_inner( | ||
99 | file: &SourceFile, | ||
100 | offset: TextUnit, | ||
101 | char_typed: char, | ||
102 | ) -> Option<SingleFileChange> { | ||
103 | assert!(TRIGGER_CHARS.contains(char_typed)); | ||
104 | match char_typed { | ||
105 | '.' => on_dot_typed(file, offset), | ||
106 | '=' => on_eq_typed(file, offset), | ||
107 | '>' => on_arrow_typed(file, offset), | ||
108 | _ => unreachable!(), | ||
109 | } | ||
110 | } | ||
111 | |||
112 | /// Returns an edit which should be applied after `=` was typed. Primarily, | ||
113 | /// this works when adding `let =`. | ||
114 | // FIXME: use a snippet completion instead of this hack here. | ||
115 | fn on_eq_typed(file: &SourceFile, offset: TextUnit) -> Option<SingleFileChange> { | ||
116 | assert_eq!(file.syntax().text().char_at(offset), Some('=')); | ||
117 | let let_stmt: ast::LetStmt = find_node_at_offset(file.syntax(), offset)?; | ||
74 | if let_stmt.has_semi() { | 118 | if let_stmt.has_semi() { |
75 | return None; | 119 | return None; |
76 | } | 120 | } |
77 | if let Some(expr) = let_stmt.initializer() { | 121 | if let Some(expr) = let_stmt.initializer() { |
78 | let expr_range = expr.syntax().text_range(); | 122 | let expr_range = expr.syntax().text_range(); |
79 | if expr_range.contains(eq_offset) && eq_offset != expr_range.start() { | 123 | if expr_range.contains(offset) && offset != expr_range.start() { |
80 | return None; | 124 | return None; |
81 | } | 125 | } |
82 | if file.syntax().text().slice(eq_offset..expr_range.start()).contains_char('\n') { | 126 | if file.syntax().text().slice(offset..expr_range.start()).contains_char('\n') { |
83 | return None; | 127 | return None; |
84 | } | 128 | } |
85 | } else { | 129 | } else { |
86 | return None; | 130 | return None; |
87 | } | 131 | } |
88 | let offset = let_stmt.syntax().text_range().end(); | 132 | let offset = let_stmt.syntax().text_range().end(); |
89 | let mut edit = TextEditBuilder::default(); | 133 | Some(SingleFileChange { |
90 | edit.insert(offset, ";".to_string()); | 134 | label: "add semicolon".to_string(), |
91 | Some(edit.finish()) | 135 | edit: TextEdit::insert(offset, ";".to_string()), |
136 | cursor_position: None, | ||
137 | }) | ||
92 | } | 138 | } |
93 | 139 | ||
94 | pub(crate) fn on_dot_typed(db: &RootDatabase, position: FilePosition) -> Option<SourceChange> { | 140 | /// Returns an edit which should be applied when a dot ('.') is typed on a blank line, indenting the line appropriately. |
95 | let parse = db.parse(position.file_id); | 141 | fn on_dot_typed(file: &SourceFile, offset: TextUnit) -> Option<SingleFileChange> { |
96 | assert_eq!(parse.tree().syntax().text().char_at(position.offset), Some('.')); | 142 | assert_eq!(file.syntax().text().char_at(offset), Some('.')); |
97 | 143 | let whitespace = | |
98 | let whitespace = parse | 144 | file.syntax().token_at_offset(offset).left_biased().and_then(ast::Whitespace::cast)?; |
99 | .tree() | ||
100 | .syntax() | ||
101 | .token_at_offset(position.offset) | ||
102 | .left_biased() | ||
103 | .and_then(ast::Whitespace::cast)?; | ||
104 | 145 | ||
105 | let current_indent = { | 146 | let current_indent = { |
106 | let text = whitespace.text(); | 147 | let text = whitespace.text(); |
@@ -117,20 +158,36 @@ pub(crate) fn on_dot_typed(db: &RootDatabase, position: FilePosition) -> Option< | |||
117 | if current_indent_len == target_indent_len { | 158 | if current_indent_len == target_indent_len { |
118 | return None; | 159 | return None; |
119 | } | 160 | } |
120 | let mut edit = TextEditBuilder::default(); | 161 | |
121 | edit.replace( | 162 | Some(SingleFileChange { |
122 | TextRange::from_to(position.offset - current_indent_len, position.offset), | 163 | label: "reindent dot".to_string(), |
123 | target_indent, | 164 | edit: TextEdit::replace( |
124 | ); | 165 | TextRange::from_to(offset - current_indent_len, offset), |
125 | 166 | target_indent, | |
126 | let res = SourceChange::source_file_edit_from("reindent dot", position.file_id, edit.finish()) | 167 | ), |
127 | .with_cursor(FilePosition { | 168 | cursor_position: Some( |
128 | offset: position.offset + target_indent_len - current_indent_len | 169 | offset + target_indent_len - current_indent_len + TextUnit::of_char('.'), |
129 | + TextUnit::of_char('.'), | 170 | ), |
130 | file_id: position.file_id, | 171 | }) |
131 | }); | 172 | } |
132 | 173 | ||
133 | Some(res) | 174 | /// Adds a space after an arrow when `fn foo() { ... }` is turned into `fn foo() -> { ... }` |
175 | fn on_arrow_typed(file: &SourceFile, offset: TextUnit) -> Option<SingleFileChange> { | ||
176 | let file_text = file.syntax().text(); | ||
177 | assert_eq!(file_text.char_at(offset), Some('>')); | ||
178 | let after_arrow = offset + TextUnit::of_char('>'); | ||
179 | if file_text.char_at(after_arrow) != Some('{') { | ||
180 | return None; | ||
181 | } | ||
182 | if find_node_at_offset::<ast::RetType>(file.syntax(), offset).is_none() { | ||
183 | return None; | ||
184 | } | ||
185 | |||
186 | Some(SingleFileChange { | ||
187 | label: "add space after return type".to_string(), | ||
188 | edit: TextEdit::insert(after_arrow, " ".to_string()), | ||
189 | cursor_position: Some(after_arrow), | ||
190 | }) | ||
134 | } | 191 | } |
135 | 192 | ||
136 | #[cfg(test)] | 193 | #[cfg(test)] |
@@ -142,21 +199,87 @@ mod tests { | |||
142 | use super::*; | 199 | use super::*; |
143 | 200 | ||
144 | #[test] | 201 | #[test] |
145 | fn test_on_eq_typed() { | 202 | fn test_on_enter() { |
146 | fn type_eq(before: &str, after: &str) { | 203 | fn apply_on_enter(before: &str) -> Option<String> { |
147 | let (offset, before) = extract_offset(before); | 204 | let (offset, before) = extract_offset(before); |
148 | let mut edit = TextEditBuilder::default(); | 205 | let (analysis, file_id) = single_file(&before); |
149 | edit.insert(offset, "=".to_string()); | 206 | let result = analysis.on_enter(FilePosition { offset, file_id }).unwrap()?; |
150 | let before = edit.finish().apply(&before); | 207 | |
151 | let parse = SourceFile::parse(&before); | 208 | assert_eq!(result.source_file_edits.len(), 1); |
152 | if let Some(result) = on_eq_typed(&parse.tree(), offset) { | 209 | let actual = result.source_file_edits[0].edit.apply(&before); |
153 | let actual = result.apply(&before); | 210 | let actual = add_cursor(&actual, result.cursor_position.unwrap().offset); |
154 | assert_eq_text!(after, &actual); | 211 | Some(actual) |
155 | } else { | 212 | } |
156 | assert_eq_text!(&before, after) | 213 | |
157 | }; | 214 | fn do_check(before: &str, after: &str) { |
215 | let actual = apply_on_enter(before).unwrap(); | ||
216 | assert_eq_text!(after, &actual); | ||
217 | } | ||
218 | |||
219 | fn do_check_noop(text: &str) { | ||
220 | assert!(apply_on_enter(text).is_none()) | ||
221 | } | ||
222 | |||
223 | do_check( | ||
224 | r" | ||
225 | /// Some docs<|> | ||
226 | fn foo() { | ||
227 | } | ||
228 | ", | ||
229 | r" | ||
230 | /// Some docs | ||
231 | /// <|> | ||
232 | fn foo() { | ||
233 | } | ||
234 | ", | ||
235 | ); | ||
236 | do_check( | ||
237 | r" | ||
238 | impl S { | ||
239 | /// Some<|> docs. | ||
240 | fn foo() {} | ||
241 | } | ||
242 | ", | ||
243 | r" | ||
244 | impl S { | ||
245 | /// Some | ||
246 | /// <|> docs. | ||
247 | fn foo() {} | ||
248 | } | ||
249 | ", | ||
250 | ); | ||
251 | do_check_noop(r"<|>//! docz"); | ||
252 | } | ||
253 | |||
254 | fn do_type_char(char_typed: char, before: &str) -> Option<(String, SingleFileChange)> { | ||
255 | let (offset, before) = extract_offset(before); | ||
256 | let edit = TextEdit::insert(offset, char_typed.to_string()); | ||
257 | let before = edit.apply(&before); | ||
258 | let parse = SourceFile::parse(&before); | ||
259 | on_char_typed_inner(&parse.tree(), offset, char_typed) | ||
260 | .map(|it| (it.edit.apply(&before), it)) | ||
261 | } | ||
262 | |||
263 | fn type_char(char_typed: char, before: &str, after: &str) { | ||
264 | let (actual, file_change) = do_type_char(char_typed, before) | ||
265 | .expect(&format!("typing `{}` did nothing", char_typed)); | ||
266 | |||
267 | if after.contains("<|>") { | ||
268 | let (offset, after) = extract_offset(after); | ||
269 | assert_eq_text!(&after, &actual); | ||
270 | assert_eq!(file_change.cursor_position, Some(offset)) | ||
271 | } else { | ||
272 | assert_eq_text!(after, &actual); | ||
158 | } | 273 | } |
274 | } | ||
159 | 275 | ||
276 | fn type_char_noop(char_typed: char, before: &str) { | ||
277 | let file_change = do_type_char(char_typed, before); | ||
278 | assert!(file_change.is_none()) | ||
279 | } | ||
280 | |||
281 | #[test] | ||
282 | fn test_on_eq_typed() { | ||
160 | // do_check(r" | 283 | // do_check(r" |
161 | // fn foo() { | 284 | // fn foo() { |
162 | // let foo =<|> | 285 | // let foo =<|> |
@@ -166,7 +289,8 @@ mod tests { | |||
166 | // let foo =; | 289 | // let foo =; |
167 | // } | 290 | // } |
168 | // "); | 291 | // "); |
169 | type_eq( | 292 | type_char( |
293 | '=', | ||
170 | r" | 294 | r" |
171 | fn foo() { | 295 | fn foo() { |
172 | let foo <|> 1 + 1 | 296 | let foo <|> 1 + 1 |
@@ -191,24 +315,10 @@ fn foo() { | |||
191 | // "); | 315 | // "); |
192 | } | 316 | } |
193 | 317 | ||
194 | fn type_dot(before: &str, after: &str) { | ||
195 | let (offset, before) = extract_offset(before); | ||
196 | let mut edit = TextEditBuilder::default(); | ||
197 | edit.insert(offset, ".".to_string()); | ||
198 | let before = edit.finish().apply(&before); | ||
199 | let (analysis, file_id) = single_file(&before); | ||
200 | if let Some(result) = analysis.on_dot_typed(FilePosition { offset, file_id }).unwrap() { | ||
201 | assert_eq!(result.source_file_edits.len(), 1); | ||
202 | let actual = result.source_file_edits[0].edit.apply(&before); | ||
203 | assert_eq_text!(after, &actual); | ||
204 | } else { | ||
205 | assert_eq_text!(&before, after) | ||
206 | }; | ||
207 | } | ||
208 | |||
209 | #[test] | 318 | #[test] |
210 | fn indents_new_chain_call() { | 319 | fn indents_new_chain_call() { |
211 | type_dot( | 320 | type_char( |
321 | '.', | ||
212 | r" | 322 | r" |
213 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | 323 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { |
214 | self.child_impl(db, name) | 324 | self.child_impl(db, name) |
@@ -222,25 +332,21 @@ fn foo() { | |||
222 | } | 332 | } |
223 | ", | 333 | ", |
224 | ); | 334 | ); |
225 | type_dot( | 335 | type_char_noop( |
336 | '.', | ||
226 | r" | 337 | r" |
227 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | 338 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { |
228 | self.child_impl(db, name) | 339 | self.child_impl(db, name) |
229 | <|> | 340 | <|> |
230 | } | 341 | } |
231 | ", | 342 | ", |
232 | r" | ||
233 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | ||
234 | self.child_impl(db, name) | ||
235 | . | ||
236 | } | ||
237 | ", | ||
238 | ) | 343 | ) |
239 | } | 344 | } |
240 | 345 | ||
241 | #[test] | 346 | #[test] |
242 | fn indents_new_chain_call_with_semi() { | 347 | fn indents_new_chain_call_with_semi() { |
243 | type_dot( | 348 | type_char( |
349 | '.', | ||
244 | r" | 350 | r" |
245 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | 351 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { |
246 | self.child_impl(db, name) | 352 | self.child_impl(db, name) |
@@ -254,25 +360,21 @@ fn foo() { | |||
254 | } | 360 | } |
255 | ", | 361 | ", |
256 | ); | 362 | ); |
257 | type_dot( | 363 | type_char_noop( |
364 | '.', | ||
258 | r" | 365 | r" |
259 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | 366 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { |
260 | self.child_impl(db, name) | 367 | self.child_impl(db, name) |
261 | <|>; | 368 | <|>; |
262 | } | 369 | } |
263 | ", | 370 | ", |
264 | r" | ||
265 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | ||
266 | self.child_impl(db, name) | ||
267 | .; | ||
268 | } | ||
269 | ", | ||
270 | ) | 371 | ) |
271 | } | 372 | } |
272 | 373 | ||
273 | #[test] | 374 | #[test] |
274 | fn indents_continued_chain_call() { | 375 | fn indents_continued_chain_call() { |
275 | type_dot( | 376 | type_char( |
377 | '.', | ||
276 | r" | 378 | r" |
277 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | 379 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { |
278 | self.child_impl(db, name) | 380 | self.child_impl(db, name) |
@@ -288,7 +390,8 @@ fn foo() { | |||
288 | } | 390 | } |
289 | ", | 391 | ", |
290 | ); | 392 | ); |
291 | type_dot( | 393 | type_char_noop( |
394 | '.', | ||
292 | r" | 395 | r" |
293 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | 396 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { |
294 | self.child_impl(db, name) | 397 | self.child_impl(db, name) |
@@ -296,19 +399,13 @@ fn foo() { | |||
296 | <|> | 399 | <|> |
297 | } | 400 | } |
298 | ", | 401 | ", |
299 | r" | ||
300 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | ||
301 | self.child_impl(db, name) | ||
302 | .first() | ||
303 | . | ||
304 | } | ||
305 | ", | ||
306 | ); | 402 | ); |
307 | } | 403 | } |
308 | 404 | ||
309 | #[test] | 405 | #[test] |
310 | fn indents_middle_of_chain_call() { | 406 | fn indents_middle_of_chain_call() { |
311 | type_dot( | 407 | type_char( |
408 | '.', | ||
312 | r" | 409 | r" |
313 | fn source_impl() { | 410 | fn source_impl() { |
314 | let var = enum_defvariant_list().unwrap() | 411 | let var = enum_defvariant_list().unwrap() |
@@ -326,7 +423,8 @@ fn foo() { | |||
326 | } | 423 | } |
327 | ", | 424 | ", |
328 | ); | 425 | ); |
329 | type_dot( | 426 | type_char_noop( |
427 | '.', | ||
330 | r" | 428 | r" |
331 | fn source_impl() { | 429 | fn source_impl() { |
332 | let var = enum_defvariant_list().unwrap() | 430 | let var = enum_defvariant_list().unwrap() |
@@ -335,95 +433,31 @@ fn foo() { | |||
335 | .unwrap(); | 433 | .unwrap(); |
336 | } | 434 | } |
337 | ", | 435 | ", |
338 | r" | ||
339 | fn source_impl() { | ||
340 | let var = enum_defvariant_list().unwrap() | ||
341 | . | ||
342 | .nth(92) | ||
343 | .unwrap(); | ||
344 | } | ||
345 | ", | ||
346 | ); | 436 | ); |
347 | } | 437 | } |
348 | 438 | ||
349 | #[test] | 439 | #[test] |
350 | fn dont_indent_freestanding_dot() { | 440 | fn dont_indent_freestanding_dot() { |
351 | type_dot( | 441 | type_char_noop( |
442 | '.', | ||
352 | r" | 443 | r" |
353 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | 444 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { |
354 | <|> | 445 | <|> |
355 | } | 446 | } |
356 | ", | 447 | ", |
357 | r" | ||
358 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | ||
359 | . | ||
360 | } | ||
361 | ", | ||
362 | ); | 448 | ); |
363 | type_dot( | 449 | type_char_noop( |
450 | '.', | ||
364 | r" | 451 | r" |
365 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | 452 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { |
366 | <|> | 453 | <|> |
367 | } | 454 | } |
368 | ", | 455 | ", |
369 | r" | ||
370 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | ||
371 | . | ||
372 | } | ||
373 | ", | ||
374 | ); | 456 | ); |
375 | } | 457 | } |
376 | 458 | ||
377 | #[test] | 459 | #[test] |
378 | fn test_on_enter() { | 460 | fn adds_space_after_return_type() { |
379 | fn apply_on_enter(before: &str) -> Option<String> { | 461 | type_char('>', "fn foo() -<|>{ 92 }", "fn foo() -><|> { 92 }") |
380 | let (offset, before) = extract_offset(before); | ||
381 | let (analysis, file_id) = single_file(&before); | ||
382 | let result = analysis.on_enter(FilePosition { offset, file_id }).unwrap()?; | ||
383 | |||
384 | assert_eq!(result.source_file_edits.len(), 1); | ||
385 | let actual = result.source_file_edits[0].edit.apply(&before); | ||
386 | let actual = add_cursor(&actual, result.cursor_position.unwrap().offset); | ||
387 | Some(actual) | ||
388 | } | ||
389 | |||
390 | fn do_check(before: &str, after: &str) { | ||
391 | let actual = apply_on_enter(before).unwrap(); | ||
392 | assert_eq_text!(after, &actual); | ||
393 | } | ||
394 | |||
395 | fn do_check_noop(text: &str) { | ||
396 | assert!(apply_on_enter(text).is_none()) | ||
397 | } | ||
398 | |||
399 | do_check( | ||
400 | r" | ||
401 | /// Some docs<|> | ||
402 | fn foo() { | ||
403 | } | ||
404 | ", | ||
405 | r" | ||
406 | /// Some docs | ||
407 | /// <|> | ||
408 | fn foo() { | ||
409 | } | ||
410 | ", | ||
411 | ); | ||
412 | do_check( | ||
413 | r" | ||
414 | impl S { | ||
415 | /// Some<|> docs. | ||
416 | fn foo() {} | ||
417 | } | ||
418 | ", | ||
419 | r" | ||
420 | impl S { | ||
421 | /// Some | ||
422 | /// <|> docs. | ||
423 | fn foo() {} | ||
424 | } | ||
425 | ", | ||
426 | ); | ||
427 | do_check_noop(r"<|>//! docz"); | ||
428 | } | 462 | } |
429 | } | 463 | } |
diff --git a/crates/ra_lsp_server/src/caps.rs b/crates/ra_lsp_server/src/caps.rs index 30bcbd7a8..eea0965ed 100644 --- a/crates/ra_lsp_server/src/caps.rs +++ b/crates/ra_lsp_server/src/caps.rs | |||
@@ -38,7 +38,7 @@ pub fn server_capabilities() -> ServerCapabilities { | |||
38 | document_range_formatting_provider: None, | 38 | document_range_formatting_provider: None, |
39 | document_on_type_formatting_provider: Some(DocumentOnTypeFormattingOptions { | 39 | document_on_type_formatting_provider: Some(DocumentOnTypeFormattingOptions { |
40 | first_trigger_character: "=".to_string(), | 40 | first_trigger_character: "=".to_string(), |
41 | more_trigger_character: Some(vec![".".to_string()]), | 41 | more_trigger_character: Some(vec![".".to_string(), ">".to_string()]), |
42 | }), | 42 | }), |
43 | selection_range_provider: Some(GenericCapability::default()), | 43 | selection_range_provider: Some(GenericCapability::default()), |
44 | folding_range_provider: Some(FoldingRangeProviderCapability::Simple(true)), | 44 | folding_range_provider: Some(FoldingRangeProviderCapability::Simple(true)), |
diff --git a/crates/ra_lsp_server/src/main_loop/handlers.rs b/crates/ra_lsp_server/src/main_loop/handlers.rs index a29971d10..16fb07266 100644 --- a/crates/ra_lsp_server/src/main_loop/handlers.rs +++ b/crates/ra_lsp_server/src/main_loop/handlers.rs | |||
@@ -132,6 +132,7 @@ pub fn handle_on_enter( | |||
132 | } | 132 | } |
133 | } | 133 | } |
134 | 134 | ||
135 | // Don't forget to add new trigger characters to `ServerCapabilities` in `caps.rs`. | ||
135 | pub fn handle_on_type_formatting( | 136 | pub fn handle_on_type_formatting( |
136 | world: WorldSnapshot, | 137 | world: WorldSnapshot, |
137 | params: req::DocumentOnTypeFormattingParams, | 138 | params: req::DocumentOnTypeFormattingParams, |
@@ -144,12 +145,17 @@ pub fn handle_on_type_formatting( | |||
144 | // in `ra_ide_api`, the `on_type` invariant is that | 145 | // in `ra_ide_api`, the `on_type` invariant is that |
145 | // `text.char_at(position) == typed_char`. | 146 | // `text.char_at(position) == typed_char`. |
146 | position.offset = position.offset - TextUnit::of_char('.'); | 147 | position.offset = position.offset - TextUnit::of_char('.'); |
148 | let char_typed = params.ch.chars().next().unwrap_or('\0'); | ||
147 | 149 | ||
148 | let edit = match params.ch.as_str() { | 150 | // We have an assist that inserts ` ` after typing `->` in `fn foo() ->{`, |
149 | "=" => world.analysis().on_eq_typed(position), | 151 | // but it requires precise cursor positioning to work, and one can't |
150 | "." => world.analysis().on_dot_typed(position), | 152 | // position the cursor with on_type formatting. So, let's just toggle this |
151 | _ => return Ok(None), | 153 | // feature off here, hoping that we'll enable it one day, 😿. |
152 | }?; | 154 | if char_typed == '>' { |
155 | return Ok(None); | ||
156 | } | ||
157 | |||
158 | let edit = world.analysis().on_char_typed(position, char_typed)?; | ||
153 | let mut edit = match edit { | 159 | let mut edit = match edit { |
154 | Some(it) => it, | 160 | Some(it) => it, |
155 | None => return Ok(None), | 161 | None => return Ok(None), |
diff --git a/crates/ra_text_edit/src/text_edit.rs b/crates/ra_text_edit/src/text_edit.rs index 0381ea000..413c7d782 100644 --- a/crates/ra_text_edit/src/text_edit.rs +++ b/crates/ra_text_edit/src/text_edit.rs | |||
@@ -32,6 +32,24 @@ impl TextEditBuilder { | |||
32 | } | 32 | } |
33 | 33 | ||
34 | impl TextEdit { | 34 | impl TextEdit { |
35 | pub fn insert(offset: TextUnit, text: String) -> TextEdit { | ||
36 | let mut builder = TextEditBuilder::default(); | ||
37 | builder.insert(offset, text); | ||
38 | builder.finish() | ||
39 | } | ||
40 | |||
41 | pub fn delete(range: TextRange) -> TextEdit { | ||
42 | let mut builder = TextEditBuilder::default(); | ||
43 | builder.delete(range); | ||
44 | builder.finish() | ||
45 | } | ||
46 | |||
47 | pub fn replace(range: TextRange, replace_with: String) -> TextEdit { | ||
48 | let mut builder = TextEditBuilder::default(); | ||
49 | builder.replace(range, replace_with); | ||
50 | builder.finish() | ||
51 | } | ||
52 | |||
35 | pub(crate) fn from_atoms(mut atoms: Vec<AtomTextEdit>) -> TextEdit { | 53 | pub(crate) fn from_atoms(mut atoms: Vec<AtomTextEdit>) -> TextEdit { |
36 | atoms.sort_by_key(|a| (a.delete.start(), a.delete.end())); | 54 | atoms.sort_by_key(|a| (a.delete.start(), a.delete.end())); |
37 | for (a1, a2) in atoms.iter().zip(atoms.iter().skip(1)) { | 55 | for (a1, a2) in atoms.iter().zip(atoms.iter().skip(1)) { |