use super::*;
pub(crate) fn format_edits_via_db(
snapshot: &Analysis,
path: &Path,
text: &str,
style: FormatStyle,
) -> Option<Vec<TextEdit>> {
let cached = salsa::Cancelled::catch(AssertUnwindSafe(|| {
let file = snapshot.lookup_file(path)?;
if snapshot.file_text(file) != text {
return None;
}
if !snapshot.parse_diagnostics(file).is_empty() {
return Some(None);
}
let root = snapshot.parsed_tree(file);
let formatted = format_node(&root, style, text.ends_with('\n')).ok();
Some(formatted.map(|formatted| edits_for_formatted(text, formatted)))
}));
match cached {
Ok(Some(edits)) => edits,
Ok(None) | Err(_) => compute_format_edits(text, style),
}
}
pub(crate) fn format_range_edits_via_db(
snapshot: &Analysis,
path: &Path,
text: &str,
range: Range,
style: FormatStyle,
) -> Option<Vec<TextEdit>> {
let cached = salsa::Cancelled::catch(AssertUnwindSafe(|| {
let file = snapshot.lookup_file(path)?;
if snapshot.file_text(file) != text {
return None;
}
if !snapshot.parse_diagnostics(file).is_empty() {
return Some(None);
}
let root = snapshot.parsed_tree(file);
let line_index = LineIndex::new(text);
let text_range = lsp_range_to_text_range(&line_index, range);
let edits = match format_range(&root, text_range, style) {
Ok(Some(formatted)) => Some(range_edits(&line_index, text, formatted)),
Ok(None) => Some(Vec::new()),
Err(_) => None,
};
Some(edits)
}));
match cached {
Ok(Some(edits)) => edits,
Ok(None) | Err(_) => compute_format_range_edits(text, range, style),
}
}
pub fn compute_format_edits(text: &str, style: FormatStyle) -> Option<Vec<TextEdit>> {
let formatted = format_with_style(text, style).ok()?;
Some(edits_for_formatted(text, formatted))
}
pub fn compute_format_range_edits(
text: &str,
range: Range,
style: FormatStyle,
) -> Option<Vec<TextEdit>> {
let parsed = parse(text);
if !parsed.diagnostics.is_empty() {
return None;
}
let line_index = LineIndex::new(text);
let text_range = lsp_range_to_text_range(&line_index, range);
match format_range(&parsed.cst, text_range, style).ok()? {
Some(formatted) => Some(range_edits(&line_index, text, formatted)),
None => Some(Vec::new()),
}
}
pub(crate) fn text_range_to_lsp_range(line_index: &LineIndex, range: TextRange) -> Range {
Range {
start: line_index.byte_to_position(u32::from(range.start()) as usize),
end: line_index.byte_to_position(u32::from(range.end()) as usize),
}
}
pub(crate) fn lsp_range_to_text_range(line_index: &LineIndex, range: Range) -> TextRange {
let start = line_index.position_to_byte(range.start);
let end = line_index.position_to_byte(range.end);
TextRange::new(
TextSize::new(start as u32),
TextSize::new(start.max(end) as u32),
)
}
pub(crate) fn range_edits(
line_index: &LineIndex,
text: &str,
formatted: crate::formatter::RangeFormatted,
) -> Vec<TextEdit> {
let start = usize::from(formatted.range.start());
let end = usize::from(formatted.range.end());
if text.get(start..end) == Some(formatted.text.as_str()) {
return Vec::new();
}
vec![TextEdit {
range: Range {
start: line_index.byte_to_position(start),
end: line_index.byte_to_position(end),
},
new_text: formatted.text,
}]
}
pub(crate) fn edits_for_formatted(text: &str, formatted: String) -> Vec<TextEdit> {
if formatted == text {
return Vec::new();
}
let line_index = LineIndex::new(text);
let end = line_index.byte_to_position(text.len());
vec![TextEdit {
range: Range {
start: Position::new(0, 0),
end,
},
new_text: formatted,
}]
}
pub(crate) fn to_lsp_diagnostic(d: &Diagnostic, idx: &LineIndex) -> LspDiagnostic {
let start = idx.byte_to_position(u32::from(d.range.start()) as usize);
let end = idx.byte_to_position(u32::from(d.range.end()) as usize);
let severity = match d.severity {
Severity::Error => DiagnosticSeverity::ERROR,
Severity::Warning => DiagnosticSeverity::WARNING,
Severity::Info => DiagnosticSeverity::INFORMATION,
Severity::Hint => DiagnosticSeverity::HINT,
};
LspDiagnostic {
range: Range { start, end },
severity: Some(severity),
code: Some(NumberOrString::String(d.rule.to_string())),
source: Some("arity".to_string()),
message: d.message.body.clone(),
..Default::default()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn format_via_db_matches_compute_and_falls_back() {
use crate::incremental::IncrementalDatabase;
let style = FormatStyle::default();
let path = test_path();
let buffer = "x<-f(1 )\n";
let expected = compute_format_edits(buffer, style);
assert!(
matches!(&expected, Some(edits) if !edits.is_empty()),
"fixture must require reformatting"
);
let mut db = IncrementalDatabase::default();
db.upsert_file(path, buffer.to_string());
let snapshot = db.snapshot();
assert_eq!(
format_edits_via_db(&snapshot, path, buffer, style),
expected,
"cached-tree format must match the re-parse path"
);
let mut stale = IncrementalDatabase::default();
stale.upsert_file(path, "y <- 1\n".to_string());
assert_eq!(
format_edits_via_db(&stale.snapshot(), path, buffer, style),
expected,
"version skew must fall back to the buffer text"
);
let empty = IncrementalDatabase::default();
assert_eq!(
format_edits_via_db(&empty.snapshot(), path, buffer, style),
expected,
"untracked path must fall back to the buffer text"
);
}
}