use datafusion::sql::sqlparser::tokenizer::Location;
use crate::parser::byte_offset;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Edit {
output: (usize, usize),
input: (usize, usize),
}
#[derive(Debug, Default)]
pub(crate) struct Rewritten {
pub text: String,
edits: Vec<Edit>,
copied: usize,
}
impl Rewritten {
pub fn new(capacity: usize) -> Self {
Self {
text: String::with_capacity(capacity),
..Self::default()
}
}
pub fn replace(&mut self, input: &str, start: usize, end: usize, with: &str) {
self.text.push_str(&input[self.copied..start]);
let output_start = self.text.len();
self.text.push_str(with);
self.edits.push(Edit {
output: (output_start, self.text.len()),
input: (start, end),
});
self.copied = end;
}
pub fn finish(mut self, input: &str) -> Self {
self.text.push_str(&input[self.copied..]);
self.copied = input.len();
self
}
fn input_offset(&self, at: usize) -> usize {
match self.edits.iter().rev().find(|e| e.output.0 <= at) {
None => at,
Some(e) if at < e.output.1 => e.input.0,
Some(e) => at - e.output.1 + e.input.1,
}
}
}
#[derive(Debug, Default)]
pub struct SourceMap {
original: String,
steps: Vec<Rewritten>,
}
impl SourceMap {
pub(crate) fn new(original: &str) -> Self {
Self {
original: original.to_string(),
steps: Vec::new(),
}
}
pub(crate) fn push(&mut self, step: Rewritten) {
self.steps.push(step);
}
pub(crate) fn original(&self) -> &str {
&self.original
}
pub(crate) fn text(&self) -> &str {
self.steps
.last()
.map_or(self.original.as_str(), |s| s.text.as_str())
}
fn changed(&self) -> bool {
self.steps.iter().any(|s| !s.edits.is_empty())
}
fn location(&self, at: Location) -> Option<Location> {
let mut offset = byte_offset(self.text(), at)?;
for step in self.steps.iter().rev() {
offset = step.input_offset(offset);
}
Some(location_of(&self.original, offset))
}
pub fn user_message(&self, message: &str) -> String {
const LINE: &str = "Line: ";
const COLUMN: &str = ", Column: ";
if !self.changed() {
return message.to_string();
}
let mut out = String::with_capacity(message.len());
let mut rest = message;
while let Some(at) = rest.find(LINE) {
let (before, after) = rest.split_at(at);
out.push_str(before);
let after_line = &after[LINE.len()..];
let line_digits = digits(after_line);
let after_digits = &after_line[line_digits..];
let parsed = after_digits.strip_prefix(COLUMN).and_then(|c| {
let column_digits = digits(c);
let line = after_line[..line_digits].parse().ok()?;
let column = c[..column_digits].parse().ok()?;
Some((line, column, &c[column_digits..]))
});
match parsed.and_then(|(line, column, tail)| {
Some((self.location(Location::new(line, column))?, tail))
}) {
Some((location, tail)) => {
out.push_str(&format!(
"{LINE}{}{COLUMN}{}",
location.line, location.column
));
rest = tail;
}
None => {
out.push_str(LINE);
rest = after_line;
}
}
}
out.push_str(rest);
out
}
}
fn digits(s: &str) -> usize {
s.len() - s.trim_start_matches(|c: char| c.is_ascii_digit()).len()
}
fn location_of(sql: &str, offset: usize) -> Location {
let offset = offset.min(sql.len());
let before = &sql[..offset];
let line = before.matches('\n').count() + 1;
let line_start = before.rfind('\n').map_or(0, |i| i + 1);
let column = sql[line_start..offset].chars().count() + 1;
Location::new(line as u64, column as u64)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn locations_map_through_replacements() {
let input = "ab\nFROM t WHERE x";
let mut step = Rewritten::new(0);
step.replace(input, 3, 3, "SELECT * ");
let step = step.finish(input);
assert_eq!(step.text, "ab\nSELECT * FROM t WHERE x");
let mut map = SourceMap::new(input);
map.push(step);
assert_eq!(
map.user_message("found: WHERE at Line: 2, Column: 17"),
"found: WHERE at Line: 2, Column: 8"
);
assert_eq!(
map.user_message("found: * at Line: 2, Column: 8 and Line: 1, Column: 2"),
"found: * at Line: 2, Column: 1 and Line: 1, Column: 2"
);
assert_eq!(map.user_message("Line: x"), "Line: x");
}
#[test]
fn two_rewrites_compose() {
let input = "FROM GRAPH_TABLE (g MATCH (a)) x WHERE";
let mut first = Rewritten::new(0);
first.replace(input, 5, 30, "graph_table('g MATCH (a)')");
let first = first.finish(input);
let middle = first.text.clone();
let mut second = Rewritten::new(0);
second.replace(&middle, 0, 0, "SELECT * ");
let second = second.finish(&middle);
assert_eq!(
second.text,
"SELECT * FROM graph_table('g MATCH (a)') x WHERE"
);
let mut map = SourceMap::new(input);
map.push(first);
map.push(second);
let at = second_column(&map, "WHERE");
assert_eq!(
map.user_message(&format!("at Line: 1, Column: {at}")),
format!("at Line: 1, Column: {}", input.find("WHERE").unwrap() + 1)
);
}
fn second_column(map: &SourceMap, word: &str) -> usize {
map.text().find(word).unwrap() + 1
}
}