///|
/// One edit enriched with source locations for display and transport.
pub struct EditPreview {
source : SourceId
source_name : String
span : Span
start : Location
end : Location
original : String
replacement : String
}
///|
pub fn EditPreview::source(self : EditPreview) -> SourceId {
self.source
}
///|
pub fn EditPreview::source_name(self : EditPreview) -> String {
self.source_name
}
///|
pub fn EditPreview::span(self : EditPreview) -> Span {
self.span
}
///|
pub fn EditPreview::start(self : EditPreview) -> Location {
self.start
}
///|
pub fn EditPreview::end(self : EditPreview) -> Location {
self.end
}
///|
pub fn EditPreview::original(self : EditPreview) -> String {
self.original
}
///|
pub fn EditPreview::replacement(self : EditPreview) -> String {
self.replacement
}
///|
pub fn EditPreview::is_insertion(self : EditPreview) -> Bool {
self.span.is_empty() && self.replacement.length() > 0
}
///|
pub fn EditPreview::is_deletion(self : EditPreview) -> Bool {
!self.span.is_empty() && self.replacement.length() == 0
}
///|
/// A validated, presentation-ready view of a fix.
pub struct FixPreview {
title : String
applicability : Applicability
edits : Array[EditPreview]
}
///|
pub fn FixPreview::title(self : FixPreview) -> String {
self.title
}
///|
pub fn FixPreview::applicability(self : FixPreview) -> Applicability {
self.applicability
}
///|
pub fn FixPreview::edits(self : FixPreview) -> Array[EditPreview] {
self.edits.copy()
}
///|
pub fn FixPreview::edit_count(self : FixPreview) -> Int {
self.edits.length()
}
///|
pub fn FixPreview::source_count(self : FixPreview) -> Int {
let seen : Array[SourceId] = []
for edit in self.edits {
if !seen.contains(edit.source()) {
seen.push(edit.source())
}
}
seen.length()
}
///|
/// Builds a preview only after the complete fix has passed validation.
pub fn Fix::preview(
self : Fix,
sources : SourceMap,
) -> (FixPreview?, FixValidation) {
let validation = self.validate(sources)
if !validation.is_valid() {
return (None, validation)
}
let previews : Array[EditPreview] = []
for edit in self.edits() {
let source = sources.get(edit.source()).unwrap()
previews.push({
source: edit.source(),
source_name: source.name(),
span: edit.span(),
start: source.location(edit.span().start()),
end: source.location(edit.span().end()),
original: source.slice(edit.span()),
replacement: edit.replacement(),
})
}
previews.sort_by(fn(left, right) {
let source_order = left.source().compare(right.source())
if source_order != 0 {
source_order
} else {
left.span().start().compare(right.span().start())
}
})
(
Some({
title: self.title(),
applicability: self.applicability(),
edits: previews,
}),
Valid,
)
}
///|
fn write_prefixed_lines(
output : StringBuilder,
prefix : Char,
text : String,
) -> Unit {
if text.length() == 0 {
output.write_char(prefix)
output.write_char('\n')
return
}
let lines = text.split("\n")
for line in lines {
output.write_char(prefix)
output.write_string(line.to_owned())
output.write_char('\n')
}
}
///|
fn write_preview_header(output : StringBuilder, edit : EditPreview) -> Unit {
output.write_string("@@ -")
output.write_string((edit.start().line() + 1).to_string())
output.write_char(':')
output.write_string((edit.start().column() + 1).to_string())
output.write_string(" +")
output.write_string((edit.start().line() + 1).to_string())
output.write_char(':')
output.write_string((edit.start().column() + 1).to_string())
output.write_string(" @@\n")
}
///|
/// Renders a compact unified-diff-like preview.
///
/// Locations are one-based for humans. The format is deliberately a preview
/// rather than a patch file: replacements may start or end mid-line.
pub fn render_fix_preview(preview : FixPreview) -> String {
let output = StringBuilder::new()
output.write_string("fix: ")
output.write_string(preview.title())
output.write_string(" [")
output.write_string(preview.applicability().name())
output.write_string("]\n")
let mut previous_source : SourceId? = None
for edit in preview.edits() {
if previous_source != Some(edit.source()) {
output.write_string("--- ")
output.write_string(edit.source_name())
output.write_char('\n')
output.write_string("+++ ")
output.write_string(edit.source_name())
output.write_string(" (fixed)\n")
previous_source = Some(edit.source())
}
write_preview_header(output, edit)
write_prefixed_lines(output, '-', edit.original())
write_prefixed_lines(output, '+', edit.replacement())
}
output.to_string()
}
///|
fn write_json_location(output : StringBuilder, location : Location) -> Unit {
output.write_string("{\"line\":")
output.write_string((location.line() + 1).to_string())
output.write_string(",\"column\":")
output.write_string((location.column() + 1).to_string())
output.write_string(",\"offset\":")
output.write_string(location.offset().to_string())
output.write_char('}')
}
///|
fn write_json_edit_preview(output : StringBuilder, edit : EditPreview) -> Unit {
output.write_string("{\"source_id\":")
output.write_string(edit.source().value().to_string())
output.write_string(",\"source_name\":")
write_json_string(output, edit.source_name())
output.write_string(",\"start\":")
write_json_location(output, edit.start())
output.write_string(",\"end\":")
write_json_location(output, edit.end())
output.write_string(",\"original\":")
write_json_string(output, edit.original())
output.write_string(",\"replacement\":")
write_json_string(output, edit.replacement())
output.write_char('}')
}
///|
/// Serializes a validated fix preview to stable JSON for editor integrations.
pub fn render_fix_json(preview : FixPreview) -> String {
let output = StringBuilder::new()
output.write_string("{\"title\":")
write_json_string(output, preview.title())
output.write_string(",\"applicability\":")
write_json_string(output, preview.applicability().name())
output.write_string(",\"source_count\":")
output.write_string(preview.source_count().to_string())
output.write_string(",\"edits\":[")
for index, edit in preview.edits() {
if index > 0 {
output.write_char(',')
}
write_json_edit_preview(output, edit)
}
output.write_string("]}")
output.to_string()
}
///|
/// Convenience operation for callers that want validation and JSON in one step.
pub fn Fix::render_json(
self : Fix,
sources : SourceMap,
) -> (String?, FixValidation) {
let (preview, validation) = self.preview(sources)
match preview {
Some(value) => (Some(render_fix_json(value)), validation)
None => (None, validation)
}
}
///|
/// Convenience operation for callers that want validation and text in one step.
pub fn Fix::render_preview(
self : Fix,
sources : SourceMap,
) -> (String?, FixValidation) {
let (preview, validation) = self.preview(sources)
match preview {
Some(value) => (Some(render_fix_preview(value)), validation)
None => (None, validation)
}
}