aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAleksey Kladov <[email protected]>2019-10-25 09:19:26 +0100
committerAleksey Kladov <[email protected]>2019-10-25 09:30:46 +0100
commit8d2fd59cfb00211573419b0a59cf91d92d636f5a (patch)
treefeafe88a932c70aa9e25a6b87323094e2b40b750
parent518f99e16b993e3414a81181c8bad7a89e590ece (diff)
make typing infra slightly more extensible
-rw-r--r--crates/ra_ide_api/src/lib.rs28
-rw-r--r--crates/ra_ide_api/src/typing.rs92
-rw-r--r--crates/ra_lsp_server/src/main_loop/handlers.rs8
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
3use ra_db::{FilePosition, SourceDatabase}; 16use ra_db::{FilePosition, SourceDatabase};
4use ra_fmt::leading_indent; 17use 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
71pub fn on_eq_typed(file: &SourceFile, eq_offset: TextUnit) -> Option<TextEdit> { 84pub(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.
116fn 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
94pub(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); 140fn 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),