2026-05-29 17:52:49 +02:00
|
|
|
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 lines: [EditorLine]
|
|
|
|
|
public var selection: EditorSelection
|
|
|
|
|
public var activeLineIndex: Int
|
|
|
|
|
|
|
|
|
|
public init(
|
|
|
|
|
document: EditorDocument,
|
|
|
|
|
selection: EditorSelection = EditorSelection(),
|
|
|
|
|
activeLineIndex: Int = 0
|
|
|
|
|
) {
|
|
|
|
|
self.document = document
|
|
|
|
|
self.selection = selection
|
|
|
|
|
self.activeLineIndex = activeLineIndex
|
2026-05-29 20:08:46 +02:00
|
|
|
self.lines = EditorActiveLineTracker.lines(
|
2026-05-29 17:52:49 +02:00
|
|
|
from: document.source,
|
|
|
|
|
activeLineIndex: activeLineIndex
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public var hasUnsavedChanges: Bool {
|
|
|
|
|
document.hasUnsavedChanges
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-29 19:19:59 +02:00
|
|
|
public var activeColumnNumber: Int {
|
|
|
|
|
guard lines.indices.contains(activeLineIndex) else { return 1 }
|
|
|
|
|
let activeLine = lines[activeLineIndex]
|
|
|
|
|
let offset = selection.location - activeLine.range.location
|
|
|
|
|
return max(0, min(offset, activeLine.range.length)) + 1
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-29 17:52:49 +02:00
|
|
|
public mutating func updateSource(_ source: String) {
|
|
|
|
|
document.source = source
|
2026-05-29 20:08:46 +02:00
|
|
|
selection = EditorActiveLineTracker.clampedSelection(selection, in: source)
|
|
|
|
|
activeLineIndex = EditorActiveLineTracker.lineIndex(containing: selection.location, in: source)
|
|
|
|
|
lines = EditorActiveLineTracker.lines(from: source, activeLineIndex: activeLineIndex)
|
2026-05-29 17:52:49 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public mutating func updateSelection(_ selection: EditorSelection) {
|
2026-05-29 20:08:46 +02:00
|
|
|
self.selection = EditorActiveLineTracker.clampedSelection(selection, in: document.source)
|
|
|
|
|
activeLineIndex = EditorActiveLineTracker.lineIndex(containing: self.selection.location, in: document.source)
|
|
|
|
|
lines = EditorActiveLineTracker.lines(from: document.source, activeLineIndex: activeLineIndex)
|
2026-05-29 17:52:49 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|