feat(editor): implement document loading and saving

This commit is contained in:
Feror 2026-05-29 17:58:23 +02:00
parent 93d095feeb
commit 1e15778364

View file

@ -1,4 +1,5 @@
import SwiftUI import SwiftUI
import UniformTypeIdentifiers
import SaplingCore import SaplingCore
import SaplingWorkspace import SaplingWorkspace
import SaplingGit import SaplingGit
@ -40,6 +41,7 @@ private final class SaplingAppModel: ObservableObject {
@Published var editorViewModel: HybridMarkdownEditorViewModel? @Published var editorViewModel: HybridMarkdownEditorViewModel?
@Published var gitStatuses: [GitFileStatus] = [] @Published var gitStatuses: [GitFileStatus] = []
@Published var configuration: SaplingConfiguration @Published var configuration: SaplingConfiguration
@Published var editorErrorMessage: String?
private let gitProvider: any GitProvider private let gitProvider: any GitProvider
private let configurationStore: any ConfigurationStore private let configurationStore: any ConfigurationStore
@ -71,11 +73,40 @@ private final class SaplingAppModel: ObservableObject {
if file.url == SaplingSampleData.document.url { if file.url == SaplingSampleData.document.url {
setSelectedDocument(SaplingSampleData.document) setSelectedDocument(SaplingSampleData.document)
} else { } else {
setSelectedDocument(MarkdownDocument( openDocument(at: file.url)
url: file.url, }
title: file.name, }
content: "# \(file.name)\n\nTODO: Load document contents from disk."
)) func openDocument(at url: URL) {
let didStartAccessing = url.startAccessingSecurityScopedResource()
defer {
if didStartAccessing {
url.stopAccessingSecurityScopedResource()
}
}
do {
let document = try HybridMarkdownEditorViewModel.loadDocument(at: url)
setSelectedDocument(document)
editorErrorMessage = nil
logger.info("Opened document: \(document.title)", category: .editor)
} catch {
editorErrorMessage = "Unable to open \(url.lastPathComponent): \(error.localizedDescription)"
logger.error("Failed to open document: \(error)", category: .editor)
}
}
func saveSelectedDocument() {
guard let editorViewModel else { return }
do {
try editorViewModel.save()
selectedDocument = editorViewModel.document
editorErrorMessage = nil
logger.info("Saved document: \(editorViewModel.document.title)", category: .editor)
} catch {
editorErrorMessage = "Unable to save \(editorViewModel.document.title): \(error.localizedDescription)"
logger.error("Failed to save document: \(error)", category: .editor)
} }
} }
@ -123,6 +154,7 @@ private final class SaplingAppModel: ObservableObject {
private struct MainWindow: View { private struct MainWindow: View {
@ObservedObject var model: SaplingAppModel @ObservedObject var model: SaplingAppModel
@State private var isImportingMarkdown = false
var body: some View { var body: some View {
NavigationSplitView { NavigationSplitView {
@ -138,18 +170,73 @@ private struct MainWindow: View {
viewModel: editorViewModel, viewModel: editorViewModel,
renderer: MarkdownRenderer() renderer: MarkdownRenderer()
) )
.navigationTitle(editorViewModel.document.title) .navigationTitle(navigationTitle(for: editorViewModel))
} else { } else {
EmptyEditorPlaceholder() EmptyEditorPlaceholder()
} }
} detail: { } detail: {
SaplingInspectorView( SaplingInspectorView(
project: model.selectedProject, project: model.selectedProject,
document: model.selectedDocument, document: model.editorViewModel?.document ?? model.selectedDocument,
statuses: model.gitStatuses statuses: model.gitStatuses
) )
.frame(minWidth: 260) .frame(minWidth: 260)
} }
.toolbar {
ToolbarItemGroup {
Button {
isImportingMarkdown = true
} label: {
Label("Open Markdown", systemImage: "doc.badge.plus")
}
.help("Open Markdown")
Button {
model.saveSelectedDocument()
} label: {
Label("Save", systemImage: "square.and.arrow.down")
}
.help("Save")
.disabled(model.editorViewModel?.hasUnsavedChanges != true)
.keyboardShortcut("s", modifiers: .command)
}
}
.fileImporter(
isPresented: $isImportingMarkdown,
allowedContentTypes: [.saplingMarkdown, .plainText],
allowsMultipleSelection: false
) { result in
switch result {
case .success(let urls):
guard let url = urls.first else { return }
model.openDocument(at: url)
case .failure(let error):
model.editorErrorMessage = "Unable to open document: \(error.localizedDescription)"
}
}
.alert(
"Editor Error",
isPresented: Binding(
get: { model.editorErrorMessage != nil },
set: { if !$0 { model.editorErrorMessage = nil } }
)
) {
Button("OK", role: .cancel) {
model.editorErrorMessage = nil
}
} message: {
Text(model.editorErrorMessage ?? "")
}
}
private func navigationTitle(for viewModel: HybridMarkdownEditorViewModel) -> String {
viewModel.hasUnsavedChanges ? "\(viewModel.document.title) *" : viewModel.document.title
}
}
private extension UTType {
static var saplingMarkdown: UTType {
UTType(filenameExtension: "md") ?? .plainText
} }
} }