use super::*;
pub(crate) fn format_edits_via_db(
snapshot: &Analysis,
path: &Path,
text: &str,
style: FormatStyle,
encoding: PositionEncoding,
) -> 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).ok();
Some(formatted.map(|formatted| edits_for_formatted(text, formatted, encoding)))
}));
match cached {
Ok(Some(edits)) => edits,
Ok(None) | Err(_) => compute_format_edits(text, style, encoding),
}
}
pub(crate) fn format_range_edits_via_db(
snapshot: &Analysis,
path: &Path,
text: &str,
range: Range,
style: FormatStyle,
encoding: PositionEncoding,
) -> 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, encoding);
let edits = match format_range(&root, text_range, style, text) {
Ok(Some(formatted)) => Some(range_edits(&line_index, text, formatted, encoding)),
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, encoding),
}
}
pub fn compute_format_edits(
text: &str,
style: FormatStyle,
encoding: PositionEncoding,
) -> Option<Vec<TextEdit>> {
let formatted = format_with_style(text, style).ok()?;
Some(edits_for_formatted(text, formatted, encoding))
}
pub fn compute_format_range_edits(
text: &str,
range: Range,
style: FormatStyle,
encoding: PositionEncoding,
) -> 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, encoding);
match format_range(&parsed.cst, text_range, style, text).ok()? {
Some(formatted) => Some(range_edits(&line_index, text, formatted, encoding)),
None => Some(Vec::new()),
}
}
pub(crate) fn text_range_to_lsp_range(
line_index: &LineIndex,
range: TextRange,
encoding: PositionEncoding,
) -> Range {
Range {
start: line_index.byte_to_position(u32::from(range.start()) as usize, encoding),
end: line_index.byte_to_position(u32::from(range.end()) as usize, encoding),
}
}
pub(crate) fn lsp_range_to_text_range(
line_index: &LineIndex,
range: Range,
encoding: PositionEncoding,
) -> TextRange {
let start = line_index.position_to_byte(range.start, encoding);
let end = line_index.position_to_byte(range.end, encoding);
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,
encoding: PositionEncoding,
) -> 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, encoding),
end: line_index.byte_to_position(end, encoding),
},
new_text: formatted.text,
}]
}
pub(crate) fn edits_for_formatted(
text: &str,
formatted: String,
encoding: PositionEncoding,
) -> Vec<TextEdit> {
if formatted == text {
return Vec::new();
}
let line_index = LineIndex::new(text);
let end = line_index.byte_to_position(text.len(), encoding);
vec![TextEdit {
range: Range {
start: Position::new(0, 0),
end,
},
new_text: formatted,
}]
}
pub(crate) fn to_lsp_diagnostic(
d: &Diagnostic,
idx: &LineIndex,
encoding: PositionEncoding,
) -> LspDiagnostic {
let start = idx.byte_to_position(u32::from(d.range.start()) as usize, encoding);
let end = idx.byte_to_position(u32::from(d.range.end()) as usize, encoding);
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()
}
}
pub(crate) fn findings_to_items(
findings: &[Diagnostic],
text: &str,
encoding: PositionEncoding,
) -> Vec<LspDiagnostic> {
let idx = LineIndex::new(text);
findings
.iter()
.map(|d| to_lsp_diagnostic(d, &idx, encoding))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn findings_to_items_maps_range_severity_and_code() {
use crate::linter::ViolationData;
let text = "line0\nWARN\n";
let findings = vec![Diagnostic {
rule: "demo-rule",
severity: Severity::Warning,
path: test_path().to_path_buf(),
range: TextRange::new(TextSize::from(6), TextSize::from(10)),
message: ViolationData::new("demo-rule", "a demo finding"),
fix: None,
}];
let items = findings_to_items(&findings, text, PositionEncoding::Utf16);
assert_eq!(items.len(), 1);
let item = &items[0];
assert_eq!(item.range.start, Position::new(1, 0));
assert_eq!(item.range.end, Position::new(1, 4));
assert_eq!(item.severity, Some(DiagnosticSeverity::WARNING));
assert_eq!(item.source.as_deref(), Some("arity"));
assert_eq!(item.message, "a demo finding");
assert!(matches!(&item.code, Some(NumberOrString::String(c)) if c == "demo-rule"));
}
#[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 encoding = PositionEncoding::Utf16;
let expected = compute_format_edits(buffer, style, encoding);
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, encoding),
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, encoding),
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, encoding),
expected,
"untracked path must fall back to the buffer text"
);
}
}