199 lines
5.8 KiB
Swift
199 lines
5.8 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 = Self.makeLines(
|
|
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
|
|
activeLineIndex = Self.lineIndex(containing: selection.location, in: source)
|
|
lines = Self.makeLines(from: source, activeLineIndex: activeLineIndex)
|
|
}
|
|
|
|
public mutating func updateSelection(_ selection: EditorSelection) {
|
|
self.selection = selection
|
|
activeLineIndex = Self.lineIndex(containing: selection.location, in: document.source)
|
|
lines = Self.makeLines(from: document.source, activeLineIndex: activeLineIndex)
|
|
}
|
|
|
|
public mutating func markSaved() {
|
|
document.lastSavedSource = document.source
|
|
}
|
|
|
|
private static func makeLines(from source: String, activeLineIndex: Int) -> [EditorLine] {
|
|
var lines: [EditorLine] = []
|
|
var lineStart = source.startIndex
|
|
var utf16Location = 0
|
|
var index = 0
|
|
|
|
while lineStart < source.endIndex {
|
|
let lineEnd = source[lineStart...].firstIndex(of: "\n") ?? source.endIndex
|
|
let line = String(source[lineStart..<lineEnd])
|
|
let length = line.utf16.count
|
|
lines.append(EditorLine(
|
|
index: index,
|
|
source: line,
|
|
range: NSRange(location: utf16Location, length: length),
|
|
mode: index == activeLineIndex ? .source : .rendered
|
|
))
|
|
|
|
if lineEnd == source.endIndex {
|
|
lineStart = lineEnd
|
|
utf16Location += length
|
|
} else {
|
|
lineStart = source.index(after: lineEnd)
|
|
utf16Location += length + 1
|
|
}
|
|
index += 1
|
|
}
|
|
|
|
if source.isEmpty || source.hasSuffix("\n") {
|
|
lines.append(EditorLine(
|
|
index: index,
|
|
source: "",
|
|
range: NSRange(location: utf16Location, length: 0),
|
|
mode: index == activeLineIndex ? .source : .rendered
|
|
))
|
|
}
|
|
|
|
return lines
|
|
}
|
|
|
|
private static func lineIndex(containing location: Int, in source: String) -> Int {
|
|
let clampedLocation = max(0, min(location, source.utf16.count))
|
|
var currentLocation = 0
|
|
|
|
for (index, line) in source.split(separator: "\n", omittingEmptySubsequences: false).enumerated() {
|
|
let length = line.utf16.count
|
|
if clampedLocation <= currentLocation + length {
|
|
return index
|
|
}
|
|
currentLocation += length + 1
|
|
}
|
|
|
|
return 0
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|