use std::ops::Range;
use sql_dialect_fmt_syntax::{SyntaxKind, SyntaxNode};
use crate::{format, FormatOptions};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RangeEdit {
pub range: Range<usize>,
pub new_text: String,
}
pub fn format_range(
source: &str,
target: Range<usize>,
options: &FormatOptions,
) -> Option<RangeEdit> {
let parse = sql_dialect_fmt_parser::parse_with_dialect(source, options.dialect);
if !parse.errors().is_empty() {
return None;
}
let statements = collect_statements(&parse.syntax());
let mut selected = statements
.iter()
.filter(|statement| statement.intersects(&target));
let first = selected.next()?;
let last = selected.next_back().unwrap_or(first);
let block_range = first.sig_start..last.full_end;
let block = source.get(block_range.clone())?;
let formatted = format(block, options);
let new_text = strip_one_trailing_newline(&formatted);
if new_text == block {
return None;
}
Some(RangeEdit {
range: block_range,
new_text: new_text.to_string(),
})
}
struct StatementSpan {
sig_start: usize,
full_end: usize,
}
impl StatementSpan {
fn intersects(&self, target: &Range<usize>) -> bool {
self.sig_start <= target.end && target.start <= self.full_end
}
}
fn collect_statements(root: &SyntaxNode) -> Vec<StatementSpan> {
let mut spans = Vec::new();
let mut pending: Option<(usize, usize)> = None; for element in root.children_with_tokens() {
if let Some(node) = element.as_node() {
if let Some((sig_start, node_end)) = pending.take() {
spans.push(StatementSpan {
sig_start,
full_end: node_end,
});
}
let node_start = usize::from(node.text_range().start());
let node_end = usize::from(node.text_range().end());
pending = Some((significant_start(node).unwrap_or(node_start), node_end));
} else if let Some(token) = element.as_token() {
if token.kind() == SyntaxKind::SEMICOLON {
if let Some((sig_start, _node_end)) = pending.take() {
spans.push(StatementSpan {
sig_start,
full_end: usize::from(token.text_range().end()),
});
}
}
}
}
if let Some((sig_start, node_end)) = pending.take() {
spans.push(StatementSpan {
sig_start,
full_end: node_end,
});
}
spans
}
fn significant_start(node: &SyntaxNode) -> Option<usize> {
node.descendants_with_tokens()
.filter_map(|element| element.into_token())
.find(|token| !token.kind().is_trivia())
.map(|token| usize::from(token.text_range().start()))
}
fn strip_one_trailing_newline(text: &str) -> &str {
text.strip_suffix("\r\n")
.or_else(|| text.strip_suffix('\n'))
.unwrap_or(text)
}
#[cfg(test)]
mod tests {
use super::*;
fn apply(source: &str, edit: &RangeEdit) -> String {
let mut out = String::new();
out.push_str(&source[..edit.range.start]);
out.push_str(&edit.new_text);
out.push_str(&source[edit.range.end..]);
out
}
fn range_format(source: &str, target: Range<usize>) -> Option<String> {
format_range(source, target, &FormatOptions::default()).map(|edit| apply(source, &edit))
}
#[test]
fn reformats_only_the_selected_statement() {
let source = "select 1 ;\n\n\nselect a,b from t;\n\nSELECT 3;\n";
let start = source.find("select a").unwrap();
let out = range_format(source, start..start + 5).expect("edit");
assert_eq!(out, "select 1 ;\n\n\nSELECT a, b\nFROM t;\n\nSELECT 3;\n");
assert!(out.starts_with("select 1 ;\n\n\n"));
assert!(out.ends_with("\n\nSELECT 3;\n"));
}
#[test]
fn keeps_same_line_trailing_comment_attached() {
let source = "select a,b from t; -- keep me\nselect 2;\n";
let out = range_format(source, 0..3).expect("edit");
assert_eq!(out, "SELECT a, b\nFROM t; -- keep me\nselect 2;\n");
}
#[test]
fn full_range_matches_whole_document_format() {
let source = "select 1;\nselect 2;\n";
let out = range_format(source, 0..source.len()).expect("edit");
assert_eq!(out, format(source, &FormatOptions::default()));
}
#[test]
fn cursor_between_statements_makes_no_edit() {
let source = "select 1;\n\nselect 2;\n";
let gap = source.find("\n\n").unwrap() + 1;
assert_eq!(
format_range(source, gap..gap, &FormatOptions::default()),
None
);
}
#[test]
fn already_formatted_selection_makes_no_edit() {
let source = "SELECT 1;\nSELECT a, b\nFROM t;\n";
assert_eq!(format_range(source, 0..8, &FormatOptions::default()), None);
}
#[test]
fn unparseable_source_makes_no_edit() {
let source = "ALTER TABLE t ADD COLUMN c INT;\n";
assert_eq!(format_range(source, 0..5, &FormatOptions::default()), None);
}
#[test]
fn range_formatting_is_idempotent() {
let source = "select 1 ;\n\nselect a,b from t;\n";
let start = source.find("select a").unwrap();
let once = range_format(source, start..start + 5).expect("edit");
let new_start = once.find("SELECT a").unwrap();
assert_eq!(
format_range(&once, new_start..new_start + 5, &FormatOptions::default()),
None
);
}
#[test]
fn selection_spanning_two_statements_reformats_both() {
let source = "select 1;select 2;\nSELECT 3;\n";
let out = range_format(source, 0..12).expect("edit");
assert_eq!(out, "SELECT 1;\nSELECT 2;\nSELECT 3;\n");
}
}