PadXcode-iPad/Editor/RunestoneEditorView.swift

109 lines
4.7 KiB
Swift
Raw Normal View History

2026-04-12 00:46:30 -07:00
import SwiftUI
import UIKit
import Runestone
struct RunestoneEditorView: UIViewRepresentable {
@Binding var content: String
@Binding var textViewRef: Runestone.TextView?
let filePath: String
let theme: any Theme
let showLineNumbers: Bool
let lineWrapping: Bool
let indentWidth: Int
var diagnosticRanges: [DiagnosticRange] = []
var onContentChange: ((String) -> Void)?
var onCursorPositionChange: ((Int, Int) -> Void)?
@ObservedObject var completionController: CompletionController
func makeCoordinator() -> Coordinator { Coordinator(self) }
func makeUIView(context: Context) -> Runestone.TextView {
let tv = Runestone.TextView()
configureAppearance(tv); configureEditing(tv)
tv.editorDelegate = context.coordinator
completionController.attach(to: tv)
DispatchQueue.main.async { textViewRef = tv }
loadState(into: tv)
return tv
}
func updateUIView(_ tv: Runestone.TextView, context: Context) {
if tv.text != content && !context.coordinator.isEditing { loadState(into: tv) }
tv.showLineNumbers = showLineNumbers
tv.isLineWrappingEnabled = lineWrapping
tv.indentStrategy = indentWidth == 0 ? .tab : .space(length: indentWidth)
applyDiagnostics(to: tv)
}
private func loadState(into tv: Runestone.TextView) {
let lang = LanguageDetector.language(for: filePath)
let capturedContent = content; let capturedTheme = theme
Task.detached(priority: .userInitiated) {
let state = TextViewState(text: capturedContent, theme: capturedTheme,
language: lang.map { TreeSitterLanguageMode(language: $0) })
await MainActor.run { tv.setState(state) }
}
}
private func configureAppearance(_ tv: Runestone.TextView) {
tv.backgroundColor = UIColor(hex: "#1E1E1E")
tv.showLineNumbers = showLineNumbers
tv.showSpaces = false; tv.showTabs = false; tv.showLineBreaks = false
tv.isLineWrappingEnabled = lineWrapping
tv.highlightSelectedLine = true
tv.lineHeightMultiplier = 1.4; tv.kern = 0.3
tv.contentInset = UIEdgeInsets(top: 8, left: 0, bottom: 120, right: 0)
tv.verticalOverscrollFactor = 0.5
}
private func configureEditing(_ tv: Runestone.TextView) {
tv.isEditable = true; tv.autocorrectionType = .no; tv.autocapitalizationType = .none
tv.smartDashesType = .no; tv.smartQuotesType = .no
tv.smartInsertDeleteType = .no; tv.spellCheckingType = .no
tv.characterPairs = [
BasicCharacterPair(leading: "(", trailing: ")"),
BasicCharacterPair(leading: "[", trailing: "]"),
BasicCharacterPair(leading: "{", trailing: "}"),
BasicCharacterPair(leading: "\"", trailing: "\""),
]
tv.characterPairTrailingComponentDeletionMode = .immediatelyFollowingLeadingComponent
tv.indentStrategy = indentWidth == 0 ? .tab : .space(length: indentWidth)
}
private func applyDiagnostics(to tv: Runestone.TextView) {
guard !diagnosticRanges.isEmpty else { tv.setHighlightedRanges([]); return }
let highlights: [HighlightedRange] = diagnosticRanges.compactMap { diag in
guard let range = tv.range(atLine: diag.line, column: diag.column) else { return nil }
let color: UIColor = diag.severity == .error ? UIColor(hex: "#F44747") : UIColor(hex: "#CCA700")
return HighlightedRange(range: range, color: color, cornerRadius: 0, underlineStyle: .single)
}
tv.setHighlightedRanges(highlights)
}
// MARK: - Coordinator
final class Coordinator: NSObject, TextViewDelegate {
var parent: RunestoneEditorView
var isEditing = false
init(_ parent: RunestoneEditorView) { self.parent = parent }
func textViewDidChange(_ tv: Runestone.TextView) {
isEditing = true
parent.content = tv.text
parent.onContentChange?(tv.text)
parent.completionController.textDidChange(in: tv, filePath: parent.filePath)
isEditing = false
}
func textViewDidChangeSelection(_ tv: Runestone.TextView) {
guard let sel = tv.selectedRange, let loc = tv.textLocation(at: sel.location) else { return }
parent.onCursorPositionChange?(loc.lineNumber, loc.column)
if !isEditing { parent.completionController.dismiss() }
}
func textViewDidBeginEditing(_ tv: Runestone.TextView) { isEditing = true }
func textViewDidEndEditing(_ tv: Runestone.TextView) { isEditing = false; parent.completionController.dismiss() }
}
}