PureXML/Sources/QuickFix.swift
Mihaela Mihaljevic d667b4cf1e feat(editor): structured quick-fixes from completions
Slice 4 of #94 (completes the epic). Quick-fixes are derived from the structured
schema completions, never by parsing a diagnostic message:

- TreeNode gains contentRange (the span between an element's start and end tags),
  captured by the ranged reader, so an inserted attribute or child is placed
  exactly.
- QuickFix and TextEdit model an editor edit (a replacement over a source range;
  an insertion is a zero-width range).
- QuickFixEngine turns a Completions into fixes: a required-but-absent attribute
  becomes an insertion before the start tag's >, and a still-expected required
  child becomes an insertion before the end tag. Schema.Document.quickFixes(at:in:)
  is the entry over a ranged tree.

Tests apply a fix and re-validate: adding the required id, or inserting the
required qty, yields XML the schema then accepts; a complete element offers none.

#94 (editor integration) is complete: ranged recovering read, unified lint,
schema completions, and quick-fixes.
2026-06-08 13:35:33 +02:00

59 lines
2.5 KiB
Swift

public extension PureXML {
/// A single text edit an editor can apply: replace `range` with `replacement`.
/// An insertion is a zero-width range at the insertion point.
struct TextEdit: Equatable, Sendable {
public var range: Parsing.SourceRange
public var replacement: String
public init(range: Parsing.SourceRange, replacement: String) {
self.range = range
self.replacement = replacement
}
}
/// An offered fix: a human-readable title and the edits that apply it.
struct QuickFix: Equatable, Sendable {
public var title: String
public var edits: [TextEdit]
public init(title: String, edits: [TextEdit]) {
self.title = title
self.edits = edits
}
}
/// Derives quick-fixes from the *structured* schema completions of an element
/// (never from parsing a diagnostic message): a required-but-absent attribute
/// becomes an insertion just before the start tag's `>`, and a still-expected
/// required child becomes an insertion just before the end tag.
enum QuickFixEngine {
static func fixes(from completions: Schema.Completions, element: Model.TreeNode) -> [QuickFix] {
guard let content = element.contentRange else { return [] }
var fixes: [QuickFix] = []
// The `>` of the start tag sits one character before the content start.
let tagEnd = Parsing.Mark(
line: content.start.line,
column: Swift.max(1, content.start.column - 1),
offset: Swift.max(0, content.start.offset - 1),
)
let attributePoint = Parsing.SourceRange(start: tagEnd, end: tagEnd)
for attribute in completions.attributes where attribute.required && !attribute.present {
fixes.append(QuickFix(
title: "Add required attribute '\(attribute.name)'",
edits: [TextEdit(range: attributePoint, replacement: " \(attribute.name)=\"\"")],
))
}
if !completions.complete, let next = completions.elements.first {
let childPoint = Parsing.SourceRange(start: content.end, end: content.end)
fixes.append(QuickFix(
title: "Insert <\(next)>",
edits: [TextEdit(range: childPoint, replacement: "<\(next)></\(next)>")],
))
}
return fixes
}
}
}