import SwiftUI import UIKit import Runestone // MARK: - CharacterPair concrete implementation // CharacterPair is a protocol in Runestone — no concrete BasicCharacterPair exists. private struct EditorCharacterPair: CharacterPair { let leading: String let trailing: String } // MARK: - RunestoneEditorView 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(length: 4) : .space(length: indentWidth) applyDiagnostics(to: tv) } private func loadState(into tv: Runestone.TextView) { let lang = LanguageDetector.language(for: filePath) let capturedText = content let capturedTheme = theme Task.detached(priority: .userInitiated) { // TextViewState init takes TreeSitterLanguage (non-optional). // Use two separate inits: one with language, one without (plain text). let state: TextViewState if let lang = lang { state = TextViewState(text: capturedText, theme: capturedTheme, language: lang) } else { state = TextViewState(text: capturedText, theme: capturedTheme) } 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 // highlightSelectedLine was replaced by lineSelectionDisplayType. // .line highlights the active line in the text area only. tv.lineSelectionDisplayType = .line 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 = [ EditorCharacterPair(leading: "(", trailing: ")"), EditorCharacterPair(leading: "[", trailing: "]"), EditorCharacterPair(leading: "{", trailing: "}"), EditorCharacterPair(leading: "\"", trailing: "\""), ] tv.characterPairTrailingComponentDeletionMode = .immediatelyFollowingLeadingComponent tv.indentStrategy = indentWidth == 0 ? .tab(length: 4) : .space(length: indentWidth) } private func applyDiagnostics(to tv: Runestone.TextView) { guard !diagnosticRanges.isEmpty else { tv.highlightedRanges = [] return } var highlights: [HighlightedRange] = [] for diag in diagnosticRanges { // TextLocation is 0-indexed; compiler diagnostics are 1-indexed. let textLoc = TextLocation(lineNumber: diag.line - 1, column: diag.column - 1) guard let charOffset = tv.location(at: textLoc) else { continue } let nsRange = NSRange(location: charOffset, length: max(1, diag.length)) let color: UIColor = diag.severity == .error ? UIColor(hex: "#F44747") : UIColor(hex: "#CCA700") // HighlightedRange only takes range and color — no cornerRadius/underlineStyle. highlights.append(HighlightedRange(range: nsRange, color: color)) } tv.highlightedRanges = highlights } // MARK: - Coordinator @MainActor final class Coordinator: NSObject, TextViewDelegate { var parent: RunestoneEditorView var isEditing: Bool = 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) { let sel = tv.selectedRange if let loc = tv.textLocation(at: sel.location) { parent.onCursorPositionChange?(loc.lineNumber + 1, loc.column + 1) } if !isEditing { parent.completionController.dismiss() } } func textViewDidBeginEditing(_ tv: Runestone.TextView) { isEditing = true } func textViewDidEndEditing(_ tv: Runestone.TextView) { isEditing = false parent.completionController.dismiss() } } }