diff --git a/crates/ra_ide_api/src/lib.rs b/crates/ra_ide_api/src/lib.rs
index 0832229fd1..b2a1d185b6 100644
--- a/crates/ra_ide_api/src/lib.rs
+++ b/crates/ra_ide_api/src/lib.rs
@@ -407,24 +407,16 @@ impl Analysis {
self.with_db(|db| typing::on_enter(&db, position))
}
- /// Returns an edit which should be applied after `=` was typed. Primarily,
- /// this works when adding `let =`.
- // FIXME: use a snippet completion instead of this hack here.
- pub fn on_eq_typed(&self, position: FilePosition) -> Cancelable> {
- self.with_db(|db| {
- let parse = db.parse(position.file_id);
- let file = parse.tree();
- let edit = typing::on_eq_typed(&file, position.offset)?;
- Some(SourceChange::source_file_edit(
- "add semicolon",
- SourceFileEdit { edit, file_id: position.file_id },
- ))
- })
- }
-
- /// Returns an edit which should be applied when a dot ('.') is typed on a blank line, indenting the line appropriately.
- pub fn on_dot_typed(&self, position: FilePosition) -> Cancelable > {
- self.with_db(|db| typing::on_dot_typed(&db, position))
+ /// Returns an edit which should be applied after a character was typed.
+ ///
+ /// This is useful for some on-the-fly fixups, like adding `;` to `let =`
+ /// automatically.
+ pub fn on_char_typed(
+ &self,
+ position: FilePosition,
+ char_typed: char,
+ ) -> Cancelable > {
+ self.with_db(|db| typing::on_char_typed(&db, position, char_typed))
}
/// 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 2f5782012b..44cc461471 100644
--- a/crates/ra_ide_api/src/typing.rs
+++ b/crates/ra_ide_api/src/typing.rs
@@ -1,4 +1,17 @@
-//! FIXME: write short doc here
+//! This module handles auto-magic editing actions applied together with users
+//! edits. For example, if the user typed
+//!
+//! ```text
+//! foo
+//! .bar()
+//! .baz()
+//! | // <- cursor is here
+//! ```
+//!
+//! and types `.` next, we want to indent the dot.
+//!
+//! Language server executes such typing assists synchronously. That is, they
+//! block user's typing and should be pretty fast for this reason!
use ra_db::{FilePosition, SourceDatabase};
use ra_fmt::leading_indent;
@@ -68,18 +81,50 @@ fn node_indent(file: &SourceFile, token: &SyntaxToken) -> Option {
Some(text[pos..].into())
}
-pub fn on_eq_typed(file: &SourceFile, eq_offset: TextUnit) -> Option {
- assert_eq!(file.syntax().text().char_at(eq_offset), Some('='));
- let let_stmt: ast::LetStmt = find_node_at_offset(file.syntax(), eq_offset)?;
+pub(crate) fn on_char_typed(
+ db: &RootDatabase,
+ position: FilePosition,
+ char_typed: char,
+) -> Option {
+ let file = &db.parse(position.file_id).tree();
+ assert_eq!(file.syntax().text().char_at(position.offset), Some(char_typed));
+ match char_typed {
+ '=' => {
+ let edit = on_eq_typed(file, position.offset)?;
+ Some(SourceChange::source_file_edit(
+ "add semicolon",
+ SourceFileEdit { edit, file_id: position.file_id },
+ ))
+ }
+ '.' => {
+ let (edit, cursor_offset) = on_dot_typed(file, position.offset)?;
+ Some(
+ SourceChange::source_file_edit(
+ "reindent dot",
+ SourceFileEdit { edit, file_id: position.file_id },
+ )
+ .with_cursor(FilePosition { file_id: position.file_id, offset: cursor_offset }),
+ )
+ }
+ _ => None,
+ }
+}
+
+/// Returns an edit which should be applied after `=` was typed. Primarily,
+/// this works when adding `let =`.
+// FIXME: use a snippet completion instead of this hack here.
+fn on_eq_typed(file: &SourceFile, offset: TextUnit) -> Option {
+ assert_eq!(file.syntax().text().char_at(offset), Some('='));
+ let let_stmt: ast::LetStmt = find_node_at_offset(file.syntax(), offset)?;
if let_stmt.has_semi() {
return None;
}
if let Some(expr) = let_stmt.initializer() {
let expr_range = expr.syntax().text_range();
- if expr_range.contains(eq_offset) && eq_offset != expr_range.start() {
+ if expr_range.contains(offset) && offset != expr_range.start() {
return None;
}
- if file.syntax().text().slice(eq_offset..expr_range.start()).contains_char('\n') {
+ if file.syntax().text().slice(offset..expr_range.start()).contains_char('\n') {
return None;
}
} else {
@@ -91,16 +136,11 @@ pub fn on_eq_typed(file: &SourceFile, eq_offset: TextUnit) -> Option {
Some(edit.finish())
}
-pub(crate) fn on_dot_typed(db: &RootDatabase, position: FilePosition) -> Option {
- let parse = db.parse(position.file_id);
- assert_eq!(parse.tree().syntax().text().char_at(position.offset), Some('.'));
-
- let whitespace = parse
- .tree()
- .syntax()
- .token_at_offset(position.offset)
- .left_biased()
- .and_then(ast::Whitespace::cast)?;
+/// Returns an edit which should be applied when a dot ('.') is typed on a blank line, indenting the line appropriately.
+fn on_dot_typed(file: &SourceFile, offset: TextUnit) -> Option<(TextEdit, TextUnit)> {
+ assert_eq!(file.syntax().text().char_at(offset), Some('.'));
+ let whitespace =
+ file.syntax().token_at_offset(offset).left_biased().and_then(ast::Whitespace::cast)?;
let current_indent = {
let text = whitespace.text();
@@ -118,19 +158,11 @@ pub(crate) fn on_dot_typed(db: &RootDatabase, position: FilePosition) -> Option<
return None;
}
let mut edit = TextEditBuilder::default();
- edit.replace(
- TextRange::from_to(position.offset - current_indent_len, position.offset),
- target_indent,
- );
+ edit.replace(TextRange::from_to(offset - current_indent_len, offset), target_indent);
- let res = SourceChange::source_file_edit_from("reindent dot", position.file_id, edit.finish())
- .with_cursor(FilePosition {
- offset: position.offset + target_indent_len - current_indent_len
- + TextUnit::of_char('.'),
- file_id: position.file_id,
- });
+ let cursor_offset = offset + target_indent_len - current_indent_len + TextUnit::of_char('.');
- Some(res)
+ Some((edit.finish(), cursor_offset))
}
#[cfg(test)]
@@ -197,9 +229,9 @@ fn foo() {
edit.insert(offset, ".".to_string());
let before = edit.finish().apply(&before);
let (analysis, file_id) = single_file(&before);
- if let Some(result) = analysis.on_dot_typed(FilePosition { offset, file_id }).unwrap() {
- assert_eq!(result.source_file_edits.len(), 1);
- let actual = result.source_file_edits[0].edit.apply(&before);
+ let file = analysis.parse(file_id).unwrap();
+ if let Some((edit, _cursor_offset)) = on_dot_typed(&file, offset) {
+ let actual = edit.apply(&before);
assert_eq_text!(after, &actual);
} else {
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 a29971d107..530c4d8b60 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(
// in `ra_ide_api`, the `on_type` invariant is that
// `text.char_at(position) == typed_char`.
position.offset = position.offset - TextUnit::of_char('.');
-
- let edit = match params.ch.as_str() {
- "=" => world.analysis().on_eq_typed(position),
- "." => world.analysis().on_dot_typed(position),
- _ => return Ok(None),
- }?;
+ let char_typed = params.ch.chars().next().unwrap_or('\0');
+ let edit = world.analysis().on_char_typed(position, char_typed)?;
let mut edit = match edit {
Some(it) => it,
None => return Ok(None),