feat(editor): introduce document sessions

This commit is contained in:
Feror 2026-06-02 14:39:46 +02:00
parent 1ca75c60bd
commit 3d3d1aee28
2 changed files with 127 additions and 0 deletions

View file

@ -0,0 +1,72 @@
import Foundation
import SaplingCore
@MainActor
public final class MarkdownDocumentSession: Identifiable, ObservableObject {
public let id: UUID
public let documentURL: URL
public let viewModel: HybridMarkdownEditorViewModel
public init(
id: UUID = UUID(),
documentURL: URL,
viewModel: HybridMarkdownEditorViewModel
) {
self.id = id
self.documentURL = documentURL
self.viewModel = viewModel
}
}
@MainActor
public final class DocumentSessionStore: ObservableObject {
@Published public private(set) var sessions: [MarkdownDocumentSession]
@Published public private(set) var activeSession: MarkdownDocumentSession?
public init(sessions: [MarkdownDocumentSession] = []) {
self.sessions = sessions
self.activeSession = sessions.first
}
@discardableResult
public func openDocument(
at url: URL,
loadDocument: @MainActor (URL) throws -> MarkdownDocument = HybridMarkdownEditorViewModel.loadDocument(at:)
) throws -> MarkdownDocumentSession {
let key = sessionKey(for: url)
if let existingSession = sessions.first(where: { sessionKey(for: $0.documentURL) == key }) {
activeSession = existingSession
return existingSession
}
let document = try loadDocument(url)
let session = MarkdownDocumentSession(
documentURL: url,
viewModel: HybridMarkdownEditorViewModel(document: document)
)
sessions.append(session)
activeSession = session
return session
}
@discardableResult
public func activateDocument(at url: URL) -> Bool {
let key = sessionKey(for: url)
guard let session = sessions.first(where: { sessionKey(for: $0.documentURL) == key }) else {
return false
}
activeSession = session
return true
}
public func updateActiveSessionFromViewModel() {
guard let activeSession else { return }
objectWillChange.send()
activeSession.objectWillChange.send()
}
private func sessionKey(for url: URL) -> String {
url.standardizedFileURL.path
}
}

View file

@ -0,0 +1,55 @@
import Foundation
import SaplingCore
import SaplingEditor
import XCTest
@MainActor
final class DocumentSessionStoreTests: XCTestCase {
func testOpeningSameFileReusesExistingSession() throws {
let url = URL(fileURLWithPath: "/tmp/Sapling/Notes.md")
var loadCount = 0
let store = DocumentSessionStore()
let first = try store.openDocument(at: url) { url in
loadCount += 1
return MarkdownDocument(url: url, title: "Notes", content: "# Notes")
}
let second = try store.openDocument(at: url.standardizedFileURL) { url in
loadCount += 1
return MarkdownDocument(url: url, title: "Notes", content: "# Reloaded")
}
XCTAssertIdentical(first, second)
XCTAssertIdentical(store.activeSession, first)
XCTAssertEqual(store.sessions.count, 1)
XCTAssertEqual(loadCount, 1)
XCTAssertEqual(first.viewModel.document.content, "# Notes")
}
func testOpeningDifferentFilesCreatesSeparateSessionsAndActivatesLatest() throws {
let firstURL = URL(fileURLWithPath: "/tmp/Sapling/One.md")
let secondURL = URL(fileURLWithPath: "/tmp/Sapling/Two.md")
let store = DocumentSessionStore()
let first = try store.openDocument(at: firstURL) { url in
MarkdownDocument(url: url, title: "One", content: "# One")
}
let second = try store.openDocument(at: secondURL) { url in
MarkdownDocument(url: url, title: "Two", content: "# Two")
}
XCTAssertEqual(store.sessions.count, 2)
XCTAssertIdentical(store.activeSession, second)
XCTAssertTrue(store.activateDocument(at: firstURL))
XCTAssertIdentical(store.activeSession, first)
}
func testActivateMissingDocumentReturnsFalse() {
let store = DocumentSessionStore()
XCTAssertFalse(store.activateDocument(at: URL(fileURLWithPath: "/tmp/missing.md")))
XCTAssertNil(store.activeSession)
}
}