108 lines
4.7 KiB
Swift
108 lines
4.7 KiB
Swift
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() }
|
|
}
|
|
}
|