diff options
author | Aleksey Kladov <[email protected]> | 2019-10-25 09:19:26 +0100 |
---|---|---|
committer | Aleksey Kladov <[email protected]> | 2019-10-25 09:30:46 +0100 |
commit | 8d2fd59cfb00211573419b0a59cf91d92d636f5a (patch) | |
tree | feafe88a932c70aa9e25a6b87323094e2b40b750 | |
parent | 518f99e16b993e3414a81181c8bad7a89e590ece (diff) |
make typing infra slightly more extensible
-rw-r--r-- | crates/ra_ide_api/src/lib.rs | 28 | ||||
-rw-r--r-- | crates/ra_ide_api/src/typing.rs | 92 | ||||
-rw-r--r-- | crates/ra_lsp_server/src/main_loop/handlers.rs | 8 |
3 files changed, 74 insertions, 54 deletions
diff --git a/crates/ra_ide_api/src/lib.rs b/crates/ra_ide_api/src/lib.rs index 0832229fd..b2a1d185b 100644 --- a/crates/ra_ide_api/src/lib.rs +++ b/crates/ra_ide_api/src/lib.rs | |||
@@ -407,24 +407,16 @@ impl Analysis { | |||
407 | self.with_db(|db| typing::on_enter(&db, position)) | 407 | self.with_db(|db| typing::on_enter(&db, position)) |
408 | } | 408 | } |
409 | 409 | ||
410 | /// Returns an edit which should be applied after `=` was typed. Primarily, | 410 | /// Returns an edit which should be applied after a character was typed. |
411 | /// this works when adding `let =`. | 411 | /// |
412 | // FIXME: use a snippet completion instead of this hack here. | 412 | /// 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>> { | 413 | /// automatically. |
414 | self.with_db(|db| { | 414 | pub fn on_char_typed( |
415 | let parse = db.parse(position.file_id); | 415 | &self, |
416 | let file = parse.tree(); | 416 | position: FilePosition, |
417 | let edit = typing::on_eq_typed(&file, position.offset)?; | 417 | char_typed: char, |
418 | Some(SourceChange::source_file_edit( | 418 | ) -> Cancelable<Option<SourceChange>> { |
419 | "add semicolon", | 419 | self.with_db(|db| typing::on_char_typed(&db, position, char_typed)) |
420 | SourceFileEdit { edit, file_id: position.file_id }, | ||
421 | )) | ||
422 | }) | ||
423 | } | ||
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 | } | 420 | } |
429 | 421 | ||
430 | /// Returns a tree representation of symbols in the file. Useful to draw a | 422 | /// Returns a tree representation of symbols in the file. Useful to draw a |
diff --git a/crates/ra_ide_api/src/typing.rs b/crates/ra_ide_api/src/typing.rs index 2f5782012..44cc46147 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; |
@@ -68,18 +81,50 @@ 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) fn on_char_typed( |
72 | assert_eq!(file.syntax().text().char_at(eq_offset), Some('=')); | 85 | db: &RootDatabase, |
73 | let let_stmt: ast::LetStmt = find_node_at_offset(file.syntax(), eq_offset)?; | 86 | position: FilePosition, |
87 | char_typed: char, | ||
88 | ) -> Option<SourceChange> { | ||
89 | let file = &db.parse(position.file_id).tree(); | ||
90 | assert_eq!(file.syntax().text().char_at(position.offset), Some(char_typed)); | ||
91 | match char_typed { | ||
92 | '=' => { | ||
93 | let edit = on_eq_typed(file, position.offset)?; | ||
94 | Some(SourceChange::source_file_edit( | ||
95 | "add semicolon", | ||
96 | SourceFileEdit { edit, file_id: position.file_id }, | ||
97 | )) | ||
98 | } | ||
99 | '.' => { | ||
100 | let (edit, cursor_offset) = on_dot_typed(file, position.offset)?; | ||
101 | Some( | ||
102 | SourceChange::source_file_edit( | ||
103 | "reindent dot", | ||
104 | SourceFileEdit { edit, file_id: position.file_id }, | ||
105 | ) | ||
106 | .with_cursor(FilePosition { file_id: position.file_id, offset: cursor_offset }), | ||
107 | ) | ||
108 | } | ||
109 | _ => None, | ||
110 | } | ||
111 | } | ||
112 | |||
113 | /// Returns an edit which should be applied after `=` was typed. Primarily, | ||
114 | /// this works when adding `let =`. | ||
115 | // FIXME: use a snippet completion instead of this hack here. | ||
116 | fn on_eq_typed(file: &SourceFile, offset: TextUnit) -> Option<TextEdit> { | ||
117 | assert_eq!(file.syntax().text().char_at(offset), Some('=')); | ||
118 | let let_stmt: ast::LetStmt = find_node_at_offset(file.syntax(), offset)?; | ||
74 | if let_stmt.has_semi() { | 119 | if let_stmt.has_semi() { |
75 | return None; | 120 | return None; |
76 | } | 121 | } |
77 | if let Some(expr) = let_stmt.initializer() { | 122 | if let Some(expr) = let_stmt.initializer() { |
78 | let expr_range = expr.syntax().text_range(); | 123 | let expr_range = expr.syntax().text_range(); |
79 | if expr_range.contains(eq_offset) && eq_offset != expr_range.start() { | 124 | if expr_range.contains(offset) && offset != expr_range.start() { |
80 | return None; | 125 | return None; |
81 | } | 126 | } |
82 | if file.syntax().text().slice(eq_offset..expr_range.start()).contains_char('\n') { | 127 | if file.syntax().text().slice(offset..expr_range.start()).contains_char('\n') { |
83 | return None; | 128 | return None; |
84 | } | 129 | } |
85 | } else { | 130 | } else { |
@@ -91,16 +136,11 @@ pub fn on_eq_typed(file: &SourceFile, eq_offset: TextUnit) -> Option<TextEdit> { | |||
91 | Some(edit.finish()) | 136 | Some(edit.finish()) |
92 | } | 137 | } |
93 | 138 | ||
94 | pub(crate) fn on_dot_typed(db: &RootDatabase, position: FilePosition) -> Option<SourceChange> { | 139 | /// 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); | 140 | fn on_dot_typed(file: &SourceFile, offset: TextUnit) -> Option<(TextEdit, TextUnit)> { |
96 | assert_eq!(parse.tree().syntax().text().char_at(position.offset), Some('.')); | 141 | assert_eq!(file.syntax().text().char_at(offset), Some('.')); |
97 | 142 | let whitespace = | |
98 | let whitespace = parse | 143 | 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 | 144 | ||
105 | let current_indent = { | 145 | let current_indent = { |
106 | let text = whitespace.text(); | 146 | let text = whitespace.text(); |
@@ -118,19 +158,11 @@ pub(crate) fn on_dot_typed(db: &RootDatabase, position: FilePosition) -> Option< | |||
118 | return None; | 158 | return None; |
119 | } | 159 | } |
120 | let mut edit = TextEditBuilder::default(); | 160 | let mut edit = TextEditBuilder::default(); |
121 | edit.replace( | 161 | edit.replace(TextRange::from_to(offset - current_indent_len, offset), target_indent); |
122 | TextRange::from_to(position.offset - current_indent_len, position.offset), | ||
123 | target_indent, | ||
124 | ); | ||
125 | 162 | ||
126 | let res = SourceChange::source_file_edit_from("reindent dot", position.file_id, edit.finish()) | 163 | let cursor_offset = offset + target_indent_len - current_indent_len + TextUnit::of_char('.'); |
127 | .with_cursor(FilePosition { | ||
128 | offset: position.offset + target_indent_len - current_indent_len | ||
129 | + TextUnit::of_char('.'), | ||
130 | file_id: position.file_id, | ||
131 | }); | ||
132 | 164 | ||
133 | Some(res) | 165 | Some((edit.finish(), cursor_offset)) |
134 | } | 166 | } |
135 | 167 | ||
136 | #[cfg(test)] | 168 | #[cfg(test)] |
@@ -197,9 +229,9 @@ fn foo() { | |||
197 | edit.insert(offset, ".".to_string()); | 229 | edit.insert(offset, ".".to_string()); |
198 | let before = edit.finish().apply(&before); | 230 | let before = edit.finish().apply(&before); |
199 | let (analysis, file_id) = single_file(&before); | 231 | let (analysis, file_id) = single_file(&before); |
200 | if let Some(result) = analysis.on_dot_typed(FilePosition { offset, file_id }).unwrap() { | 232 | let file = analysis.parse(file_id).unwrap(); |
201 | assert_eq!(result.source_file_edits.len(), 1); | 233 | if let Some((edit, _cursor_offset)) = on_dot_typed(&file, offset) { |
202 | let actual = result.source_file_edits[0].edit.apply(&before); | 234 | let actual = edit.apply(&before); |
203 | assert_eq_text!(after, &actual); | 235 | assert_eq_text!(after, &actual); |
204 | } else { | 236 | } else { |
205 | assert_eq_text!(&before, after) | 237 | assert_eq_text!(&before, after) |
diff --git a/crates/ra_lsp_server/src/main_loop/handlers.rs b/crates/ra_lsp_server/src/main_loop/handlers.rs index a29971d10..530c4d8b6 100644 --- a/crates/ra_lsp_server/src/main_loop/handlers.rs +++ b/crates/ra_lsp_server/src/main_loop/handlers.rs | |||
@@ -144,12 +144,8 @@ pub fn handle_on_type_formatting( | |||
144 | // in `ra_ide_api`, the `on_type` invariant is that | 144 | // in `ra_ide_api`, the `on_type` invariant is that |
145 | // `text.char_at(position) == typed_char`. | 145 | // `text.char_at(position) == typed_char`. |
146 | position.offset = position.offset - TextUnit::of_char('.'); | 146 | position.offset = position.offset - TextUnit::of_char('.'); |
147 | 147 | let char_typed = params.ch.chars().next().unwrap_or('\0'); | |
148 | let edit = match params.ch.as_str() { | 148 | let edit = world.analysis().on_char_typed(position, char_typed)?; |
149 | "=" => world.analysis().on_eq_typed(position), | ||
150 | "." => world.analysis().on_dot_typed(position), | ||
151 | _ => return Ok(None), | ||
152 | }?; | ||
153 | let mut edit = match edit { | 149 | let mut edit = match edit { |
154 | Some(it) => it, | 150 | Some(it) => it, |
155 | None => return Ok(None), | 151 | None => return Ok(None), |