Files
sing-box-for-apple/SFI/RunestoneTextView.swift
2026-01-05 16:28:59 +08:00

140 lines
3.9 KiB
Swift

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()
textView.showLineNumbers = true
textView.isLineWrappingEnabled = false
textView.showTabs = false
textView.showSpaces = false
textView.showLineBreaks = false
textView.showSoftLineBreaks = false
textView.showNonBreakingSpaces = false
textView.autocorrectionType = .no
textView.autocapitalizationType = .none
textView.smartDashesType = .no
textView.smartQuotesType = .no
textView.smartInsertDeleteType = .no
textView.backgroundColor = .secondarySystemGroupedBackground
textView.contentInsetAdjustmentBehavior = .always
textView.alwaysBounceVertical = true
textView.kern = 0.3
textView.lineHeightMultiplier = 1.3
textView.characterPairs = [
BasicCharacterPair(leading: "{", trailing: "}"),
BasicCharacterPair(leading: "[", trailing: "]"),
BasicCharacterPair(leading: "\"", trailing: "\""),
]
let theme = ProfileEditorTheme()
let state = TextViewState(text: text, theme: theme, language: .json5)
textView.setState(state)
textView.isEditable = isEditable
textView.editorDelegate = context.coordinator
context.coordinator.textView = textView
controller?.textView = textView
Task { @MainActor in
controller?.updateUndoState()
}
return textView
}
func updateUIView(_ textView: TextView, context _: Context) {
if textView.text != text {
textView.text = text
}
if textView.isEditable != isEditable {
textView.isEditable = isEditable
}
controller?.textView = textView
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
final class Coordinator: TextViewDelegate {
var parent: RunestoneTextView
weak var textView: TextView?
init(_ parent: RunestoneTextView) {
self.parent = parent
}
func textViewDidChange(_ textView: TextView) {
parent.text = textView.text
Task { @MainActor in
parent.controller?.updateUndoState()
}
}
}
}
final class BasicCharacterPair: CharacterPair {
let leading: String
let trailing: String
init(leading: String, trailing: String) {
self.leading = leading
self.trailing = trailing
}
}