Add toolbar for JSON editor

This commit is contained in:
世界
2026-01-05 16:28:59 +08:00
parent 3b447dd46c
commit 91edd77b00
10 changed files with 462 additions and 5 deletions
+1 -1
View File
@@ -17,7 +17,7 @@ struct MainView: View {
@State private var buttonState = ButtonVisibilityState()
private let profileEditor: (Binding<String>, Bool) -> AnyView = { text, isEditable in
AnyView(RunestoneTextView(text: text, isEditable: isEditable))
AnyView(ProfileEditorWrapperView(text: text, isEditable: isEditable))
}
private var shouldShowBottomAccessory: Bool {
+72
View File
@@ -0,0 +1,72 @@
import ApplicationLibrary
import Libbox
import SwiftUI
struct ProfileEditorWrapperView: View {
@Binding var text: String
let isEditable: Bool
@StateObject private var controller = RunestoneEditorController()
@State private var configurationError: String?
@State private var validationTask: Task<Void, Never>?
var body: some View {
VStack(spacing: 0) {
RunestoneTextView(text: $text, isEditable: isEditable, controller: controller)
if isEditable {
EditorToolbarView(
canUndo: controller.canUndo,
canRedo: controller.canRedo,
onUndo: { controller.undo() },
onRedo: { controller.redo() },
onFormat: { formatConfiguration() },
onInsertSymbol: { controller.insertSymbol($0) },
configurationError: configurationError,
onDismissError: { configurationError = nil }
)
}
}
.onChangeCompat(of: text) {
if isEditable {
scheduleValidation()
}
}
}
private func scheduleValidation() {
configurationError = nil
validationTask?.cancel()
validationTask = Task {
try? await Task.sleep(nanoseconds: 2 * NSEC_PER_SEC)
guard !Task.isCancelled else { return }
await checkConfiguration()
}
}
private func checkConfiguration() async {
let content = text
if content.isEmpty { return }
var error: NSError?
LibboxCheckConfig(content, &error)
if let error {
configurationError = error.localizedDescription
} else {
configurationError = nil
}
}
private func formatConfiguration() {
let content = text
if content.isEmpty { return }
var error: NSError?
let result = LibboxFormatConfig(content, &error)
if let error {
configurationError = error.localizedDescription
return
}
if let formatted = result?.value, formatted != content {
controller.setText(formatted)
}
}
}
+56
View File
@@ -2,9 +2,54 @@ import Runestone
import SwiftUI
import TreeSitterJSON5Runestone
@MainActor
public final class RunestoneEditorController: ObservableObject {
weak var textView: TextView?
@Published public var canUndo = false
@Published public var canRedo = false
public init() {}
public func undo() {
textView?.undoManager?.undo()
updateUndoState()
}
public func redo() {
textView?.undoManager?.redo()
updateUndoState()
}
public func insertSymbol(_ symbol: String) {
guard let textView else { return }
textView.insertText(symbol)
}
public func setText(_ newText: String) {
guard let textView else { return }
if let textRange = textView.textRange(from: textView.beginningOfDocument, to: textView.endOfDocument) {
textView.selectedTextRange = textRange
textView.insertText(newText)
}
}
func updateUndoState() {
canUndo = textView?.undoManager?.canUndo ?? false
canRedo = textView?.undoManager?.canRedo ?? false
}
}
struct RunestoneTextView: UIViewRepresentable {
@Binding var text: String
let isEditable: Bool
let controller: RunestoneEditorController?
init(text: Binding<String>, isEditable: Bool, controller: RunestoneEditorController? = nil) {
_text = text
self.isEditable = isEditable
self.controller = controller
}
func makeUIView(context: Context) -> TextView {
let textView = TextView()
@@ -43,6 +88,12 @@ struct RunestoneTextView: UIViewRepresentable {
textView.isEditable = isEditable
textView.editorDelegate = context.coordinator
context.coordinator.textView = textView
controller?.textView = textView
Task { @MainActor in
controller?.updateUndoState()
}
return textView
}
@@ -53,6 +104,7 @@ struct RunestoneTextView: UIViewRepresentable {
if textView.isEditable != isEditable {
textView.isEditable = isEditable
}
controller?.textView = textView
}
func makeCoordinator() -> Coordinator {
@@ -61,6 +113,7 @@ struct RunestoneTextView: UIViewRepresentable {
final class Coordinator: TextViewDelegate {
var parent: RunestoneTextView
weak var textView: TextView?
init(_ parent: RunestoneTextView) {
self.parent = parent
@@ -68,6 +121,9 @@ struct RunestoneTextView: UIViewRepresentable {
func textViewDidChange(_ textView: TextView) {
parent.text = textView.text
Task { @MainActor in
parent.controller?.updateUndoState()
}
}
}
}