///|
/// How confidently an automated edit can be applied.
pub(all) enum Applicability {
/// The replacement is semantics-preserving and safe for unattended use.
MachineApplicable
/// The replacement is normally correct but may require user review.
MaybeIncorrect
/// The replacement contains explanatory placeholders.
HasPlaceholders
/// The edit is a suggestion that requires a human decision.
Unspecified
} derive(Eq, Debug)
///|
pub fn Applicability::name(self : Applicability) -> String {
match self {
MachineApplicable => "machine-applicable"
MaybeIncorrect => "maybe-incorrect"
HasPlaceholders => "has-placeholders"
Unspecified => "unspecified"
}
}
///|
pub fn Applicability::is_automatic(self : Applicability) -> Bool {
self == MachineApplicable
}
///|
/// One UTF-8 byte-range replacement in a registered source.
pub struct TextEdit {
source : SourceId
span : Span
replacement : String
} derive(Eq, Debug)
///|
pub fn TextEdit::replace(
source : SourceId,
span : Span,
replacement : String,
) -> TextEdit {
{ source, span, replacement }
}
///|
pub fn TextEdit::insert(
source : SourceId,
offset : Int,
text : String,
) -> TextEdit? {
match Span::new(offset, offset) {
Some(span) => Some({ source, span, replacement: text })
None => None
}
}
///|
pub fn TextEdit::delete(source : SourceId, span : Span) -> TextEdit {
{ source, span, replacement: "" }
}
///|
pub fn TextEdit::source(self : TextEdit) -> SourceId {
self.source
}
///|
pub fn TextEdit::span(self : TextEdit) -> Span {
self.span
}
///|
pub fn TextEdit::replacement(self : TextEdit) -> String {
self.replacement
}
///|
pub fn TextEdit::is_insertion(self : TextEdit) -> Bool {
self.span.is_empty() && self.replacement.length() > 0
}
///|
pub fn TextEdit::is_deletion(self : TextEdit) -> Bool {
!self.span.is_empty() && self.replacement.length() == 0
}
///|
/// The outcome of checking a fix against a source registry.
pub(all) enum FixValidation {
Valid
Empty
UnknownSource(SourceId)
OutOfBounds(SourceId, Span, Int)
Overlapping(SourceId, Span, Span)
} derive(Eq, Debug)
///|
pub fn FixValidation::is_valid(self : FixValidation) -> Bool {
self == Valid
}
///|
pub fn FixValidation::message(self : FixValidation) -> String {
match self {
Valid => "valid"
Empty => "fix contains no edits"
UnknownSource(id) => "unknown source " + id.value().to_string()
OutOfBounds(id, span, length) =>
"edit " +
span.start().to_string() +
".." +
span.end().to_string() +
" exceeds source " +
id.value().to_string() +
" length " +
length.to_string()
Overlapping(id, left, right) =>
"overlapping edits in source " +
id.value().to_string() +
": " +
left.start().to_string() +
".." +
left.end().to_string() +
" and " +
right.start().to_string() +
".." +
right.end().to_string()
}
}
///|
/// A named group of edits that should be accepted or rejected together.
pub struct Fix {
title : String
applicability : Applicability
edits : Array[TextEdit]
}
///|
pub fn Fix::new(
title : String,
applicability? : Applicability = Unspecified,
) -> Fix {
{ title, applicability, edits: [] }
}
///|
pub fn Fix::with_edit(self : Fix, edit : TextEdit) -> Fix {
{ ..self, edits: self.edits + [edit] }
}
///|
pub fn Fix::title(self : Fix) -> String {
self.title
}
///|
pub fn Fix::applicability(self : Fix) -> Applicability {
self.applicability
}
///|
pub fn Fix::edits(self : Fix) -> Array[TextEdit] {
self.edits.copy()
}
///|
pub fn Fix::is_automatic(self : Fix) -> Bool {
self.applicability.is_automatic()
}
///|
fn TextEdit::compare_position(self : TextEdit, other : TextEdit) -> Int {
let source_order = self.source.compare(other.source)
if source_order != 0 {
source_order
} else {
let start_order = self.span.start().compare(other.span.start())
if start_order != 0 {
start_order
} else {
self.span.end().compare(other.span.end())
}
}
}
///|
fn Fix::sorted_edits(self : Fix) -> Array[TextEdit] {
let sorted = self.edits.copy()
sorted.sort_by(fn(left, right) { left.compare_position(right) })
sorted
}
///|
pub fn Fix::validate(self : Fix, sources : SourceMap) -> FixValidation {
if self.edits.is_empty() {
return Empty
}
let sorted = self.sorted_edits()
for index, edit in sorted {
match sources.get(edit.source()) {
None => return UnknownSource(edit.source())
Some(source) =>
if edit.span().end() > source.byte_length() {
return OutOfBounds(edit.source(), edit.span(), source.byte_length())
}
}
if index > 0 {
let previous = sorted[index - 1]
if previous.source() == edit.source() &&
previous.span().end() > edit.span().start() {
return Overlapping(edit.source(), previous.span(), edit.span())
}
}
}
Valid
}
///|
fn apply_source_edits(source : Source, edits : Array[TextEdit]) -> String {
let output = StringBuilder::new()
let mut cursor = 0
for edit in edits {
output.write_string(source.slice_offsets(cursor, edit.span().start()))
output.write_string(edit.replacement())
cursor = edit.span().end()
}
output.write_string(source.slice_offsets(cursor, source.byte_length()))
output.to_string()
}
///|
/// Applies this fix and returns a new source map.
///
/// The original map is never mutated. Invalid fixes return their validation
/// result and no partial edit is observable.
pub fn Fix::apply(
self : Fix,
sources : SourceMap,
) -> (SourceMap?, FixValidation) {
let validation = self.validate(sources)
if !validation.is_valid() {
return (None, validation)
}
let sorted = self.sorted_edits()
let result = SourceMap::new()
for source_index = 0
source_index < sources.length()
source_index = source_index + 1 {
let source_id = SourceId::new(source_index).unwrap()
let source = sources.get(source_id).unwrap()
let edits = sorted.filter(fn(edit) { edit.source() == source_id })
let text = if edits.is_empty() {
source.text()
} else {
apply_source_edits(source, edits)
}
result.add(source.name(), text) |> ignore
}
(Some(result), Valid)
}
///|
/// Applies only edits for one source and leaves validation explicit.
pub fn Fix::apply_to_source(
self : Fix,
sources : SourceMap,
source_id : SourceId,
) -> (String?, FixValidation) {
let validation = self.validate(sources)
if !validation.is_valid() {
return (None, validation)
}
match sources.get(source_id) {
None => (None, UnknownSource(source_id))
Some(source) => {
let edits = self
.sorted_edits()
.filter(fn(edit) { edit.source() == source_id })
if edits.is_empty() {
(Some(source.text()), Valid)
} else {
(Some(apply_source_edits(source, edits)), Valid)
}
}
}
}