make typing infra slightly more extensible

This commit is contained in:
Aleksey Kladov 2019-10-25 11:19:26 +03:00
parent 518f99e16b
commit 8d2fd59cfb
3 changed files with 74 additions and 54 deletions

View file

@ -407,24 +407,16 @@ impl Analysis {
self.with_db(|db| typing::on_enter(&db, position)) self.with_db(|db| typing::on_enter(&db, position))
} }
/// Returns an edit which should be applied after `=` was typed. Primarily, /// Returns an edit which should be applied after a character was typed.
/// this works when adding `let =`. ///
// FIXME: use a snippet completion instead of this hack here. /// This is useful for some on-the-fly fixups, like adding `;` to `let =`
pub fn on_eq_typed(&self, position: FilePosition) -> Cancelable<Option<SourceChange>> { /// automatically.
self.with_db(|db| { pub fn on_char_typed(
let parse = db.parse(position.file_id); &self,
let file = parse.tree(); position: FilePosition,
let edit = typing::on_eq_typed(&file, position.offset)?; char_typed: char,
Some(SourceChange::source_file_edit( ) -> Cancelable<Option<SourceChange>> {
"add semicolon", self.with_db(|db| typing::on_char_typed(&db, position, char_typed))
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<Option<SourceChange>> {
self.with_db(|db| typing::on_dot_typed(&db, position))
} }
/// Returns a tree representation of symbols in the file. Useful to draw a /// Returns a tree representation of symbols in the file. Useful to draw a

View file

@ -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_db::{FilePosition, SourceDatabase};
use ra_fmt::leading_indent; use ra_fmt::leading_indent;
@ -68,18 +81,50 @@ fn node_indent(file: &SourceFile, token: &SyntaxToken) -> Option<SmolStr> {
Some(text[pos..].into()) Some(text[pos..].into())
} }
pub fn on_eq_typed(file: &SourceFile, eq_offset: TextUnit) -> Option<TextEdit> { pub(crate) fn on_char_typed(
assert_eq!(file.syntax().text().char_at(eq_offset), Some('=')); db: &RootDatabase,
let let_stmt: ast::LetStmt = find_node_at_offset(file.syntax(), eq_offset)?; position: FilePosition,
char_typed: char,
) -> Option<SourceChange> {
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<TextEdit> {
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() { if let_stmt.has_semi() {
return None; return None;
} }
if let Some(expr) = let_stmt.initializer() { if let Some(expr) = let_stmt.initializer() {
let expr_range = expr.syntax().text_range(); 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; 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; return None;
} }
} else { } else {
@ -91,16 +136,11 @@ pub fn on_eq_typed(file: &SourceFile, eq_offset: TextUnit) -> Option<TextEdit> {
Some(edit.finish()) Some(edit.finish())
} }
pub(crate) fn on_dot_typed(db: &RootDatabase, position: FilePosition) -> Option<SourceChange> { /// Returns an edit which should be applied when a dot ('.') is typed on a blank line, indenting the line appropriately.
let parse = db.parse(position.file_id); fn on_dot_typed(file: &SourceFile, offset: TextUnit) -> Option<(TextEdit, TextUnit)> {
assert_eq!(parse.tree().syntax().text().char_at(position.offset), Some('.')); assert_eq!(file.syntax().text().char_at(offset), Some('.'));
let whitespace =
let whitespace = parse file.syntax().token_at_offset(offset).left_biased().and_then(ast::Whitespace::cast)?;
.tree()
.syntax()
.token_at_offset(position.offset)
.left_biased()
.and_then(ast::Whitespace::cast)?;
let current_indent = { let current_indent = {
let text = whitespace.text(); let text = whitespace.text();
@ -118,19 +158,11 @@ pub(crate) fn on_dot_typed(db: &RootDatabase, position: FilePosition) -> Option<
return None; return None;
} }
let mut edit = TextEditBuilder::default(); let mut edit = TextEditBuilder::default();
edit.replace( edit.replace(TextRange::from_to(offset - current_indent_len, offset), target_indent);
TextRange::from_to(position.offset - current_indent_len, position.offset),
target_indent,
);
let res = SourceChange::source_file_edit_from("reindent dot", position.file_id, edit.finish()) let cursor_offset = offset + target_indent_len - current_indent_len + TextUnit::of_char('.');
.with_cursor(FilePosition {
offset: position.offset + target_indent_len - current_indent_len
+ TextUnit::of_char('.'),
file_id: position.file_id,
});
Some(res) Some((edit.finish(), cursor_offset))
} }
#[cfg(test)] #[cfg(test)]
@ -197,9 +229,9 @@ fn foo() {
edit.insert(offset, ".".to_string()); edit.insert(offset, ".".to_string());
let before = edit.finish().apply(&before); let before = edit.finish().apply(&before);
let (analysis, file_id) = single_file(&before); let (analysis, file_id) = single_file(&before);
if let Some(result) = analysis.on_dot_typed(FilePosition { offset, file_id }).unwrap() { let file = analysis.parse(file_id).unwrap();
assert_eq!(result.source_file_edits.len(), 1); if let Some((edit, _cursor_offset)) = on_dot_typed(&file, offset) {
let actual = result.source_file_edits[0].edit.apply(&before); let actual = edit.apply(&before);
assert_eq_text!(after, &actual); assert_eq_text!(after, &actual);
} else { } else {
assert_eq_text!(&before, after) assert_eq_text!(&before, after)

View file

@ -144,12 +144,8 @@ pub fn handle_on_type_formatting(
// in `ra_ide_api`, the `on_type` invariant is that // in `ra_ide_api`, the `on_type` invariant is that
// `text.char_at(position) == typed_char`. // `text.char_at(position) == typed_char`.
position.offset = position.offset - TextUnit::of_char('.'); position.offset = position.offset - TextUnit::of_char('.');
let char_typed = params.ch.chars().next().unwrap_or('\0');
let edit = match params.ch.as_str() { let edit = world.analysis().on_char_typed(position, char_typed)?;
"=" => world.analysis().on_eq_typed(position),
"." => world.analysis().on_dot_typed(position),
_ => return Ok(None),
}?;
let mut edit = match edit { let mut edit = match edit {
Some(it) => it, Some(it) => it,
None => return Ok(None), None => return Ok(None),