mirror of
https://github.com/rust-lang/rust-analyzer
synced 2024-12-28 14:03:35 +00:00
make typing infra slightly more extensible
This commit is contained in:
parent
518f99e16b
commit
8d2fd59cfb
3 changed files with 74 additions and 54 deletions
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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),
|
||||||
|
|
Loading…
Reference in a new issue