use std::{cmp::Reverse, fmt, ops::RangeInclusive};
use proc_macro2::{LineColumn, Span};
use rangemap::RangeInclusiveMap;
use ropey::Rope;
use syn::{parse::Parse, spanned::Spanned};
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct Editor {
source_code_string: String,
source_code_rope: Rope,
edits: RangeInclusiveMap<usize, Operation>,
}
impl Editor {
pub fn new_with_ast<T: Parse>(source_code: &str) -> syn::Result<(Self, T)> {
syn::parse_str::<T>(source_code).map(|ast| (Self::new(source_code), ast))
}
pub fn new(source_code: &str) -> Self {
Self {
source_code_string: String::from(source_code),
source_code_rope: Rope::from_str(source_code),
edits: RangeInclusiveMap::new(),
}
}
pub fn replace(&mut self, node: impl Spanned, text: impl Into<String>) -> &mut Self {
self.queue_edit_at(node.span(), Operation::Replace(text.into()))
}
pub fn prepend(&mut self, node: impl Spanned, text: impl Into<String>) -> &mut Self {
self.queue_edit_at(node.span(), Operation::Prepend(text.into()))
}
pub fn append(&mut self, node: impl Spanned, text: impl Into<String>) -> &mut Self {
self.queue_edit_at(node.span(), Operation::Append(text.into()))
}
pub fn remove(&mut self, node: impl Spanned) -> &mut Self {
self.queue_edit_at(node.span(), Operation::Remove)
}
pub fn source(&self) -> &str {
&self.source_code_string
}
pub fn source_at(&self, span: impl Spanned) -> &str {
let span = span2range(&self.source_code_rope, span.span());
let start = self.source_code_rope.char_to_byte(*span.start());
let end = self.source_code_rope.char_to_byte(*span.end());
&self.source_code_string[start..end]
}
pub fn finish(&self) -> String {
let mut edits = self.edits.iter().collect::<Vec<_>>();
edits.sort_by_key(|(range, _op)| Reverse(*range.start()));
let mut source_code = self.source_code_rope.clone();
for (range, operation) in edits {
apply(&mut source_code, range, operation)
}
source_code.into()
}
pub fn len(&self) -> usize {
self.edits.len()
}
pub fn is_empty(&self) -> bool {
self.edits.is_empty()
}
pub fn is_empty_at(&self, span: impl Spanned) -> bool {
let span = span2range(&self.source_code_rope, span.span());
self.edits.overlaps(&span)
}
fn queue_edit_at(&mut self, span: Span, operation: Operation) -> &mut Self {
let num_lines = self.source_code_rope.len_lines();
assert!(
num_lines >= span.end().line,
"span exceeds end of file. span is {} but there are {} lines",
FmtSpan(span),
num_lines
);
let range = span2range(&self.source_code_rope, span);
assert!(
!self.edits.overlaps(&range),
"an edit has already been made in the range {}",
FmtSpan(span)
);
if let Some(reported) = span.source_text() {
let internal = self.source_code_rope.slice(range.clone());
assert_eq!(
internal, reported,
"the span's reported source text does not match our calculated source text"
)
}
self.edits.insert(range, operation);
self
}
}
fn span2range(txt: &Rope, span: Span) -> RangeInclusive<usize> {
let start = span.start();
assert_column(txt, start, span);
let start = txt.line_to_char(start.line - 1) + start.column;
let end = span.end();
assert_column(txt, end, span);
let end = txt.line_to_char(end.line - 1) + end.column - 1;
start..=end
}
fn assert_column(txt: &Rope, coord: LineColumn, span: Span) {
let num_cols = txt.line(coord.line - 1).len_chars();
assert!(
num_cols >= coord.column,
"span exceeds end of line. span is {} but there are {} columns",
FmtSpan(span),
num_cols
);
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Hash)]
enum Operation {
Append(String),
Prepend(String),
Replace(String),
Remove,
}
fn apply(rope: &mut Rope, range: &RangeInclusive<usize>, operation: &Operation) {
match operation {
Operation::Prepend(it) => rope.insert(*range.start(), it),
Operation::Append(it) => rope.insert(range.end() + 1, it),
Operation::Replace(it) => {
rope.remove(range.clone());
rope.insert(*range.start(), it)
}
Operation::Remove => rope.remove(range.clone()),
}
}
struct FmtSpan(Span);
impl fmt::Display for FmtSpan {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let start = self.0.start();
let end = self.0.end();
f.write_fmt(format_args!(
"{}:{}..{}:{}",
start.line, start.column, end.line, end.column
))
}
}
#[cfg(test)]
mod tests {
use super::*;
use indoc::indoc;
use pretty_assertions::assert_str_eq;
#[test]
fn readme() {
assert!(
std::process::Command::new("cargo")
.args(["rdme", "--check"])
.output()
.expect("couldn't run `cargo rdme`")
.status
.success(),
"README.md is out of date - bless the new version by running `cargo rdme`"
)
}
#[test]
fn single_line_single_edit() {
let source_code = "const FOO: () = ();";
let ast = syn::parse_str::<syn::ItemConst>(source_code).unwrap();
let mut synsert = Editor::new(source_code);
synsert.queue_edit_at(ast.ident.span(), Operation::Replace(String::from("BAR")));
assert_str_eq!(synsert.finish(), "const BAR: () = ();");
let mut synsert = Editor::new(source_code);
synsert.queue_edit_at(ast.ident.span(), Operation::Prepend(String::from("TO")));
assert_str_eq!(synsert.finish(), "const TOFOO: () = ();");
let mut synsert = Editor::new(source_code);
synsert.queue_edit_at(ast.ident.span(), Operation::Append(String::from("BAR")));
assert_str_eq!(synsert.finish(), "const FOOBAR: () = ();");
let mut synsert = Editor::new(source_code);
synsert.queue_edit_at(ast.ident.span(), Operation::Remove);
assert_str_eq!(synsert.finish(), "const : () = ();");
}
#[test]
fn single_line_single_edit_multibyte() {
let source_code = "const 𓀕: () = ();";
let ast = syn::parse_str::<syn::ItemConst>(source_code).unwrap();
let mut synsert = Editor::new(source_code);
synsert.queue_edit_at(ast.expr.span(), Operation::Replace(String::from("ð“€ ")));
assert_str_eq!(synsert.finish(), "const 𓀕: () = 𓀠;");
let mut synsert = Editor::new(source_code);
synsert.queue_edit_at(ast.ident.span(), Operation::Prepend(String::from("ð“€ ")));
assert_str_eq!(synsert.finish(), "const 𓀠𓀕: () = ();");
let mut synsert = Editor::new(source_code);
synsert.queue_edit_at(ast.ident.span(), Operation::Append(String::from("ð“€ ")));
assert_str_eq!(synsert.finish(), "const 𓀕𓀠: () = ();");
let mut synsert = Editor::new(source_code);
synsert.queue_edit_at(ast.ty.span(), Operation::Remove);
assert_str_eq!(synsert.finish(), "const 𓀕: = ();");
}
#[test]
fn single_line_multi_edit() {
let source_code = "const FOO: () = ();";
let ast = syn::parse_str::<syn::ItemConst>(source_code).unwrap();
let mut synsert = Editor::new(source_code);
synsert.replace(ast.expr.span(), "make_bar()");
synsert.replace(ast.ident.span(), "BAR");
assert_str_eq!(synsert.finish(), "const BAR: () = make_bar();");
let mut synsert = Editor::new(source_code);
synsert.prepend(ast.expr.span(), "make_foo_bar");
synsert.append(ast.ident.span(), "_BAR");
assert_str_eq!(synsert.finish(), "const FOO_BAR: () = make_foo_bar();");
}
#[test]
fn multi_line_single_edit() {
let source_code = indoc! {"
const FOO: () = {
do_stuff();
do_more_stuff();
};"
};
let ast = syn::parse_str::<syn::ItemConst>(source_code).unwrap();
let mut synsert = Editor::new(source_code);
synsert.replace(ast.expr.span(), "make_foo()");
assert_str_eq!(synsert.finish(), "const FOO: () = make_foo();");
let source_code = indoc! {"
const FOOD: () = ();
const FOO: () = {
do_stuff();
do_more_stuff();
};
const FOO_FIGHTERS: () = ();"
};
let ast = syn::parse_str::<syn::File>(source_code).unwrap();
let mut synsert = Editor::new(source_code);
synsert.replace(ast.items[1].span(), "const BAR: () = ();");
assert_str_eq!(
synsert.finish(),
indoc! {"
const FOOD: () = ();
const BAR: () = ();
const FOO_FIGHTERS: () = ();"
}
);
}
}