Sapling/Sources/SaplingEditor/EditorArchitecture.swift

146 lines
4.2 KiB
Swift

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
self.lines = EditorActiveLineTracker.lines(
from: document.source,
activeLineIndex: activeLineIndex
)
}
public var hasUnsavedChanges: Bool {
document.hasUnsavedChanges
}
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
}
public mutating func updateSource(_ source: String) {
document.source = source
selection = EditorActiveLineTracker.clampedSelection(selection, in: source)
activeLineIndex = EditorActiveLineTracker.lineIndex(containing: selection.location, in: source)
lines = EditorActiveLineTracker.lines(from: source, activeLineIndex: activeLineIndex)
}
public mutating func updateSelection(_ selection: EditorSelection) {
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)
}
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
}