diff options
-rw-r--r-- | crates/ra_ide_api/src/typing.rs | 228 | ||||
-rw-r--r-- | crates/ra_lsp_server/src/caps.rs | 2 | ||||
-rw-r--r-- | crates/ra_lsp_server/src/main_loop/handlers.rs | 1 |
3 files changed, 119 insertions, 112 deletions
diff --git a/crates/ra_ide_api/src/typing.rs b/crates/ra_ide_api/src/typing.rs index 17d0f08a5..26a3111fd 100644 --- a/crates/ra_ide_api/src/typing.rs +++ b/crates/ra_ide_api/src/typing.rs | |||
@@ -81,7 +81,7 @@ fn node_indent(file: &SourceFile, token: &SyntaxToken) -> Option<SmolStr> { | |||
81 | Some(text[pos..].into()) | 81 | Some(text[pos..].into()) |
82 | } | 82 | } |
83 | 83 | ||
84 | pub(crate) const TRIGGER_CHARS: &str = ".="; | 84 | pub(crate) const TRIGGER_CHARS: &str = ".=>"; |
85 | 85 | ||
86 | pub(crate) fn on_char_typed( | 86 | pub(crate) fn on_char_typed( |
87 | db: &RootDatabase, | 87 | db: &RootDatabase, |
@@ -100,10 +100,12 @@ fn on_char_typed_inner( | |||
100 | offset: TextUnit, | 100 | offset: TextUnit, |
101 | char_typed: char, | 101 | char_typed: char, |
102 | ) -> Option<SingleFileChange> { | 102 | ) -> Option<SingleFileChange> { |
103 | assert!(TRIGGER_CHARS.contains(char_typed)); | ||
103 | match char_typed { | 104 | match char_typed { |
104 | '.' => on_dot_typed(file, offset), | 105 | '.' => on_dot_typed(file, offset), |
105 | '=' => on_eq_typed(file, offset), | 106 | '=' => on_eq_typed(file, offset), |
106 | _ => None, | 107 | '>' => on_arrow_typed(file, offset), |
108 | _ => unreachable!(), | ||
107 | } | 109 | } |
108 | } | 110 | } |
109 | 111 | ||
@@ -169,6 +171,25 @@ fn on_dot_typed(file: &SourceFile, offset: TextUnit) -> Option<SingleFileChange> | |||
169 | }) | 171 | }) |
170 | } | 172 | } |
171 | 173 | ||
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 | }) | ||
191 | } | ||
192 | |||
172 | #[cfg(test)] | 193 | #[cfg(test)] |
173 | mod tests { | 194 | mod tests { |
174 | use test_utils::{add_cursor, assert_eq_text, extract_offset}; | 195 | use test_utils::{add_cursor, assert_eq_text, extract_offset}; |
@@ -177,25 +198,84 @@ mod tests { | |||
177 | 198 | ||
178 | use super::*; | 199 | use super::*; |
179 | 200 | ||
180 | fn type_char(char_typed: char, before: &str, after: &str) { | 201 | #[test] |
202 | fn test_on_enter() { | ||
203 | fn apply_on_enter(before: &str) -> Option<String> { | ||
204 | let (offset, before) = extract_offset(before); | ||
205 | let (analysis, file_id) = single_file(&before); | ||
206 | let result = analysis.on_enter(FilePosition { offset, file_id }).unwrap()?; | ||
207 | |||
208 | assert_eq!(result.source_file_edits.len(), 1); | ||
209 | let actual = result.source_file_edits[0].edit.apply(&before); | ||
210 | let actual = add_cursor(&actual, result.cursor_position.unwrap().offset); | ||
211 | Some(actual) | ||
212 | } | ||
213 | |||
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)> { | ||
181 | let (offset, before) = extract_offset(before); | 255 | let (offset, before) = extract_offset(before); |
182 | let edit = TextEdit::insert(offset, char_typed.to_string()); | 256 | let edit = TextEdit::insert(offset, char_typed.to_string()); |
183 | let before = edit.apply(&before); | 257 | let before = edit.apply(&before); |
184 | let parse = SourceFile::parse(&before); | 258 | let parse = SourceFile::parse(&before); |
185 | if let Some(result) = on_char_typed_inner(&parse.tree(), offset, char_typed) { | 259 | on_char_typed_inner(&parse.tree(), offset, char_typed) |
186 | let actual = result.edit.apply(&before); | 260 | .map(|it| (it.edit.apply(&before), it)) |
187 | assert_eq_text!(after, &actual); | ||
188 | } else { | ||
189 | assert_eq_text!(&before, after) | ||
190 | }; | ||
191 | } | 261 | } |
192 | 262 | ||
193 | fn type_eq(before: &str, after: &str) { | 263 | fn type_char(char_typed: char, before: &str, after: &str) { |
194 | type_char('=', before, after); | 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); | ||
273 | } | ||
195 | } | 274 | } |
196 | 275 | ||
197 | fn type_dot(before: &str, after: &str) { | 276 | fn type_char_noop(char_typed: char, before: &str) { |
198 | type_char('.', before, after); | 277 | let file_change = do_type_char(char_typed, before); |
278 | assert!(file_change.is_none()) | ||
199 | } | 279 | } |
200 | 280 | ||
201 | #[test] | 281 | #[test] |
@@ -209,7 +289,8 @@ mod tests { | |||
209 | // let foo =; | 289 | // let foo =; |
210 | // } | 290 | // } |
211 | // "); | 291 | // "); |
212 | type_eq( | 292 | type_char( |
293 | '=', | ||
213 | r" | 294 | r" |
214 | fn foo() { | 295 | fn foo() { |
215 | let foo <|> 1 + 1 | 296 | let foo <|> 1 + 1 |
@@ -236,7 +317,8 @@ fn foo() { | |||
236 | 317 | ||
237 | #[test] | 318 | #[test] |
238 | fn indents_new_chain_call() { | 319 | fn indents_new_chain_call() { |
239 | type_dot( | 320 | type_char( |
321 | '.', | ||
240 | r" | 322 | r" |
241 | 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>> { |
242 | self.child_impl(db, name) | 324 | self.child_impl(db, name) |
@@ -250,25 +332,21 @@ fn foo() { | |||
250 | } | 332 | } |
251 | ", | 333 | ", |
252 | ); | 334 | ); |
253 | type_dot( | 335 | type_char_noop( |
336 | '.', | ||
254 | r" | 337 | r" |
255 | 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>> { |
256 | self.child_impl(db, name) | 339 | self.child_impl(db, name) |
257 | <|> | 340 | <|> |
258 | } | 341 | } |
259 | ", | 342 | ", |
260 | r" | ||
261 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | ||
262 | self.child_impl(db, name) | ||
263 | . | ||
264 | } | ||
265 | ", | ||
266 | ) | 343 | ) |
267 | } | 344 | } |
268 | 345 | ||
269 | #[test] | 346 | #[test] |
270 | fn indents_new_chain_call_with_semi() { | 347 | fn indents_new_chain_call_with_semi() { |
271 | type_dot( | 348 | type_char( |
349 | '.', | ||
272 | r" | 350 | r" |
273 | 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>> { |
274 | self.child_impl(db, name) | 352 | self.child_impl(db, name) |
@@ -282,25 +360,21 @@ fn foo() { | |||
282 | } | 360 | } |
283 | ", | 361 | ", |
284 | ); | 362 | ); |
285 | type_dot( | 363 | type_char_noop( |
364 | '.', | ||
286 | r" | 365 | r" |
287 | 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>> { |
288 | self.child_impl(db, name) | 367 | self.child_impl(db, name) |
289 | <|>; | 368 | <|>; |
290 | } | 369 | } |
291 | ", | 370 | ", |
292 | r" | ||
293 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | ||
294 | self.child_impl(db, name) | ||
295 | .; | ||
296 | } | ||
297 | ", | ||
298 | ) | 371 | ) |
299 | } | 372 | } |
300 | 373 | ||
301 | #[test] | 374 | #[test] |
302 | fn indents_continued_chain_call() { | 375 | fn indents_continued_chain_call() { |
303 | type_dot( | 376 | type_char( |
377 | '.', | ||
304 | r" | 378 | r" |
305 | 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>> { |
306 | self.child_impl(db, name) | 380 | self.child_impl(db, name) |
@@ -316,7 +390,8 @@ fn foo() { | |||
316 | } | 390 | } |
317 | ", | 391 | ", |
318 | ); | 392 | ); |
319 | type_dot( | 393 | type_char_noop( |
394 | '.', | ||
320 | r" | 395 | r" |
321 | 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>> { |
322 | self.child_impl(db, name) | 397 | self.child_impl(db, name) |
@@ -324,19 +399,13 @@ fn foo() { | |||
324 | <|> | 399 | <|> |
325 | } | 400 | } |
326 | ", | 401 | ", |
327 | r" | ||
328 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | ||
329 | self.child_impl(db, name) | ||
330 | .first() | ||
331 | . | ||
332 | } | ||
333 | ", | ||
334 | ); | 402 | ); |
335 | } | 403 | } |
336 | 404 | ||
337 | #[test] | 405 | #[test] |
338 | fn indents_middle_of_chain_call() { | 406 | fn indents_middle_of_chain_call() { |
339 | type_dot( | 407 | type_char( |
408 | '.', | ||
340 | r" | 409 | r" |
341 | fn source_impl() { | 410 | fn source_impl() { |
342 | let var = enum_defvariant_list().unwrap() | 411 | let var = enum_defvariant_list().unwrap() |
@@ -354,7 +423,8 @@ fn foo() { | |||
354 | } | 423 | } |
355 | ", | 424 | ", |
356 | ); | 425 | ); |
357 | type_dot( | 426 | type_char_noop( |
427 | '.', | ||
358 | r" | 428 | r" |
359 | fn source_impl() { | 429 | fn source_impl() { |
360 | let var = enum_defvariant_list().unwrap() | 430 | let var = enum_defvariant_list().unwrap() |
@@ -363,95 +433,31 @@ fn foo() { | |||
363 | .unwrap(); | 433 | .unwrap(); |
364 | } | 434 | } |
365 | ", | 435 | ", |
366 | r" | ||
367 | fn source_impl() { | ||
368 | let var = enum_defvariant_list().unwrap() | ||
369 | . | ||
370 | .nth(92) | ||
371 | .unwrap(); | ||
372 | } | ||
373 | ", | ||
374 | ); | 436 | ); |
375 | } | 437 | } |
376 | 438 | ||
377 | #[test] | 439 | #[test] |
378 | fn dont_indent_freestanding_dot() { | 440 | fn dont_indent_freestanding_dot() { |
379 | type_dot( | 441 | type_char_noop( |
442 | '.', | ||
380 | r" | 443 | r" |
381 | 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>> { |
382 | <|> | 445 | <|> |
383 | } | 446 | } |
384 | ", | 447 | ", |
385 | r" | ||
386 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | ||
387 | . | ||
388 | } | ||
389 | ", | ||
390 | ); | 448 | ); |
391 | type_dot( | 449 | type_char_noop( |
450 | '.', | ||
392 | r" | 451 | r" |
393 | 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>> { |
394 | <|> | 453 | <|> |
395 | } | 454 | } |
396 | ", | 455 | ", |
397 | r" | ||
398 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | ||
399 | . | ||
400 | } | ||
401 | ", | ||
402 | ); | 456 | ); |
403 | } | 457 | } |
404 | 458 | ||
405 | #[test] | 459 | #[test] |
406 | fn test_on_enter() { | 460 | fn adds_space_after_return_type() { |
407 | fn apply_on_enter(before: &str) -> Option<String> { | 461 | type_char('>', "fn foo() -<|>{ 92 }", "fn foo() -><|> { 92 }") |
408 | let (offset, before) = extract_offset(before); | ||
409 | let (analysis, file_id) = single_file(&before); | ||
410 | let result = analysis.on_enter(FilePosition { offset, file_id }).unwrap()?; | ||
411 | |||
412 | assert_eq!(result.source_file_edits.len(), 1); | ||
413 | let actual = result.source_file_edits[0].edit.apply(&before); | ||
414 | let actual = add_cursor(&actual, result.cursor_position.unwrap().offset); | ||
415 | Some(actual) | ||
416 | } | ||
417 | |||
418 | fn do_check(before: &str, after: &str) { | ||
419 | let actual = apply_on_enter(before).unwrap(); | ||
420 | assert_eq_text!(after, &actual); | ||
421 | } | ||
422 | |||
423 | fn do_check_noop(text: &str) { | ||
424 | assert!(apply_on_enter(text).is_none()) | ||
425 | } | ||
426 | |||
427 | do_check( | ||
428 | r" | ||
429 | /// Some docs<|> | ||
430 | fn foo() { | ||
431 | } | ||
432 | ", | ||
433 | r" | ||
434 | /// Some docs | ||
435 | /// <|> | ||
436 | fn foo() { | ||
437 | } | ||
438 | ", | ||
439 | ); | ||
440 | do_check( | ||
441 | r" | ||
442 | impl S { | ||
443 | /// Some<|> docs. | ||
444 | fn foo() {} | ||
445 | } | ||
446 | ", | ||
447 | r" | ||
448 | impl S { | ||
449 | /// Some | ||
450 | /// <|> docs. | ||
451 | fn foo() {} | ||
452 | } | ||
453 | ", | ||
454 | ); | ||
455 | do_check_noop(r"<|>//! docz"); | ||
456 | } | 462 | } |
457 | } | 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 530c4d8b6..6f1e59b4b 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, |