Improve JSON editor for iOS

This commit is contained in:
世界
2025-11-26 22:25:52 +08:00
parent 264c37218c
commit 3f1c716a0a
18 changed files with 3129 additions and 17 deletions
@@ -29,26 +29,30 @@
}
}
} else {
viewBuilder {
if readOnly {
TextEditor(text: .constant(viewModel.profileContent))
} else {
TextEditor(text: $viewModel.profileContent)
}
}
.font(Font.system(.caption2, design: .monospaced))
.autocorrectionDisabled(true)
.textContentType(.init(rawValue: ""))
#if os(iOS)
.keyboardType(.asciiCapable)
.textInputAutocapitalization(.none)
.background(Color(UIColor.secondarySystemGroupedBackground))
#elseif os(macOS)
.padding()
#endif
RunestoneTextView(
text: readOnly ? .constant(viewModel.profileContent) : $viewModel.profileContent,
isEditable: !readOnly
)
.onChangeCompat(of: viewModel.profileContent) {
viewModel.markAsChanged()
}
#elseif os(macOS)
viewBuilder {
if readOnly {
TextEditor(text: .constant(viewModel.profileContent))
} else {
TextEditor(text: $viewModel.profileContent)
}
}
.font(Font.system(.caption2, design: .monospaced))
.autocorrectionDisabled(true)
.textContentType(.init(rawValue: ""))
.padding()
.onChangeCompat(of: viewModel.profileContent) {
viewModel.markAsChanged()
}
#endif
}
}
.alertBinding($viewModel.alert)
@@ -0,0 +1,73 @@
#if os(iOS)
import Runestone
import UIKit
final class ProfileEditorTheme: Theme {
let font: UIFont = .monospacedSystemFont(ofSize: 14, weight: .regular)
let textColor: UIColor = .label
let gutterBackgroundColor: UIColor = .secondarySystemBackground
let gutterHairlineColor: UIColor = .separator
let lineNumberColor: UIColor = .secondaryLabel
let lineNumberFont: UIFont = .monospacedSystemFont(ofSize: 14, weight: .regular)
let selectedLineBackgroundColor: UIColor = .systemFill
let selectedLinesLineNumberColor: UIColor = .label
let selectedLinesGutterBackgroundColor: UIColor = .secondarySystemBackground
let invisibleCharactersColor: UIColor = .tertiaryLabel
let pageGuideHairlineColor: UIColor = .separator
let pageGuideBackgroundColor: UIColor = .secondarySystemBackground
let markedTextBackgroundColor: UIColor = .systemFill
let markedTextBackgroundCornerRadius: CGFloat = 4
func textColor(for rawHighlightName: String) -> UIColor? {
guard let highlightName = HighlightName(rawHighlightName) else {
return nil
}
switch highlightName {
case .comment:
return .secondaryLabel
case .property:
return .systemCyan
case .string:
return .systemGreen
case .number:
return .systemOrange
case .constantBuiltin:
return .systemPurple
case .error:
return .systemRed
}
}
func fontTraits(for _: String) -> FontTraits {
[]
}
}
private enum HighlightName: String {
case comment
case property
case string
case number
case constantBuiltin = "constant.builtin"
case error
init?(_ rawHighlightName: String) {
var components = rawHighlightName.split(separator: ".")
while !components.isEmpty {
let candidateRawHighlightName = components.joined(separator: ".")
if let highlightName = Self(rawValue: candidateRawHighlightName) {
self = highlightName
return
}
components.removeLast()
}
return nil
}
}
#endif
@@ -0,0 +1,85 @@
#if os(iOS)
import Runestone
import SwiftUI
import TreeSitterJSON5Runestone
struct RunestoneTextView: UIViewRepresentable {
@Binding var text: String
let isEditable: Bool
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
return textView
}
func updateUIView(_ textView: TextView, context _: Context) {
if textView.text != text {
textView.text = text
}
if textView.isEditable != isEditable {
textView.isEditable = isEditable
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
final class Coordinator: TextViewDelegate {
var parent: RunestoneTextView
init(_ parent: RunestoneTextView) {
self.parent = parent
}
func textViewDidChange(_ textView: TextView) {
parent.text = textView.text
}
}
}
final class BasicCharacterPair: CharacterPair {
let leading: String
let trailing: String
init(leading: String, trailing: String) {
self.leading = leading
self.trailing = trailing
}
}
#endif