aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--crates/ra_ide_api/src/lib.rs129
-rw-r--r--crates/ra_ide_api/src/source_change.rs119
-rw-r--r--crates/ra_ide_api/src/typing.rs352
-rw-r--r--crates/ra_lsp_server/src/caps.rs2
-rw-r--r--crates/ra_lsp_server/src/main_loop/handlers.rs16
-rw-r--r--crates/ra_text_edit/src/text_edit.rs18
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;
14pub mod mock_analysis; 14pub mod mock_analysis;
15mod symbol_index; 15mod symbol_index;
16mod change; 16mod change;
17mod source_change;
17mod feature_flags; 18mod feature_flags;
18 19
19mod status; 20mod status;
@@ -54,8 +55,6 @@ use ra_db::{
54 CheckCanceled, FileLoader, SourceDatabase, 55 CheckCanceled, FileLoader, SourceDatabase,
55}; 56};
56use ra_syntax::{SourceFile, TextRange, TextUnit}; 57use ra_syntax::{SourceFile, TextRange, TextUnit};
57use ra_text_edit::TextEdit;
58use relative_path::RelativePathBuf;
59 58
60use crate::{db::LineIndexDatabase, symbol_index::FileSymbol}; 59use 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::{
84pub type Cancelable<T> = Result<T, Canceled>; 84pub type Cancelable<T> = Result<T, Canceled>;
85 85
86#[derive(Debug)] 86#[derive(Debug)]
87pub 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
94impl 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)]
168pub struct SourceFileEdit {
169 pub file_id: FileId,
170 pub edit: TextEdit,
171}
172
173#[derive(Debug)]
174pub 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)]
180pub struct Diagnostic { 87pub 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
6use ra_text_edit::TextEdit;
7use relative_path::RelativePathBuf;
8
9use crate::{FileId, FilePosition, SourceRootId, TextUnit};
10
11#[derive(Debug)]
12pub 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
19impl 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)]
93pub struct SourceFileEdit {
94 pub file_id: FileId,
95 pub edit: TextEdit,
96}
97
98#[derive(Debug)]
99pub enum FileSystemEdit {
100 CreateFile { source_root: SourceRootId, path: RelativePathBuf },
101 MoveFile { src: FileId, dst_source_root: SourceRootId, dst_path: RelativePathBuf },
102}
103
104pub(crate) struct SingleFileChange {
105 pub label: String,
106 pub edit: TextEdit,
107 pub cursor_position: Option<TextUnit>,
108}
109
110impl 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
3use ra_db::{FilePosition, SourceDatabase}; 16use ra_db::{FilePosition, SourceDatabase};
4use ra_fmt::leading_indent; 17use ra_fmt::leading_indent;
@@ -11,7 +24,7 @@ use ra_syntax::{
11}; 24};
12use ra_text_edit::{TextEdit, TextEditBuilder}; 25use ra_text_edit::{TextEdit, TextEditBuilder};
13 26
14use crate::{db::RootDatabase, SourceChange, SourceFileEdit}; 27use crate::{db::RootDatabase, source_change::SingleFileChange, SourceChange, SourceFileEdit};
15 28
16pub(crate) fn on_enter(db: &RootDatabase, position: FilePosition) -> Option<SourceChange> { 29pub(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
71pub fn on_eq_typed(file: &SourceFile, eq_offset: TextUnit) -> Option<TextEdit> { 84pub(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)?; 86pub(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
98fn 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.
115fn 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
94pub(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); 141fn 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() -> { ... }`
175fn 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<|>
226fn foo() {
227}
228",
229 r"
230/// Some docs
231/// <|>
232fn foo() {
233}
234",
235 );
236 do_check(
237 r"
238impl S {
239 /// Some<|> docs.
240 fn foo() {}
241}
242",
243 r"
244impl 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"
171fn foo() { 295fn 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<|>
402fn foo() {
403}
404",
405 r"
406/// Some docs
407/// <|>
408fn foo() {
409}
410",
411 );
412 do_check(
413 r"
414impl S {
415 /// Some<|> docs.
416 fn foo() {}
417}
418",
419 r"
420impl 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`.
135pub fn handle_on_type_formatting( 136pub 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
34impl TextEdit { 34impl 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)) {