From 66be735aa98c32fb062d1c756fa9303ff2d13002 Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Sun, 12 Aug 2018 18:50:16 +0300 Subject: flip comma --- crates/assert_eq_text/Cargo.toml | 7 +++ crates/assert_eq_text/src/lib.rs | 25 +++++++++ crates/libeditor/Cargo.toml | 2 + crates/libeditor/src/code_actions.rs | 33 +++++++++++ crates/libeditor/src/edit.rs | 93 +++++++++++++++++++++++++++++++ crates/libeditor/src/lib.rs | 7 ++- crates/libeditor/tests/test.rs | 52 ++++++++++++++--- crates/libsyntax2/Cargo.toml | 2 +- crates/libsyntax2/src/algo/mod.rs | 48 +++++++++------- crates/libsyntax2/src/syntax_kinds/mod.rs | 2 +- crates/libsyntax2/src/yellow/syntax.rs | 11 ++++ crates/libsyntax2/tests/test/main.rs | 48 +++++++--------- 12 files changed, 273 insertions(+), 57 deletions(-) create mode 100644 crates/assert_eq_text/Cargo.toml create mode 100644 crates/assert_eq_text/src/lib.rs create mode 100644 crates/libeditor/src/code_actions.rs create mode 100644 crates/libeditor/src/edit.rs diff --git a/crates/assert_eq_text/Cargo.toml b/crates/assert_eq_text/Cargo.toml new file mode 100644 index 000000000..21858dfd3 --- /dev/null +++ b/crates/assert_eq_text/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "assert_eq_text" +version = "0.1.0" +authors = ["Aleksey Kladov "] + +[dependencies] +difference = "2.0.0" diff --git a/crates/assert_eq_text/src/lib.rs b/crates/assert_eq_text/src/lib.rs new file mode 100644 index 000000000..ed942d81a --- /dev/null +++ b/crates/assert_eq_text/src/lib.rs @@ -0,0 +1,25 @@ +extern crate difference; +pub use self::difference::Changeset as __Changeset; + +#[macro_export] +macro_rules! assert_eq_text { + ($expected:expr, $actual:expr) => {{ + let expected = $expected; + let actual = $actual; + if expected != actual { + let changeset = $crate::__Changeset::new(actual, expected, "\n"); + println!("Expected:\n{}\n\nActual:\n{}\nDiff:{}\n", expected, actual, changeset); + panic!("text differs"); + } + }}; + ($expected:expr, $actual:expr, $($tt:tt)*) => {{ + let expected = $expected; + let actual = $actual; + if expected != actual { + let changeset = $crate::__Changeset::new(actual, expected, "\n"); + println!("Expected:\n{}\n\nActual:\n{}\n\nDiff:\n{}\n", expected, actual, changeset); + println!($($tt)*); + panic!("text differs"); + } + }}; +} diff --git a/crates/libeditor/Cargo.toml b/crates/libeditor/Cargo.toml index d6423979b..fe688bc20 100644 --- a/crates/libeditor/Cargo.toml +++ b/crates/libeditor/Cargo.toml @@ -7,4 +7,6 @@ publish = false [dependencies] itertools = "0.7.8" superslice = "0.1.0" + libsyntax2 = { path = "../libsyntax2" } +assert_eq_text = { path = "../assert_eq_text" } diff --git a/crates/libeditor/src/code_actions.rs b/crates/libeditor/src/code_actions.rs new file mode 100644 index 000000000..7c9874588 --- /dev/null +++ b/crates/libeditor/src/code_actions.rs @@ -0,0 +1,33 @@ +use {TextUnit, File, EditBuilder, Edit}; +use libsyntax2::{ + ast::AstNode, + SyntaxKind::COMMA, + SyntaxNodeRef, + algo::{ + Direction, siblings, + find_leaf_at_offset, + }, +}; + +pub fn flip_comma<'a>(file: &'a File, offset: TextUnit) -> Option Edit + 'a> { + let syntax = file.syntax(); + let syntax = syntax.as_ref(); + + let comma = find_leaf_at_offset(syntax, offset).find(|leaf| leaf.kind() == COMMA)?; + let left = non_trivia_sibling(comma, Direction::Backward)?; + let right = non_trivia_sibling(comma, Direction::Forward)?; + Some(move || { + let mut edit = EditBuilder::new(); + edit.replace(left.range(), right.text()); + edit.replace(right.range(), left.text()); + edit.finish() + }) +} + +fn non_trivia_sibling(node: SyntaxNodeRef, direction: Direction) -> Option { + siblings(node, direction) + .skip(1) + .find(|node| !node.kind().is_trivia()) +} + + diff --git a/crates/libeditor/src/edit.rs b/crates/libeditor/src/edit.rs new file mode 100644 index 000000000..163ecf6de --- /dev/null +++ b/crates/libeditor/src/edit.rs @@ -0,0 +1,93 @@ +use {TextRange, TextUnit}; + +#[derive(Debug)] +pub struct Edit { + pub atoms: Vec, +} + +#[derive(Debug)] +pub struct AtomEdit { + pub delete: TextRange, + pub insert: String, +} + +#[derive(Debug)] +pub struct EditBuilder { + atoms: Vec +} + +impl EditBuilder { + pub fn new() -> EditBuilder { + EditBuilder { atoms: Vec::new() } + } + + pub fn replace(&mut self, range: TextRange, replacement: String) { + let range = self.translate(range); + self.atoms.push(AtomEdit { delete: range, insert: replacement }) + } + + pub fn delete(&mut self, range: TextRange) { + self.replace(range, String::new()); + } + + pub fn insert(&mut self, offset: TextUnit, text: String) { + self.replace(TextRange::offset_len(offset, 0.into()), text) + } + + pub fn finish(self) -> Edit { + Edit { atoms: self.atoms } + } + + fn translate(&self, range: TextRange) -> TextRange { + let mut range = range; + for atom in self.atoms.iter() { + range = atom.apply_to_range(range) + .expect("conflicting edits"); + } + range + } +} + +impl Edit { + pub fn apply(&self, text: &str) -> String { + let mut text = text.to_owned(); + for atom in self.atoms.iter() { + text = atom.apply(&text); + } + text + } +} + +impl AtomEdit { + fn apply(&self, text: &str) -> String { + let prefix = &text[ + TextRange::from_to(0.into(), self.delete.start()) + ]; + let suffix = &text[ + TextRange::from_to(self.delete.end(), TextUnit::of_str(text)) + ]; + let mut res = String::with_capacity(prefix.len() + self.insert.len() + suffix.len()); + res.push_str(prefix); + res.push_str(&self.insert); + res.push_str(suffix); + res + } + + fn apply_to_position(&self, pos: TextUnit) -> Option { + if pos <= self.delete.start() { + return Some(pos); + } + if pos < self.delete.end() { + return None; + } + Some(pos - self.delete.len() + TextUnit::of_str(&self.insert)) + } + + fn apply_to_range(&self, range: TextRange) -> Option { + Some(TextRange::from_to( + self.apply_to_position(range.start())?, + self.apply_to_position(range.end())?, + )) + } +} + diff --git a/crates/libeditor/src/lib.rs b/crates/libeditor/src/lib.rs index 013d27450..103f32190 100644 --- a/crates/libeditor/src/lib.rs +++ b/crates/libeditor/src/lib.rs @@ -1,9 +1,12 @@ extern crate libsyntax2; extern crate superslice; +extern crate itertools; mod extend_selection; mod symbols; mod line_index; +mod edit; +mod code_actions; use libsyntax2::{ ast::{self, NameOwner}, @@ -15,7 +18,9 @@ pub use libsyntax2::{File, TextRange, TextUnit}; pub use self::{ line_index::{LineIndex, LineCol}, extend_selection::extend_selection, - symbols::{FileSymbol, file_symbols} + symbols::{FileSymbol, file_symbols}, + edit::{EditBuilder, Edit}, + code_actions::{flip_comma}, }; #[derive(Debug)] diff --git a/crates/libeditor/tests/test.rs b/crates/libeditor/tests/test.rs index dedca49a4..369854fed 100644 --- a/crates/libeditor/tests/test.rs +++ b/crates/libeditor/tests/test.rs @@ -1,9 +1,16 @@ extern crate libeditor; +extern crate libsyntax2; extern crate itertools; +#[macro_use] +extern crate assert_eq_text; use std::fmt; use itertools::Itertools; -use libeditor::{File, highlight, runnables, extend_selection, TextRange, file_symbols}; +use libsyntax2::AstNode; +use libeditor::{ + File, TextUnit, TextRange, + highlight, runnables, extend_selection, file_symbols, flip_comma, +}; #[test] fn test_extend_selection() { @@ -27,13 +34,13 @@ fn main() {} "#); let hls = highlight(&file); dbg_eq( - &hls, r#"[HighlightedRange { range: [1; 11), tag: "comment" }, HighlightedRange { range: [12; 14), tag: "keyword" }, HighlightedRange { range: [15; 19), tag: "function" }, HighlightedRange { range: [29; 36), tag: "text" }, HighlightedRange { range: [38; 50), tag: "string" }, - HighlightedRange { range: [52; 54), tag: "literal" }]"# + HighlightedRange { range: [52; 54), tag: "literal" }]"#, + &hls, ); } @@ -51,10 +58,10 @@ fn test_foo() {} "#); let runnables = runnables(&file); dbg_eq( - &runnables, r#"[Runnable { range: [1; 13), kind: Bin }, Runnable { range: [15; 39), kind: Test { name: "test_foo" } }, Runnable { range: [41; 75), kind: Test { name: "test_foo" } }]"#, + &runnables, ) } @@ -76,7 +83,6 @@ const C: i32 = 92; "#); let symbols = file_symbols(&file); dbg_eq( - &symbols, r#"[FileSymbol { parent: None, name: "Foo", name_range: [8; 11), node_range: [1; 26), kind: STRUCT }, FileSymbol { parent: None, name: "m", name_range: [32; 33), node_range: [28; 53), kind: MODULE }, FileSymbol { parent: Some(1), name: "bar", name_range: [43; 46), node_range: [40; 51), kind: FUNCTION }, @@ -84,6 +90,19 @@ const C: i32 = 92; FileSymbol { parent: None, name: "T", name_range: [81; 82), node_range: [76; 88), kind: TYPE_ITEM }, FileSymbol { parent: None, name: "S", name_range: [96; 97), node_range: [89; 108), kind: STATIC_ITEM }, FileSymbol { parent: None, name: "C", name_range: [115; 116), node_range: [109; 127), kind: CONST_ITEM }]"#, + &symbols, + ) +} + +#[test] +fn test_swap_comma() { + check_modification( + "fn foo(x: i32,<|> y: Result<(), ()>) {}", + "fn foo(y: Result<(), ()>, x: i32) {}", + &|file, offset| { + let edit = flip_comma(file, offset).unwrap()(); + edit.apply(&file.syntax().text()) + }, ) } @@ -91,8 +110,27 @@ fn file(text: &str) -> File { File::parse(text) } -fn dbg_eq(actual: &impl fmt::Debug, expected: &str) { +fn dbg_eq(expected: &str, actual: &impl fmt::Debug) { let actual = format!("{:?}", actual); let expected = expected.lines().map(|l| l.trim()).join(" "); - assert_eq!(actual, expected); + assert_eq!(expected, actual); +} + +fn check_modification( + before: &str, + after: &str, + f: &impl Fn(&File, TextUnit) -> String, +) { + let cursor = "<|>"; + let cursor_pos = match before.find(cursor) { + None => panic!("before text should contain cursor marker"), + Some(pos) => pos, + }; + let mut text = String::with_capacity(before.len() - cursor.len()); + text.push_str(&before[..cursor_pos]); + text.push_str(&before[cursor_pos + cursor.len()..]); + let cursor_pos = TextUnit::from(cursor_pos as u32); + let file = file(&text); + let actual = f(&file, cursor_pos); + assert_eq_text!(after, &actual); } diff --git a/crates/libsyntax2/Cargo.toml b/crates/libsyntax2/Cargo.toml index 5a76ea82b..4c4040fe5 100644 --- a/crates/libsyntax2/Cargo.toml +++ b/crates/libsyntax2/Cargo.toml @@ -12,4 +12,4 @@ drop_bomb = "0.1.4" parking_lot = "0.6.0" [dev-dependencies] -difference = "2.0.0" +assert_eq_text = { path = "../assert_eq_text" } diff --git a/crates/libsyntax2/src/algo/mod.rs b/crates/libsyntax2/src/algo/mod.rs index 263b58d97..6efdff12f 100644 --- a/crates/libsyntax2/src/algo/mod.rs +++ b/crates/libsyntax2/src/algo/mod.rs @@ -74,7 +74,6 @@ impl<'f> Iterator for LeafAtOffset<'f> { } } - pub fn find_covering_node(root: SyntaxNodeRef, range: TextRange) -> SyntaxNodeRef { assert!(is_subrange(root.range(), range)); let (left, right) = match ( @@ -88,31 +87,33 @@ pub fn find_covering_node(root: SyntaxNodeRef, range: TextRange) -> SyntaxNodeRe common_ancestor(left, right) } -fn common_ancestor<'a>(n1: SyntaxNodeRef<'a>, n2: SyntaxNodeRef<'a>) -> SyntaxNodeRef<'a> { - for p in ancestors(n1) { - if ancestors(n2).any(|a| a == p) { - return p; - } - } - panic!("Can't find common ancestor of {:?} and {:?}", n1, n2) -} - pub fn ancestors<'a>(node: SyntaxNodeRef<'a>) -> impl Iterator> { - Ancestors(Some(node)) + generate(Some(node), |&node| node.parent()) } #[derive(Debug)] -struct Ancestors<'a>(Option>); +pub enum Direction { + Forward, + Backward, +} -impl<'a> Iterator for Ancestors<'a> { - type Item = SyntaxNodeRef<'a>; +pub fn siblings<'a>( + node: SyntaxNodeRef<'a>, + direction: Direction +) -> impl Iterator> { + generate(Some(node), move |&node| match direction { + Direction::Forward => node.next_sibling(), + Direction::Backward => node.prev_sibling(), + }) +} - fn next(&mut self) -> Option { - self.0.take().map(|n| { - self.0 = n.parent(); - n - }) +fn common_ancestor<'a>(n1: SyntaxNodeRef<'a>, n2: SyntaxNodeRef<'a>) -> SyntaxNodeRef<'a> { + for p in ancestors(n1) { + if ancestors(n2).any(|a| a == p) { + return p; + } } + panic!("Can't find common ancestor of {:?} and {:?}", n1, n2) } fn contains_offset_nonstrict(range: TextRange, offset: TextUnit) -> bool { @@ -122,3 +123,12 @@ fn contains_offset_nonstrict(range: TextRange, offset: TextUnit) -> bool { fn is_subrange(range: TextRange, subrange: TextRange) -> bool { range.start() <= subrange.start() && subrange.end() <= range.end() } + +fn generate(seed: Option, step: impl Fn(&T) -> Option) -> impl Iterator { + ::itertools::unfold(seed, move |slot| { + slot.take().map(|curr| { + *slot = step(&curr); + curr + }) + }) +} diff --git a/crates/libsyntax2/src/syntax_kinds/mod.rs b/crates/libsyntax2/src/syntax_kinds/mod.rs index ed4fa5d4d..332cd13ac 100644 --- a/crates/libsyntax2/src/syntax_kinds/mod.rs +++ b/crates/libsyntax2/src/syntax_kinds/mod.rs @@ -17,7 +17,7 @@ pub(crate) struct SyntaxInfo { } impl SyntaxKind { - pub(crate) fn is_trivia(self) -> bool { + pub fn is_trivia(self) -> bool { match self { WHITESPACE | COMMENT | DOC_COMMENT => true, _ => false, diff --git a/crates/libsyntax2/src/yellow/syntax.rs b/crates/libsyntax2/src/yellow/syntax.rs index a22275ed9..00f76e51c 100644 --- a/crates/libsyntax2/src/yellow/syntax.rs +++ b/crates/libsyntax2/src/yellow/syntax.rs @@ -101,6 +101,17 @@ impl SyntaxNode { }) } + pub fn prev_sibling(&self) -> Option> { + let red = self.red(); + let parent = self.parent()?; + let prev_sibling_idx = red.index_in_parent()?.checked_sub(1)?; + let sibling_red = parent.red().get_child(prev_sibling_idx)?; + Some(SyntaxNode { + root: self.root.clone(), + red: sibling_red, + }) + } + pub fn is_leaf(&self) -> bool { self.first_child().is_none() } diff --git a/crates/libsyntax2/tests/test/main.rs b/crates/libsyntax2/tests/test/main.rs index 18e5bc4d4..64d080dfd 100644 --- a/crates/libsyntax2/tests/test/main.rs +++ b/crates/libsyntax2/tests/test/main.rs @@ -1,5 +1,6 @@ extern crate libsyntax2; -extern crate difference; +#[macro_use] +extern crate assert_eq_text; use std::{ fs, @@ -7,8 +8,6 @@ use std::{ fmt::Write, }; -use difference::Changeset; - #[test] fn lexer_tests() { dir_tests(&["lexer"], |text| { @@ -63,10 +62,26 @@ pub fn dir_tests(paths: &[&str], f: F) } } +const REWRITE: bool = false; + fn assert_equal_text(expected: &str, actual: &str, path: &Path) { - if expected != actual { - print_difference(expected, actual, path) + if expected == actual { + return; + } + let dir = project_dir(); + let path = path.strip_prefix(&dir).unwrap_or_else(|_| path); + if expected.trim() == actual.trim() { + println!("whitespace difference, rewriting"); + println!("file: {}\n", path.display()); + fs::write(path, actual).unwrap(); + return; + } + if REWRITE { + println!("rewriting {}", path.display()); + fs::write(path, actual).unwrap(); + return; } + assert_eq_text!(expected, actual, "file: {}", path.display()); } fn collect_tests(paths: &[&str]) -> Vec { @@ -92,29 +107,6 @@ fn test_from_dir(dir: &Path) -> Vec { acc } -const REWRITE: bool = false; - -fn print_difference(expected: &str, actual: &str, path: &Path) { - let dir = project_dir(); - let path = path.strip_prefix(&dir).unwrap_or_else(|_| path); - if expected.trim() == actual.trim() { - println!("whitespace difference, rewriting"); - println!("file: {}\n", path.display()); - fs::write(path, actual).unwrap(); - return; - } - if REWRITE { - println!("rewriting {}", path.display()); - fs::write(path, actual).unwrap(); - return; - } - let changeset = Changeset::new(actual, expected, "\n"); - println!("Expected:\n{}\n\nActual:\n{}\n", expected, actual); - print!("{}", changeset); - println!("file: {}\n", path.display()); - panic!("Comparison failed") -} - fn project_dir() -> PathBuf { let dir = env!("CARGO_MANIFEST_DIR"); PathBuf::from(dir) -- cgit v1.2.3