From 8d2fd59cfb00211573419b0a59cf91d92d636f5a Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Fri, 25 Oct 2019 11:19:26 +0300 Subject: make typing infra slightly more extensible --- crates/ra_ide_api/src/lib.rs | 28 +++----- crates/ra_ide_api/src/typing.rs | 92 +++++++++++++++++--------- crates/ra_lsp_server/src/main_loop/handlers.rs | 8 +-- 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 { 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 2f5782012..44cc46147 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 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( // 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), -- cgit v1.2.3 From b112430ca73f646b6cb779ab09a3f691aad22442 Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Fri, 25 Oct 2019 11:26:53 +0300 Subject: move source change to a dedicated file --- crates/ra_ide_api/src/lib.rs | 97 +------------------------------ crates/ra_ide_api/src/source_change.rs | 102 +++++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+), 95 deletions(-) create mode 100644 crates/ra_ide_api/src/source_change.rs diff --git a/crates/ra_ide_api/src/lib.rs b/crates/ra_ide_api/src/lib.rs index b2a1d185b..6b8aa7a8e 100644 --- a/crates/ra_ide_api/src/lib.rs +++ b/crates/ra_ide_api/src/lib.rs @@ -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 = Result; -#[derive(Debug)] -pub struct SourceChange { - pub label: String, - pub source_file_edits: Vec, - pub file_system_edits: Vec, - pub cursor_position: Option, -} - -impl SourceChange { - /// Creates a new SourceChange with the given label - /// from the edits. - pub(crate) fn from_edits>( - label: L, - source_file_edits: Vec, - file_system_edits: Vec, - ) -> 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>(label: L, edits: Vec) -> 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>(label: L, edits: Vec) -> 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>(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>( - 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>(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) -> 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, diff --git a/crates/ra_ide_api/src/source_change.rs b/crates/ra_ide_api/src/source_change.rs new file mode 100644 index 000000000..80e8821b0 --- /dev/null +++ b/crates/ra_ide_api/src/source_change.rs @@ -0,0 +1,102 @@ +//! 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}; + +#[derive(Debug)] +pub struct SourceChange { + pub label: String, + pub source_file_edits: Vec, + pub file_system_edits: Vec, + pub cursor_position: Option, +} + +impl SourceChange { + /// Creates a new SourceChange with the given label + /// from the edits. + pub(crate) fn from_edits>( + label: L, + source_file_edits: Vec, + file_system_edits: Vec, + ) -> 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>(label: L, edits: Vec) -> 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>(label: L, edits: Vec) -> 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>(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>( + 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>(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) -> 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 }, +} -- cgit v1.2.3 From 6f00bb1cb0e5fb72fac092d63c07f8652091d4d9 Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Fri, 25 Oct 2019 11:49:38 +0300 Subject: introduce SingleFileChange --- crates/ra_ide_api/src/source_change.rs | 19 ++++++++++- crates/ra_ide_api/src/typing.rs | 62 +++++++++++++++------------------- crates/ra_text_edit/src/text_edit.rs | 18 ++++++++++ 3 files changed, 64 insertions(+), 35 deletions(-) diff --git a/crates/ra_ide_api/src/source_change.rs b/crates/ra_ide_api/src/source_change.rs index 80e8821b0..4e63bbf6f 100644 --- a/crates/ra_ide_api/src/source_change.rs +++ b/crates/ra_ide_api/src/source_change.rs @@ -6,7 +6,7 @@ use ra_text_edit::TextEdit; use relative_path::RelativePathBuf; -use crate::{FileId, FilePosition, SourceRootId}; +use crate::{FileId, FilePosition, SourceRootId, TextUnit}; #[derive(Debug)] pub struct SourceChange { @@ -100,3 +100,20 @@ 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, +} + +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 }), + } + } +} diff --git a/crates/ra_ide_api/src/typing.rs b/crates/ra_ide_api/src/typing.rs index 44cc46147..c5ec6c1c1 100644 --- a/crates/ra_ide_api/src/typing.rs +++ b/crates/ra_ide_api/src/typing.rs @@ -24,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 { let parse = db.parse(position.file_id); @@ -88,32 +88,19 @@ pub(crate) fn on_char_typed( ) -> 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, - } + let single_file_change = match char_typed { + '=' => on_eq_typed(file, position.offset)?, + '.' => on_dot_typed(file, position.offset)?, + _ => return None, + }; + + Some(single_file_change.into_source_change(position.file_id)) } /// 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 { +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() { @@ -131,13 +118,15 @@ fn on_eq_typed(file: &SourceFile, offset: TextUnit) -> Option { 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, + }) } /// 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)> { +fn on_dot_typed(file: &SourceFile, offset: TextUnit) -> Option { assert_eq!(file.syntax().text().char_at(offset), Some('.')); let whitespace = file.syntax().token_at_offset(offset).left_biased().and_then(ast::Whitespace::cast)?; @@ -157,12 +146,17 @@ fn on_dot_typed(file: &SourceFile, offset: TextUnit) -> Option<(TextEdit, TextUn if current_indent_len == target_indent_len { return None; } - let mut edit = TextEditBuilder::default(); - edit.replace(TextRange::from_to(offset - current_indent_len, offset), target_indent); - - let cursor_offset = offset + target_indent_len - current_indent_len + TextUnit::of_char('.'); - Some((edit.finish(), cursor_offset)) + 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('.'), + ), + }) } #[cfg(test)] @@ -182,7 +176,7 @@ mod tests { 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); + let actual = result.edit.apply(&before); assert_eq_text!(after, &actual); } else { assert_eq_text!(&before, after) @@ -230,8 +224,8 @@ fn foo() { let before = edit.finish().apply(&before); let (analysis, file_id) = single_file(&before); let file = analysis.parse(file_id).unwrap(); - if let Some((edit, _cursor_offset)) = on_dot_typed(&file, offset) { - let actual = edit.apply(&before); + if let Some(result) = on_dot_typed(&file, offset) { + let actual = result.edit.apply(&before); assert_eq_text!(after, &actual); } else { assert_eq_text!(&before, after) diff --git a/crates/ra_text_edit/src/text_edit.rs b/crates/ra_text_edit/src/text_edit.rs index 0381ea000..413c7d782 100644 --- a/crates/ra_text_edit/src/text_edit.rs +++ b/crates/ra_text_edit/src/text_edit.rs @@ -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) -> TextEdit { atoms.sort_by_key(|a| (a.delete.start(), a.delete.end())); for (a1, a2) in atoms.iter().zip(atoms.iter().skip(1)) { -- cgit v1.2.3 From ea948e9fbb519ab5f4a21e0cce0dc5f0f365a716 Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Fri, 25 Oct 2019 12:04:17 +0300 Subject: refactor typing_handlers --- crates/ra_ide_api/src/lib.rs | 4 +++ crates/ra_ide_api/src/typing.rs | 72 +++++++++++++++++++++-------------------- 2 files changed, 41 insertions(+), 35 deletions(-) diff --git a/crates/ra_ide_api/src/lib.rs b/crates/ra_ide_api/src/lib.rs index 6b8aa7a8e..d0188da44 100644 --- a/crates/ra_ide_api/src/lib.rs +++ b/crates/ra_ide_api/src/lib.rs @@ -323,6 +323,10 @@ impl Analysis { position: FilePosition, char_typed: char, ) -> Cancelable> { + // 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)) } diff --git a/crates/ra_ide_api/src/typing.rs b/crates/ra_ide_api/src/typing.rs index c5ec6c1c1..17d0f08a5 100644 --- a/crates/ra_ide_api/src/typing.rs +++ b/crates/ra_ide_api/src/typing.rs @@ -81,22 +81,32 @@ fn node_indent(file: &SourceFile, token: &SyntaxToken) -> Option { Some(text[pos..].into()) } +pub(crate) const TRIGGER_CHARS: &str = ".="; + pub(crate) fn on_char_typed( db: &RootDatabase, position: FilePosition, char_typed: char, ) -> Option { + 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 = match char_typed { - '=' => on_eq_typed(file, position.offset)?, - '.' => on_dot_typed(file, position.offset)?, - _ => return None, - }; - + 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 { + match char_typed { + '.' => on_dot_typed(file, offset), + '=' => on_eq_typed(file, 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. @@ -167,22 +177,29 @@ mod tests { use super::*; + fn type_char(char_typed: char, before: &str, after: &str) { + 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); + if let Some(result) = on_char_typed_inner(&parse.tree(), offset, char_typed) { + let actual = result.edit.apply(&before); + assert_eq_text!(after, &actual); + } else { + assert_eq_text!(&before, after) + }; + } + + fn type_eq(before: &str, after: &str) { + type_char('=', before, after); + } + + fn type_dot(before: &str, after: &str) { + type_char('.', before, after); + } + #[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.edit.apply(&before); - assert_eq_text!(after, &actual); - } else { - assert_eq_text!(&before, after) - }; - } - // do_check(r" // fn foo() { // let foo =<|> @@ -217,21 +234,6 @@ fn foo() { // "); } - 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); - let file = analysis.parse(file_id).unwrap(); - if let Some(result) = on_dot_typed(&file, offset) { - let actual = result.edit.apply(&before); - assert_eq_text!(after, &actual); - } else { - assert_eq_text!(&before, after) - }; - } - #[test] fn indents_new_chain_call() { type_dot( -- cgit v1.2.3 From 53e3bee0cfcd7541b5ee882ab4b47c9dde9780b8 Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Fri, 25 Oct 2019 12:16:56 +0300 Subject: insert space after `->` --- crates/ra_ide_api/src/typing.rs | 228 +++++++++++++------------ crates/ra_lsp_server/src/caps.rs | 2 +- crates/ra_lsp_server/src/main_loop/handlers.rs | 1 + 3 files changed, 119 insertions(+), 112 deletions(-) diff --git a/crates/ra_ide_api/src/typing.rs b/crates/ra_ide_api/src/typing.rs index 17d0f08a5..26a3111fd 100644 --- a/crates/ra_ide_api/src/typing.rs +++ b/crates/ra_ide_api/src/typing.rs @@ -81,7 +81,7 @@ fn node_indent(file: &SourceFile, token: &SyntaxToken) -> Option { Some(text[pos..].into()) } -pub(crate) const TRIGGER_CHARS: &str = ".="; +pub(crate) const TRIGGER_CHARS: &str = ".=>"; pub(crate) fn on_char_typed( db: &RootDatabase, @@ -100,10 +100,12 @@ fn on_char_typed_inner( offset: TextUnit, char_typed: char, ) -> Option { + assert!(TRIGGER_CHARS.contains(char_typed)); match char_typed { '.' => on_dot_typed(file, offset), '=' => on_eq_typed(file, offset), - _ => None, + '>' => on_arrow_typed(file, offset), + _ => unreachable!(), } } @@ -169,6 +171,25 @@ fn on_dot_typed(file: &SourceFile, offset: TextUnit) -> Option }) } +/// Adds a space after an arrow when `fn foo() { ... }` is turned into `fn foo() -> { ... }` +fn on_arrow_typed(file: &SourceFile, offset: TextUnit) -> Option { + 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::(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)] mod tests { use test_utils::{add_cursor, assert_eq_text, extract_offset}; @@ -177,25 +198,84 @@ mod tests { use super::*; - fn type_char(char_typed: char, before: &str, after: &str) { + #[test] + fn test_on_enter() { + fn apply_on_enter(before: &str) -> Option { + let (offset, before) = extract_offset(before); + let (analysis, file_id) = single_file(&before); + let result = analysis.on_enter(FilePosition { offset, file_id }).unwrap()?; + + assert_eq!(result.source_file_edits.len(), 1); + let actual = result.source_file_edits[0].edit.apply(&before); + let actual = add_cursor(&actual, result.cursor_position.unwrap().offset); + Some(actual) + } + + fn do_check(before: &str, after: &str) { + let actual = apply_on_enter(before).unwrap(); + assert_eq_text!(after, &actual); + } + + fn do_check_noop(text: &str) { + assert!(apply_on_enter(text).is_none()) + } + + do_check( + r" +/// Some docs<|> +fn foo() { +} +", + r" +/// Some docs +/// <|> +fn foo() { +} +", + ); + do_check( + r" +impl S { + /// Some<|> docs. + fn foo() {} +} +", + r" +impl S { + /// Some + /// <|> docs. + fn foo() {} +} +", + ); + 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); - if let Some(result) = on_char_typed_inner(&parse.tree(), offset, char_typed) { - let actual = result.edit.apply(&before); - assert_eq_text!(after, &actual); - } else { - assert_eq_text!(&before, after) - }; + on_char_typed_inner(&parse.tree(), offset, char_typed) + .map(|it| (it.edit.apply(&before), it)) } - fn type_eq(before: &str, after: &str) { - type_char('=', before, after); + 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_dot(before: &str, after: &str) { - type_char('.', before, after); + fn type_char_noop(char_typed: char, before: &str) { + let file_change = do_type_char(char_typed, before); + assert!(file_change.is_none()) } #[test] @@ -209,7 +289,8 @@ mod tests { // let foo =; // } // "); - type_eq( + type_char( + '=', r" fn foo() { let foo <|> 1 + 1 @@ -236,7 +317,8 @@ fn foo() { #[test] fn indents_new_chain_call() { - type_dot( + type_char( + '.', r" pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable> { self.child_impl(db, name) @@ -250,25 +332,21 @@ fn foo() { } ", ); - type_dot( + type_char_noop( + '.', r" pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable> { self.child_impl(db, name) <|> } ", - r" - pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable> { - self.child_impl(db, name) - . - } - ", ) } #[test] fn indents_new_chain_call_with_semi() { - type_dot( + type_char( + '.', r" pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable> { self.child_impl(db, name) @@ -282,25 +360,21 @@ fn foo() { } ", ); - type_dot( + type_char_noop( + '.', r" pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable> { self.child_impl(db, name) <|>; } ", - r" - pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable> { - self.child_impl(db, name) - .; - } - ", ) } #[test] fn indents_continued_chain_call() { - type_dot( + type_char( + '.', r" pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable> { self.child_impl(db, name) @@ -316,7 +390,8 @@ fn foo() { } ", ); - type_dot( + type_char_noop( + '.', r" pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable> { self.child_impl(db, name) @@ -324,19 +399,13 @@ fn foo() { <|> } ", - r" - pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable> { - self.child_impl(db, name) - .first() - . - } - ", ); } #[test] fn indents_middle_of_chain_call() { - type_dot( + type_char( + '.', r" fn source_impl() { let var = enum_defvariant_list().unwrap() @@ -354,7 +423,8 @@ fn foo() { } ", ); - type_dot( + type_char_noop( + '.', r" fn source_impl() { let var = enum_defvariant_list().unwrap() @@ -363,95 +433,31 @@ fn foo() { .unwrap(); } ", - r" - fn source_impl() { - let var = enum_defvariant_list().unwrap() - . - .nth(92) - .unwrap(); - } - ", ); } #[test] fn dont_indent_freestanding_dot() { - type_dot( + type_char_noop( + '.', r" pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable> { <|> } ", - r" - pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable> { - . - } - ", ); - type_dot( + type_char_noop( + '.', r" pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable> { <|> } ", - r" - pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable> { - . - } - ", ); } #[test] - fn test_on_enter() { - fn apply_on_enter(before: &str) -> Option { - let (offset, before) = extract_offset(before); - let (analysis, file_id) = single_file(&before); - let result = analysis.on_enter(FilePosition { offset, file_id }).unwrap()?; - - assert_eq!(result.source_file_edits.len(), 1); - let actual = result.source_file_edits[0].edit.apply(&before); - let actual = add_cursor(&actual, result.cursor_position.unwrap().offset); - Some(actual) - } - - fn do_check(before: &str, after: &str) { - let actual = apply_on_enter(before).unwrap(); - assert_eq_text!(after, &actual); - } - - fn do_check_noop(text: &str) { - assert!(apply_on_enter(text).is_none()) - } - - do_check( - r" -/// Some docs<|> -fn foo() { -} -", - r" -/// Some docs -/// <|> -fn foo() { -} -", - ); - do_check( - r" -impl S { - /// Some<|> docs. - fn foo() {} -} -", - r" -impl S { - /// Some - /// <|> docs. - fn foo() {} -} -", - ); - do_check_noop(r"<|>//! docz"); + fn adds_space_after_return_type() { + type_char('>', "fn foo() -<|>{ 92 }", "fn foo() -><|> { 92 }") } } diff --git a/crates/ra_lsp_server/src/caps.rs b/crates/ra_lsp_server/src/caps.rs index 30bcbd7a8..eea0965ed 100644 --- a/crates/ra_lsp_server/src/caps.rs +++ b/crates/ra_lsp_server/src/caps.rs @@ -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)), diff --git a/crates/ra_lsp_server/src/main_loop/handlers.rs b/crates/ra_lsp_server/src/main_loop/handlers.rs index 530c4d8b6..6f1e59b4b 100644 --- a/crates/ra_lsp_server/src/main_loop/handlers.rs +++ b/crates/ra_lsp_server/src/main_loop/handlers.rs @@ -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, -- cgit v1.2.3 From d5cd8b5be2a146abe75c0aa322f2313240c8f23c Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Fri, 25 Oct 2019 13:03:57 +0300 Subject: disable the new typing handler for `->` It doesn't actually work with LSP --- crates/ra_lsp_server/src/main_loop/handlers.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/crates/ra_lsp_server/src/main_loop/handlers.rs b/crates/ra_lsp_server/src/main_loop/handlers.rs index 6f1e59b4b..16fb07266 100644 --- a/crates/ra_lsp_server/src/main_loop/handlers.rs +++ b/crates/ra_lsp_server/src/main_loop/handlers.rs @@ -146,6 +146,15 @@ pub fn handle_on_type_formatting( // `text.char_at(position) == typed_char`. position.offset = position.offset - TextUnit::of_char('.'); let char_typed = params.ch.chars().next().unwrap_or('\0'); + + // 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, -- cgit v1.2.3