feat(editor): introduce document sessions
This commit is contained in:
parent
1ca75c60bd
commit
3d3d1aee28
2 changed files with 127 additions and 0 deletions
72
Sources/SaplingEditor/DocumentSessionStore.swift
Normal file
72
Sources/SaplingEditor/DocumentSessionStore.swift
Normal 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
|
||||
}
|
||||
}
|
||||
55
Tests/SaplingEditorTests/DocumentSessionStoreTests.swift
Normal file
55
Tests/SaplingEditorTests/DocumentSessionStoreTests.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue