use std::panic::AssertUnwindSafe;
use std::path::Path;
use lsp_types::{Diagnostic, DiagnosticSeverity, Position, Range, TextEdit};
use rowan::TextRange;
use crate::formatter::{FormatStyle, RangeFormatted, format_node, format_range, format_with_style};
use crate::incremental::Analysis;
use crate::parser::{ParseDiagnostic, parse};
use crate::text::{LineIndex, PositionEncoding};
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;
}
let root = snapshot.parsed_tree(file);
let formatted = format_node(&root, style).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 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(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 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;
}
let root = snapshot.parsed_tree(file);
let text_range = lsp_range_to_text_range(text, range, encoding);
Some(match format_range(&root, text_range, style) {
Ok(Some(formatted)) => Some(edits_for_range_formatted(text, formatted, encoding)),
Ok(None) => Some(Vec::new()),
Err(_) => None,
})
}));
match cached {
Ok(Some(edits)) => edits,
Ok(None) | Err(_) => compute_format_range_edits(text, range, style, encoding),
}
}
pub fn compute_format_range_edits(
text: &str,
range: Range,
style: FormatStyle,
encoding: PositionEncoding,
) -> Option<Vec<TextEdit>> {
let root = parse(text).cst;
let text_range = lsp_range_to_text_range(text, range, encoding);
match format_range(&root, text_range, style).ok()? {
Some(formatted) => Some(edits_for_range_formatted(text, formatted, encoding)),
None => Some(Vec::new()),
}
}
pub(crate) fn edits_for_range_formatted(
text: &str,
formatted: 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();
}
let line_index = LineIndex::new(text);
vec![TextEdit {
range: Range {
start: line_index.byte_to_position(start, encoding),
end: line_index.byte_to_position(end, encoding),
},
new_text: formatted.text,
}]
}
fn lsp_range_to_text_range(text: &str, range: Range, encoding: PositionEncoding) -> TextRange {
let line_index = LineIndex::new(text);
let start = line_index.position_to_byte(range.start, encoding);
let end = line_index.position_to_byte(range.end, encoding);
TextRange::new(
(start.min(end) as u32).into(),
(start.max(end) as u32).into(),
)
}
pub(crate) fn parse_diagnostics_to_lsp(
diagnostics: &[ParseDiagnostic],
text: &str,
encoding: PositionEncoding,
) -> Vec<Diagnostic> {
let line_index = LineIndex::new(text);
diagnostics
.iter()
.map(|diag| Diagnostic {
range: Range::new(
line_index.byte_to_position(diag.start, encoding),
line_index.byte_to_position(diag.end, encoding),
),
severity: Some(DiagnosticSeverity::ERROR),
source: Some("fatou".to_string()),
message: diag.message.clone(),
..Default::default()
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::incremental::IncrementalDatabase;
use std::path::Path;
#[test]
fn format_via_db_matches_compute_and_falls_back() {
let style = FormatStyle::default();
let encoding = PositionEncoding::Utf16;
let path = Path::new("/work/a.jl");
let buffer = "x=f( 1 )\n";
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"
);
}
#[test]
fn format_range_via_db_matches_compute_and_falls_back() {
let style = FormatStyle::default();
let encoding = PositionEncoding::Utf16;
let path = Path::new("/work/a.jl");
let buffer = "a=1\nx=f( 1 )\nb =2\n";
let range = Range::new(Position::new(1, 3), Position::new(1, 3));
let expected = compute_format_range_edits(buffer, range, style, encoding);
assert!(
matches!(&expected, Some(edits) if edits.len() == 1),
"fixture must require a scoped edit"
);
let mut db = IncrementalDatabase::default();
db.upsert_file(path, buffer.to_string());
let snapshot = db.snapshot();
assert_eq!(
format_range_edits_via_db(&snapshot, path, buffer, range, style, encoding),
expected,
"cached-tree range format must match the re-parse path"
);
let mut stale = IncrementalDatabase::default();
stale.upsert_file(path, "y = 1\n".to_string());
assert_eq!(
format_range_edits_via_db(&stale.snapshot(), path, buffer, range, style, encoding),
expected,
"version skew must fall back to the buffer text"
);
let empty = IncrementalDatabase::default();
assert_eq!(
format_range_edits_via_db(&empty.snapshot(), path, buffer, range, style, encoding),
expected,
"untracked path must fall back to the buffer text"
);
}
#[test]
fn edit_end_position_follows_encoding() {
let text = "x = \"\u{1F600}\"";
let formatted = "y".to_string();
let end_utf16 = edits_for_formatted(text, formatted.clone(), PositionEncoding::Utf16)[0]
.range
.end;
let end_utf8 = edits_for_formatted(text, formatted, PositionEncoding::Utf8)[0]
.range
.end;
assert_eq!(end_utf16, Position::new(0, 8));
assert_eq!(end_utf8, Position::new(0, 10));
}
}