2066: insert space after `->` r=matklad a=matklad



Co-authored-by: Aleksey Kladov <aleksey.kladov@gmail.com>
This commit is contained in:
bors[bot] 2019-10-25 11:24:43 +00:00 committed by GitHub
commit 5f779f6c46
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 464 additions and 384 deletions

View file

@ -14,6 +14,7 @@ mod db;
pub mod mock_analysis;
mod symbol_index;
mod change;
mod source_change;
mod feature_flags;
mod status;
@ -54,8 +55,6 @@ use ra_db::{
CheckCanceled, FileLoader, SourceDatabase,
};
use ra_syntax::{SourceFile, TextRange, TextUnit};
use ra_text_edit::TextEdit;
use relative_path::RelativePathBuf;
use crate::{db::LineIndexDatabase, symbol_index::FileSymbol};
@ -73,6 +72,7 @@ pub use crate::{
line_index_utils::translate_offset_with_edit,
references::{ReferenceSearchResult, SearchScope},
runnables::{Runnable, RunnableKind},
source_change::{FileSystemEdit, SourceChange, SourceFileEdit},
syntax_highlighting::HighlightedRange,
};
@ -83,99 +83,6 @@ pub use ra_db::{
pub type Cancelable<T> = Result<T, Canceled>;
#[derive(Debug)]
pub struct SourceChange {
pub label: String,
pub source_file_edits: Vec<SourceFileEdit>,
pub file_system_edits: Vec<FileSystemEdit>,
pub cursor_position: Option<FilePosition>,
}
impl SourceChange {
/// Creates a new SourceChange with the given label
/// from the edits.
pub(crate) fn from_edits<L: Into<String>>(
label: L,
source_file_edits: Vec<SourceFileEdit>,
file_system_edits: Vec<FileSystemEdit>,
) -> Self {
SourceChange {
label: label.into(),
source_file_edits,
file_system_edits,
cursor_position: None,
}
}
/// Creates a new SourceChange with the given label,
/// containing only the given `SourceFileEdits`.
pub(crate) fn source_file_edits<L: Into<String>>(label: L, edits: Vec<SourceFileEdit>) -> Self {
SourceChange {
label: label.into(),
source_file_edits: edits,
file_system_edits: vec![],
cursor_position: None,
}
}
/// Creates a new SourceChange with the given label,
/// containing only the given `FileSystemEdits`.
pub(crate) fn file_system_edits<L: Into<String>>(label: L, edits: Vec<FileSystemEdit>) -> Self {
SourceChange {
label: label.into(),
source_file_edits: vec![],
file_system_edits: edits,
cursor_position: None,
}
}
/// Creates a new SourceChange with the given label,
/// containing only a single `SourceFileEdit`.
pub(crate) fn source_file_edit<L: Into<String>>(label: L, edit: SourceFileEdit) -> Self {
SourceChange::source_file_edits(label, vec![edit])
}
/// Creates a new SourceChange with the given label
/// from the given `FileId` and `TextEdit`
pub(crate) fn source_file_edit_from<L: Into<String>>(
label: L,
file_id: FileId,
edit: TextEdit,
) -> Self {
SourceChange::source_file_edit(label, SourceFileEdit { file_id, edit })
}
/// Creates a new SourceChange with the given label
/// from the given `FileId` and `TextEdit`
pub(crate) fn file_system_edit<L: Into<String>>(label: L, edit: FileSystemEdit) -> Self {
SourceChange::file_system_edits(label, vec![edit])
}
/// Sets the cursor position to the given `FilePosition`
pub(crate) fn with_cursor(mut self, cursor_position: FilePosition) -> Self {
self.cursor_position = Some(cursor_position);
self
}
/// Sets the cursor position to the given `FilePosition`
pub(crate) fn with_cursor_opt(mut self, cursor_position: Option<FilePosition>) -> Self {
self.cursor_position = cursor_position;
self
}
}
#[derive(Debug)]
pub struct SourceFileEdit {
pub file_id: FileId,
pub edit: TextEdit,
}
#[derive(Debug)]
pub enum FileSystemEdit {
CreateFile { source_root: SourceRootId, path: RelativePathBuf },
MoveFile { src: FileId, dst_source_root: SourceRootId, dst_path: RelativePathBuf },
}
#[derive(Debug)]
pub struct Diagnostic {
pub message: String,
@ -407,24 +314,20 @@ 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<Option<SourceChange>> {
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<Option<SourceChange>> {
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<Option<SourceChange>> {
// Fast path to not even parse the file.
if !typing::TRIGGER_CHARS.contains(char_typed) {
return Ok(None);
}
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

View file

@ -0,0 +1,119 @@
//! This modules defines type to represent changes to the source code, that flow
//! from the server to the client.
//!
//! It can be viewed as a dual for `AnalysisChange`.
use ra_text_edit::TextEdit;
use relative_path::RelativePathBuf;
use crate::{FileId, FilePosition, SourceRootId, TextUnit};
#[derive(Debug)]
pub struct SourceChange {
pub label: String,
pub source_file_edits: Vec<SourceFileEdit>,
pub file_system_edits: Vec<FileSystemEdit>,
pub cursor_position: Option<FilePosition>,
}
impl SourceChange {
/// Creates a new SourceChange with the given label
/// from the edits.
pub(crate) fn from_edits<L: Into<String>>(
label: L,
source_file_edits: Vec<SourceFileEdit>,
file_system_edits: Vec<FileSystemEdit>,
) -> Self {
SourceChange {
label: label.into(),
source_file_edits,
file_system_edits,
cursor_position: None,
}
}
/// Creates a new SourceChange with the given label,
/// containing only the given `SourceFileEdits`.
pub(crate) fn source_file_edits<L: Into<String>>(label: L, edits: Vec<SourceFileEdit>) -> Self {
SourceChange {
label: label.into(),
source_file_edits: edits,
file_system_edits: vec![],
cursor_position: None,
}
}
/// Creates a new SourceChange with the given label,
/// containing only the given `FileSystemEdits`.
pub(crate) fn file_system_edits<L: Into<String>>(label: L, edits: Vec<FileSystemEdit>) -> Self {
SourceChange {
label: label.into(),
source_file_edits: vec![],
file_system_edits: edits,
cursor_position: None,
}
}
/// Creates a new SourceChange with the given label,
/// containing only a single `SourceFileEdit`.
pub(crate) fn source_file_edit<L: Into<String>>(label: L, edit: SourceFileEdit) -> Self {
SourceChange::source_file_edits(label, vec![edit])
}
/// Creates a new SourceChange with the given label
/// from the given `FileId` and `TextEdit`
pub(crate) fn source_file_edit_from<L: Into<String>>(
label: L,
file_id: FileId,
edit: TextEdit,
) -> Self {
SourceChange::source_file_edit(label, SourceFileEdit { file_id, edit })
}
/// Creates a new SourceChange with the given label
/// from the given `FileId` and `TextEdit`
pub(crate) fn file_system_edit<L: Into<String>>(label: L, edit: FileSystemEdit) -> Self {
SourceChange::file_system_edits(label, vec![edit])
}
/// Sets the cursor position to the given `FilePosition`
pub(crate) fn with_cursor(mut self, cursor_position: FilePosition) -> Self {
self.cursor_position = Some(cursor_position);
self
}
/// Sets the cursor position to the given `FilePosition`
pub(crate) fn with_cursor_opt(mut self, cursor_position: Option<FilePosition>) -> Self {
self.cursor_position = cursor_position;
self
}
}
#[derive(Debug)]
pub struct SourceFileEdit {
pub file_id: FileId,
pub edit: TextEdit,
}
#[derive(Debug)]
pub enum FileSystemEdit {
CreateFile { source_root: SourceRootId, path: RelativePathBuf },
MoveFile { src: FileId, dst_source_root: SourceRootId, dst_path: RelativePathBuf },
}
pub(crate) struct SingleFileChange {
pub label: String,
pub edit: TextEdit,
pub cursor_position: Option<TextUnit>,
}
impl SingleFileChange {
pub(crate) fn into_source_change(self, file_id: FileId) -> SourceChange {
SourceChange {
label: self.label,
source_file_edits: vec![SourceFileEdit { file_id, edit: self.edit }],
file_system_edits: Vec::new(),
cursor_position: self.cursor_position.map(|offset| FilePosition { file_id, offset }),
}
}
}

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_fmt::leading_indent;
@ -11,7 +24,7 @@ use ra_syntax::{
};
use ra_text_edit::{TextEdit, TextEditBuilder};
use crate::{db::RootDatabase, SourceChange, SourceFileEdit};
use crate::{db::RootDatabase, source_change::SingleFileChange, SourceChange, SourceFileEdit};
pub(crate) fn on_enter(db: &RootDatabase, position: FilePosition) -> Option<SourceChange> {
let parse = db.parse(position.file_id);
@ -68,39 +81,67 @@ fn node_indent(file: &SourceFile, token: &SyntaxToken) -> Option<SmolStr> {
Some(text[pos..].into())
}
pub fn on_eq_typed(file: &SourceFile, eq_offset: TextUnit) -> Option<TextEdit> {
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) const TRIGGER_CHARS: &str = ".=>";
pub(crate) fn on_char_typed(
db: &RootDatabase,
position: FilePosition,
char_typed: char,
) -> Option<SourceChange> {
assert!(TRIGGER_CHARS.contains(char_typed));
let file = &db.parse(position.file_id).tree();
assert_eq!(file.syntax().text().char_at(position.offset), Some(char_typed));
let single_file_change = on_char_typed_inner(file, position.offset, char_typed)?;
Some(single_file_change.into_source_change(position.file_id))
}
fn on_char_typed_inner(
file: &SourceFile,
offset: TextUnit,
char_typed: char,
) -> Option<SingleFileChange> {
assert!(TRIGGER_CHARS.contains(char_typed));
match char_typed {
'.' => on_dot_typed(file, offset),
'=' => on_eq_typed(file, offset),
'>' => on_arrow_typed(file, offset),
_ => unreachable!(),
}
}
/// 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<SingleFileChange> {
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 {
return None;
}
let offset = let_stmt.syntax().text_range().end();
let mut edit = TextEditBuilder::default();
edit.insert(offset, ";".to_string());
Some(edit.finish())
Some(SingleFileChange {
label: "add semicolon".to_string(),
edit: TextEdit::insert(offset, ";".to_string()),
cursor_position: None,
})
}
pub(crate) fn on_dot_typed(db: &RootDatabase, position: FilePosition) -> Option<SourceChange> {
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<SingleFileChange> {
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();
@ -117,20 +158,36 @@ pub(crate) fn on_dot_typed(db: &RootDatabase, position: FilePosition) -> Option<
if current_indent_len == target_indent_len {
return None;
}
let mut edit = TextEditBuilder::default();
edit.replace(
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())
.with_cursor(FilePosition {
offset: position.offset + target_indent_len - current_indent_len
+ TextUnit::of_char('.'),
file_id: position.file_id,
});
Some(SingleFileChange {
label: "reindent dot".to_string(),
edit: TextEdit::replace(
TextRange::from_to(offset - current_indent_len, offset),
target_indent,
),
cursor_position: Some(
offset + target_indent_len - current_indent_len + TextUnit::of_char('.'),
),
})
}
Some(res)
/// Adds a space after an arrow when `fn foo() { ... }` is turned into `fn foo() -> { ... }`
fn on_arrow_typed(file: &SourceFile, offset: TextUnit) -> Option<SingleFileChange> {
let file_text = file.syntax().text();
assert_eq!(file_text.char_at(offset), Some('>'));
let after_arrow = offset + TextUnit::of_char('>');
if file_text.char_at(after_arrow) != Some('{') {
return None;
}
if find_node_at_offset::<ast::RetType>(file.syntax(), offset).is_none() {
return None;
}
Some(SingleFileChange {
label: "add space after return type".to_string(),
edit: TextEdit::insert(after_arrow, " ".to_string()),
cursor_position: Some(after_arrow),
})
}
#[cfg(test)]
@ -141,239 +198,6 @@ mod tests {
use super::*;
#[test]
fn test_on_eq_typed() {
fn type_eq(before: &str, after: &str) {
let (offset, before) = extract_offset(before);
let mut edit = TextEditBuilder::default();
edit.insert(offset, "=".to_string());
let before = edit.finish().apply(&before);
let parse = SourceFile::parse(&before);
if let Some(result) = on_eq_typed(&parse.tree(), offset) {
let actual = result.apply(&before);
assert_eq_text!(after, &actual);
} else {
assert_eq_text!(&before, after)
};
}
// do_check(r"
// fn foo() {
// let foo =<|>
// }
// ", r"
// fn foo() {
// let foo =;
// }
// ");
type_eq(
r"
fn foo() {
let foo <|> 1 + 1
}
",
r"
fn foo() {
let foo = 1 + 1;
}
",
);
// do_check(r"
// fn foo() {
// let foo =<|>
// let bar = 1;
// }
// ", r"
// fn foo() {
// let foo =;
// let bar = 1;
// }
// ");
}
fn type_dot(before: &str, after: &str) {
let (offset, before) = extract_offset(before);
let mut edit = TextEditBuilder::default();
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);
assert_eq_text!(after, &actual);
} else {
assert_eq_text!(&before, after)
};
}
#[test]
fn indents_new_chain_call() {
type_dot(
r"
pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
self.child_impl(db, name)
<|>
}
",
r"
pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
self.child_impl(db, name)
.
}
",
);
type_dot(
r"
pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
self.child_impl(db, name)
<|>
}
",
r"
pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
self.child_impl(db, name)
.
}
",
)
}
#[test]
fn indents_new_chain_call_with_semi() {
type_dot(
r"
pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
self.child_impl(db, name)
<|>;
}
",
r"
pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
self.child_impl(db, name)
.;
}
",
);
type_dot(
r"
pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
self.child_impl(db, name)
<|>;
}
",
r"
pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
self.child_impl(db, name)
.;
}
",
)
}
#[test]
fn indents_continued_chain_call() {
type_dot(
r"
pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
self.child_impl(db, name)
.first()
<|>
}
",
r"
pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
self.child_impl(db, name)
.first()
.
}
",
);
type_dot(
r"
pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
self.child_impl(db, name)
.first()
<|>
}
",
r"
pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
self.child_impl(db, name)
.first()
.
}
",
);
}
#[test]
fn indents_middle_of_chain_call() {
type_dot(
r"
fn source_impl() {
let var = enum_defvariant_list().unwrap()
<|>
.nth(92)
.unwrap();
}
",
r"
fn source_impl() {
let var = enum_defvariant_list().unwrap()
.
.nth(92)
.unwrap();
}
",
);
type_dot(
r"
fn source_impl() {
let var = enum_defvariant_list().unwrap()
<|>
.nth(92)
.unwrap();
}
",
r"
fn source_impl() {
let var = enum_defvariant_list().unwrap()
.
.nth(92)
.unwrap();
}
",
);
}
#[test]
fn dont_indent_freestanding_dot() {
type_dot(
r"
pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
<|>
}
",
r"
pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
.
}
",
);
type_dot(
r"
pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
<|>
}
",
r"
pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
.
}
",
);
}
#[test]
fn test_on_enter() {
fn apply_on_enter(before: &str) -> Option<String> {
@ -426,4 +250,214 @@ impl S {
);
do_check_noop(r"<|>//! docz");
}
fn do_type_char(char_typed: char, before: &str) -> Option<(String, SingleFileChange)> {
let (offset, before) = extract_offset(before);
let edit = TextEdit::insert(offset, char_typed.to_string());
let before = edit.apply(&before);
let parse = SourceFile::parse(&before);
on_char_typed_inner(&parse.tree(), offset, char_typed)
.map(|it| (it.edit.apply(&before), it))
}
fn type_char(char_typed: char, before: &str, after: &str) {
let (actual, file_change) = do_type_char(char_typed, before)
.expect(&format!("typing `{}` did nothing", char_typed));
if after.contains("<|>") {
let (offset, after) = extract_offset(after);
assert_eq_text!(&after, &actual);
assert_eq!(file_change.cursor_position, Some(offset))
} else {
assert_eq_text!(after, &actual);
}
}
fn type_char_noop(char_typed: char, before: &str) {
let file_change = do_type_char(char_typed, before);
assert!(file_change.is_none())
}
#[test]
fn test_on_eq_typed() {
// do_check(r"
// fn foo() {
// let foo =<|>
// }
// ", r"
// fn foo() {
// let foo =;
// }
// ");
type_char(
'=',
r"
fn foo() {
let foo <|> 1 + 1
}
",
r"
fn foo() {
let foo = 1 + 1;
}
",
);
// do_check(r"
// fn foo() {
// let foo =<|>
// let bar = 1;
// }
// ", r"
// fn foo() {
// let foo =;
// let bar = 1;
// }
// ");
}
#[test]
fn indents_new_chain_call() {
type_char(
'.',
r"
pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
self.child_impl(db, name)
<|>
}
",
r"
pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
self.child_impl(db, name)
.
}
",
);
type_char_noop(
'.',
r"
pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
self.child_impl(db, name)
<|>
}
",
)
}
#[test]
fn indents_new_chain_call_with_semi() {
type_char(
'.',
r"
pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
self.child_impl(db, name)
<|>;
}
",
r"
pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
self.child_impl(db, name)
.;
}
",
);
type_char_noop(
'.',
r"
pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
self.child_impl(db, name)
<|>;
}
",
)
}
#[test]
fn indents_continued_chain_call() {
type_char(
'.',
r"
pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
self.child_impl(db, name)
.first()
<|>
}
",
r"
pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
self.child_impl(db, name)
.first()
.
}
",
);
type_char_noop(
'.',
r"
pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
self.child_impl(db, name)
.first()
<|>
}
",
);
}
#[test]
fn indents_middle_of_chain_call() {
type_char(
'.',
r"
fn source_impl() {
let var = enum_defvariant_list().unwrap()
<|>
.nth(92)
.unwrap();
}
",
r"
fn source_impl() {
let var = enum_defvariant_list().unwrap()
.
.nth(92)
.unwrap();
}
",
);
type_char_noop(
'.',
r"
fn source_impl() {
let var = enum_defvariant_list().unwrap()
<|>
.nth(92)
.unwrap();
}
",
);
}
#[test]
fn dont_indent_freestanding_dot() {
type_char_noop(
'.',
r"
pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
<|>
}
",
);
type_char_noop(
'.',
r"
pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
<|>
}
",
);
}
#[test]
fn adds_space_after_return_type() {
type_char('>', "fn foo() -<|>{ 92 }", "fn foo() -><|> { 92 }")
}
}

View file

@ -38,7 +38,7 @@ pub fn server_capabilities() -> ServerCapabilities {
document_range_formatting_provider: None,
document_on_type_formatting_provider: Some(DocumentOnTypeFormattingOptions {
first_trigger_character: "=".to_string(),
more_trigger_character: Some(vec![".".to_string()]),
more_trigger_character: Some(vec![".".to_string(), ">".to_string()]),
}),
selection_range_provider: Some(GenericCapability::default()),
folding_range_provider: Some(FoldingRangeProviderCapability::Simple(true)),

View file

@ -132,6 +132,7 @@ pub fn handle_on_enter(
}
}
// Don't forget to add new trigger characters to `ServerCapabilities` in `caps.rs`.
pub fn handle_on_type_formatting(
world: WorldSnapshot,
params: req::DocumentOnTypeFormattingParams,
@ -144,12 +145,17 @@ 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 char_typed = params.ch.chars().next().unwrap_or('\0');
let edit = match params.ch.as_str() {
"=" => world.analysis().on_eq_typed(position),
"." => world.analysis().on_dot_typed(position),
_ => return Ok(None),
}?;
// We have an assist that inserts ` ` after typing `->` in `fn foo() ->{`,
// but it requires precise cursor positioning to work, and one can't
// position the cursor with on_type formatting. So, let's just toggle this
// feature off here, hoping that we'll enable it one day, 😿.
if char_typed == '>' {
return Ok(None);
}
let edit = world.analysis().on_char_typed(position, char_typed)?;
let mut edit = match edit {
Some(it) => it,
None => return Ok(None),

View file

@ -32,6 +32,24 @@ impl TextEditBuilder {
}
impl TextEdit {
pub fn insert(offset: TextUnit, text: String) -> TextEdit {
let mut builder = TextEditBuilder::default();
builder.insert(offset, text);
builder.finish()
}
pub fn delete(range: TextRange) -> TextEdit {
let mut builder = TextEditBuilder::default();
builder.delete(range);
builder.finish()
}
pub fn replace(range: TextRange, replace_with: String) -> TextEdit {
let mut builder = TextEditBuilder::default();
builder.replace(range, replace_with);
builder.finish()
}
pub(crate) fn from_atoms(mut atoms: Vec<AtomTextEdit>) -> TextEdit {
atoms.sort_by_key(|a| (a.delete.start(), a.delete.end()));
for (a1, a2) in atoms.iter().zip(atoms.iter().skip(1)) {