use text_size::{TextRange, TextSize};
use crate::config::FormatConfig;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TextEdit {
pub range: TextRange,
pub new_text: String,
}
#[must_use]
pub fn format_edits(source: &str, config: &FormatConfig) -> Vec<TextEdit> {
let formatted = crate::format::format(source, config);
diff_edits(source, &formatted)
}
#[must_use]
pub fn diff_edits(source: &str, formatted: &str) -> Vec<TextEdit> {
minimal_diff(source, formatted)
}
#[must_use]
pub fn format_range(source: &str, config: &FormatConfig, range: TextRange) -> Vec<TextEdit> {
format_edits(source, config)
.into_iter()
.filter(|e| e.range.start() <= range.end() && range.start() <= e.range.end())
.collect()
}
fn minimal_diff(before: &str, after: &str) -> Vec<TextEdit> {
if before == after {
return Vec::new();
}
let prefix = common_prefix(before, after);
let suffix = common_suffix(&before[prefix..], &after[prefix..]);
let before_end = before.len() - suffix;
let after_end = after.len() - suffix;
vec![TextEdit {
range: TextRange::new(
TextSize::from(prefix as u32),
TextSize::from(before_end as u32),
),
new_text: after[prefix..after_end].to_string(),
}]
}
fn common_prefix(a: &str, b: &str) -> usize {
let max = a.len().min(b.len());
let mut i = 0;
while i < max && a.as_bytes()[i] == b.as_bytes()[i] {
i += 1;
}
while i > 0 && !a.is_char_boundary(i) {
i -= 1;
}
i
}
fn common_suffix(a: &str, b: &str) -> usize {
let max = a.len().min(b.len());
let mut i = 0;
while i < max && a.as_bytes()[a.len() - 1 - i] == b.as_bytes()[b.len() - 1 - i] {
i += 1;
}
while i > 0 && !a.is_char_boundary(a.len() - i) {
i -= 1;
}
i
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn already_formatted_yields_no_edits() {
let cfg = FormatConfig::sqlstyle();
let formatted = crate::format::format("select 1", &cfg);
assert!(format_edits(&formatted, &cfg).is_empty());
}
#[test]
fn edit_applies_to_produce_formatted_output() {
let cfg = FormatConfig::sqlstyle();
let src = "select id,name from users";
let edits = format_edits(src, &cfg);
assert_eq!(edits.len(), 1);
let edit = &edits[0];
let start = u32::from(edit.range.start()) as usize;
let end = u32::from(edit.range.end()) as usize;
let mut out = src.to_string();
out.replace_range(start..end, &edit.new_text);
assert_eq!(out, crate::format::format(src, &cfg));
}
#[test]
fn range_outside_change_yields_nothing() {
let cfg = FormatConfig::sqlstyle();
let src = "select id,name from users";
let full = format_edits(src, &cfg);
let change = full[0].range;
let far = TextRange::empty(TextSize::from(u32::from(change.end()) + 5));
assert!(format_range(src, &cfg, far).is_empty());
}
}