import Foundation import SaplingCore public enum EditorLineMode: String, Hashable, Codable, Sendable { case source case rendered } public protocol EditorView { var state: EditorState { get } } public struct EditorDocument: Identifiable, Hashable, Codable, Sendable { public var id: UUID public var url: URL public var title: String public var source: String public var lastSavedSource: String public init( id: UUID = UUID(), url: URL, title: String, source: String, lastSavedSource: String? = nil ) { self.id = id self.url = url self.title = title self.source = source self.lastSavedSource = lastSavedSource ?? source } public init(markdownDocument: MarkdownDocument) { self.init( id: markdownDocument.id, url: markdownDocument.url, title: markdownDocument.title, source: markdownDocument.content ) } public var hasUnsavedChanges: Bool { source != lastSavedSource } public var markdownDocument: MarkdownDocument { MarkdownDocument(id: id, url: url, title: title, content: source) } } public struct EditorLine: Identifiable, Hashable, Sendable { public var id: Int { index } public var index: Int public var source: String public var range: NSRange public var mode: EditorLineMode public init(index: Int, source: String, range: NSRange, mode: EditorLineMode) { self.index = index self.source = source self.range = range self.mode = mode } } public struct EditorSelection: Hashable, Codable, Sendable { public var location: Int public var length: Int public init(location: Int = 0, length: Int = 0) { self.location = max(0, location) self.length = max(0, length) } public init(range: NSRange) { self.init(location: range.location, length: range.length) } public var range: NSRange { NSRange(location: location, length: length) } } public struct EditorState: Hashable, Sendable { public var document: EditorDocument public var lineIndex: DocumentLineIndex public var selection: EditorSelection public var activeLineIndex: Int public var lines: [EditorLine] { lineIndex.editorLines(activeLineIndex: activeLineIndex) } public var lineCount: Int { lineIndex.lineCount } public init( document: EditorDocument, selection: EditorSelection = EditorSelection(), activeLineIndex: Int = 0 ) { self.document = document self.selection = selection self.activeLineIndex = activeLineIndex self.lineIndex = DocumentLineIndex(source: document.source) } public var hasUnsavedChanges: Bool { document.hasUnsavedChanges } public var activeColumnNumber: Int { guard let activeLineRange = lineIndex.lineContentRange(forLine: activeLineIndex) else { return 1 } let offset = selection.location - activeLineRange.location return max(0, min(offset, activeLineRange.length)) + 1 } public mutating func updateSource(_ source: String) { document.source = source selection = EditorActiveLineTracker.clampedSelection(selection, in: source) lineIndex = DocumentLineIndex(source: source) activeLineIndex = lineIndex.lineIndex(containing: selection.location) } public mutating func updateSource( _ source: String, edit: DocumentLineIndexEdit, selection newSelection: EditorSelection? = nil ) { lineIndex.replace(edit, updatedSource: source) document.source = lineIndex.source selection = EditorActiveLineTracker.clampedSelection(newSelection ?? selection, in: document.source) activeLineIndex = lineIndex.lineIndex(containing: selection.location) } public mutating func updateSelection(_ selection: EditorSelection) { self.selection = EditorActiveLineTracker.clampedSelection(selection, in: document.source) activeLineIndex = lineIndex.lineIndex(containing: self.selection.location) } public mutating func markSaved() { document.lastSavedSource = document.source } } public protocol EditorRenderer: Sendable { func renderedLine(for line: EditorLine) -> AttributedString } @MainActor public protocol EditorCoordinator: AnyObject { var state: EditorState { get } func replaceDocument(_ document: EditorDocument) func updateSource(_ source: String) func updateSelection(_ selection: EditorSelection) func save() throws }