import SwiftUI import Observation import UniformTypeIdentifiers import Foundation import OSLog #if canImport(UIKit) import UIKit #endif ///MARK: - Text Sanitization // Normalizes pasted and loaded text before it reaches editor state. enum EditorTextSanitizer { // Converts control/marker glyphs into safe spaces/newlines and removes unsupported scalars. nonisolated static func sanitize(_ input: String) -> String { // Normalize line endings first so CRLF does not become double newlines. let normalized = input .replacingOccurrences(of: "\r\n", with: "\n") .replacingOccurrences(of: "\r", with: "\n") var result = String.UnicodeScalarView() result.reserveCapacity(normalized.unicodeScalars.count) for scalar in normalized.unicodeScalars { switch scalar { case "\n": result.append(scalar) case "\t", "\u{000B}", "\u{000C}": result.append(" ") case "\u{00A0}": result.append(" ") case "\u{00B7}", "\u{2022}", "\u{2219}", "\u{237D}", "\u{2420}", "\u{2422}", "\u{2423}", "\u{2581}": result.append(" ") case "\u{00BB}", "\u{2192}", "\u{21E5}": result.append(" ") case "\u{00B6}", "\u{21A9}", "\u{21B2}", "\u{21B5}", "\u{23CE}", "\u{2424}", "\u{2425}": result.append("\n") case "\u{240A}", "\u{240D}": result.append("\n") default: let cat = scalar.properties.generalCategory if cat == .format || cat == .control || cat == .lineSeparator || cat == .paragraphSeparator { continue } if (0x2400...0x243F).contains(scalar.value) { continue } if cat == .spaceSeparator && scalar != " " && scalar != "\t" { result.append(" ") continue } result.append(scalar) } } return String(result) } } private enum EditorLoadHelper { nonisolated static let fastLoadSanitizeByteThreshold = 2_000_000 nonisolated static let largeFileCandidateByteThreshold = 2_000_000 nonisolated static let skipFingerprintByteThreshold = 4_000_000 nonisolated static let streamChunkBytes = 262_144 nonisolated static func sanitizeTextForFileLoad(_ input: String, useFastPath: Bool) -> String { if useFastPath { // Fast path for large files: preserve visible content, normalize line endings, // and only strip NUL which frequently breaks text system behavior. if !input.contains("\0") && !input.contains("\r") { return input } return input .replacingOccurrences(of: "\0", with: "") .replacingOccurrences(of: "\r\n", with: "\n") .replacingOccurrences(of: "\r", with: "\n") } return EditorTextSanitizer.sanitize(input) } nonisolated static func streamFileData(from url: URL) throws -> Data { guard let input = InputStream(url: url) else { throw CocoaError(.fileReadNoSuchFile) } input.open() defer { input.close() } var aggregate = Data() if let expectedSize = try? url.resourceValues(forKeys: [.fileSizeKey]).fileSize, expectedSize > 0 { aggregate.reserveCapacity(expectedSize) } var buffer = [UInt8](repeating: 0, count: streamChunkBytes) while true { let bytesRead = input.read(&buffer, maxLength: buffer.count) if bytesRead < 0 { throw input.streamError ?? CocoaError(.fileReadUnknown) } if bytesRead == 0 { if input.streamStatus == .atEnd || input.streamStatus == .closed { break } continue } aggregate.append(buffer, count: bytesRead) } if let expectedSize = try? url.resourceValues(forKeys: [.fileSizeKey]).fileSize, expectedSize > 0, aggregate.count < expectedSize { // Fallback for rare short-read stream behavior. return try Data(contentsOf: url, options: [.mappedIfSafe]) } return aggregate } } private struct EditorFileLoadResult: Sendable { let content: String let detectedLanguage: String let languageLocked: Bool let fingerprint: UInt64? let fileModificationDate: Date? let isLargeCandidate: Bool } private struct EditorFileSavePayload: Sendable { let content: String let fingerprint: UInt64 } ///MARK: - Piece Table Storage // Mutable text buffer using original/add buffers and piece spans. final class PieceTableDocument { private enum Source { case original case add } private struct Piece { let source: Source let startUTF16: Int let lengthUTF16: Int } private var originalBuffer: String private var addBuffer: String = "" private var pieces: [Piece] = [] private var cachedString: String? init(_ text: String) { originalBuffer = text let len = (text as NSString).length if len > 0 { pieces = [Piece(source: .original, startUTF16: 0, lengthUTF16: len)] } } var utf16Length: Int { pieces.reduce(0) { $0 + $1.lengthUTF16 } } func string() -> String { if let cachedString { return cachedString } if pieces.isEmpty { cachedString = "" return "" } let originalNSString = originalBuffer as NSString let addNSString = addBuffer as NSString var out = String() out.reserveCapacity(max(0, utf16Length)) for piece in pieces { guard piece.lengthUTF16 > 0 else { continue } let ns = piece.source == .original ? originalNSString : addNSString out += ns.substring(with: NSRange(location: piece.startUTF16, length: piece.lengthUTF16)) } cachedString = out return out } func replaceAll(with text: String) { originalBuffer = text addBuffer = "" cachedString = text pieces.removeAll(keepingCapacity: true) let len = (text as NSString).length if len > 0 { pieces.append(Piece(source: .original, startUTF16: 0, lengthUTF16: len)) } } func replace(range: NSRange, with replacement: String) { let total = utf16Length let clampedLocation = min(max(0, range.location), total) let maxLen = max(0, total - clampedLocation) let clampedLength = min(max(0, range.length), maxLen) let lower = clampedLocation let upper = clampedLocation + clampedLength var newPieces: [Piece] = [] newPieces.reserveCapacity(pieces.count + 2) var cursor = 0 for piece in pieces { let pieceStart = cursor let pieceEnd = pieceStart + piece.lengthUTF16 defer { cursor = pieceEnd } if piece.lengthUTF16 == 0 { continue } if pieceEnd <= lower || pieceStart >= upper { newPieces.append(piece) continue } if lower > pieceStart { let leftLen = lower - pieceStart if leftLen > 0 { newPieces.append(Piece(source: piece.source, startUTF16: piece.startUTF16, lengthUTF16: leftLen)) } } if upper < pieceEnd { let rightOffset = upper - pieceStart let rightLen = pieceEnd - upper if rightLen > 0 { newPieces.append(Piece(source: piece.source, startUTF16: piece.startUTF16 + rightOffset, lengthUTF16: rightLen)) } } } if !replacement.isEmpty { let addStart = (addBuffer as NSString).length addBuffer.append(replacement) let addLen = (replacement as NSString).length if addLen > 0 { let insertIndex: Int = { if clampedLength > 0 { return indexForUTF16Location(in: newPieces, location: lower) } return insertionIndexForUTF16Location(in: newPieces, location: lower) }() newPieces.insert(Piece(source: .add, startUTF16: addStart, lengthUTF16: addLen), at: insertIndex) } } pieces = coalescedPieces(newPieces) cachedString = nil } private func indexForUTF16Location(in pieces: [Piece], location: Int) -> Int { var cursor = 0 for (idx, piece) in pieces.enumerated() { let end = cursor + piece.lengthUTF16 if location < end { return idx } cursor = end } return pieces.count } private func insertionIndexForUTF16Location(in pieces: [Piece], location: Int) -> Int { var cursor = 0 for (idx, piece) in pieces.enumerated() { let end = cursor + piece.lengthUTF16 if location <= cursor { return idx } if location < end { return idx + 1 } cursor = end } return pieces.count } private func coalescedPieces(_ items: [Piece]) -> [Piece] { var result: [Piece] = [] result.reserveCapacity(items.count) for piece in items where piece.lengthUTF16 > 0 { if let last = result.last, last.source == piece.source, last.startUTF16 + last.lengthUTF16 == piece.startUTF16 { result[result.count - 1] = Piece( source: last.source, startUTF16: last.startUTF16, lengthUTF16: last.lengthUTF16 + piece.lengthUTF16 ) } else { result.append(piece) } } return result } } ///MARK: - Tab Model // Represents one editor tab and its mutable editing state. @MainActor @Observable final class TabData: Identifiable { let id: UUID fileprivate(set) var name: String private var contentStorage: PieceTableDocument private(set) var contentRevision: Int = 0 fileprivate(set) var language: String fileprivate(set) var fileURL: URL? fileprivate(set) var languageLocked: Bool fileprivate(set) var isDirty: Bool fileprivate(set) var lastSavedFingerprint: UInt64? fileprivate(set) var lastKnownFileModificationDate: Date? fileprivate(set) var isLoadingContent: Bool fileprivate(set) var isLargeFileCandidate: Bool init( id: UUID = UUID(), name: String, content: String, language: String, fileURL: URL?, languageLocked: Bool = false, isDirty: Bool = false, lastSavedFingerprint: UInt64? = nil, lastKnownFileModificationDate: Date? = nil, isLoadingContent: Bool = false, isLargeFileCandidate: Bool = false ) { self.id = id self.name = name self.contentStorage = PieceTableDocument(content) self.language = language self.fileURL = fileURL self.languageLocked = languageLocked self.isDirty = isDirty self.lastSavedFingerprint = lastSavedFingerprint self.lastKnownFileModificationDate = lastKnownFileModificationDate self.isLoadingContent = isLoadingContent self.isLargeFileCandidate = isLargeFileCandidate } var content: String { contentStorage.string() } var contentUTF16Length: Int { contentStorage.utf16Length } @discardableResult func replaceContentStorage( with text: String, markDirty: Bool = false, compareIfLengthAtMost equalityCheckUTF16Length: Int? = nil ) -> Bool { let previousLength = contentStorage.utf16Length let newLength = (text as NSString).length if let equalityCheckUTF16Length, previousLength == newLength, newLength <= equalityCheckUTF16Length, contentStorage.string() == text { return false } contentStorage.replaceAll(with: text) contentRevision &+= 1 if markDirty && !isDirty { isDirty = true } return true } @discardableResult func replaceContent(in range: NSRange, with replacement: String, markDirty: Bool = false) -> Bool { let totalLength = contentStorage.utf16Length let safeLocation = min(max(0, range.location), totalLength) let maxLength = max(0, totalLength - safeLocation) let safeLength = min(max(0, range.length), maxLength) if safeLength == 0, replacement.isEmpty { return false } contentStorage.replace(range: NSRange(location: safeLocation, length: safeLength), with: replacement) contentRevision &+= 1 if markDirty && !isDirty { isDirty = true } return true } func markClean(withFingerprint fingerprint: UInt64?) { isDirty = false lastSavedFingerprint = fingerprint } func updateLastKnownFileModificationDate(_ date: Date?) { lastKnownFileModificationDate = date } func resetContentRevision() { contentRevision = 0 } } ///MARK: - Editor View Model // Owns tab lifecycle, file IO, and language-detection behavior. @MainActor @Observable class EditorViewModel { struct ExternalFileConflictState: Sendable { let tabID: UUID let fileURL: URL let diskModifiedAt: Date? } struct ExternalFileComparisonSnapshot: Sendable { let fileName: String let localContent: String let diskContent: String } private actor TabCommandQueue { private var isLocked = false private var waiters: [CheckedContinuation] = [] func acquire() async { guard isLocked else { isLocked = true return } await withCheckedContinuation { continuation in waiters.append(continuation) } } func release() { if waiters.isEmpty { isLocked = false return } let next = waiters.removeFirst() next.resume() } } private static let saveSignposter = OSSignposter(subsystem: "h3p.Neon-Vision-Editor", category: "FileIO") private static let largeContentLanguageBypassUTF16Length = 1_000_000 private static let deferredLanguageDetectionUTF16Length = 180_000 private static let deferredLanguageDetectionDelayNanos: UInt64 = 220_000_000 private static let deferredLanguageDetectionSampleUTF16Length = 180_000 private(set) var tabs: [TabData] = [] private(set) var selectedTabID: UUID? var pendingExternalFileConflict: ExternalFileConflictState? var showSidebar: Bool = true var isBrainDumpMode: Bool = false var showingRename: Bool = false var renameText: String = "" var isLineWrapEnabled: Bool = true @ObservationIgnored private let tabCommandQueue = TabCommandQueue() @ObservationIgnored private var pendingLanguageDetectionTasks: [UUID: Task] = [:] @ObservationIgnored private var tabIndexByID: [UUID: Int] = [:] @ObservationIgnored private var tabIDByStandardizedFilePath: [String: UUID] = [:] @ObservationIgnored private var tabStateVersion: Int = 0 var selectedTab: TabData? { get { guard let selectedTabID, let index = tabIndexByID[selectedTabID], tabs.indices.contains(index) else { return nil } return tabs[index] } set { selectTab(id: newValue?.id) } } // Observable token for tab-array and tab-state changes when Combine publishers are unavailable. var tabsObservationToken: Int { tabStateVersion } private func tabIndex(for tabID: UUID) -> Int? { guard let index = tabIndexByID[tabID], tabs.indices.contains(index) else { return nil } return index } private static func normalizedFilePathKey(for url: URL?) -> String? { guard let url else { return nil } return url.resolvingSymlinksInPath().standardizedFileURL.path } private func rebuildTabIndexes() { tabIndexByID.removeAll(keepingCapacity: true) tabIDByStandardizedFilePath.removeAll(keepingCapacity: true) tabIndexByID.reserveCapacity(tabs.count) tabIDByStandardizedFilePath.reserveCapacity(tabs.count) for (index, tab) in tabs.enumerated() { tabIndexByID[tab.id] = index if let key = Self.normalizedFilePathKey(for: tab.fileURL), tabIDByStandardizedFilePath[key] == nil { tabIDByStandardizedFilePath[key] = tab.id } } } private func recordTabStateMutation(rebuildIndexes: Bool = false) { if rebuildIndexes { rebuildTabIndexes() } tabStateVersion &+= 1 } // Phase 1 command pipeline for tab-state mutations. private enum TabContentMutation: Sendable { case replaceAll(text: String, markDirty: Bool, compareIfLengthAtMost: Int?) case replaceRange(range: NSRange, replacement: String, markDirty: Bool) } struct RestoredTabSnapshot: Sendable { let name: String let content: String let language: String let fileURL: URL? let languageLocked: Bool let isDirty: Bool let lastSavedFingerprint: UInt64? let lastKnownFileModificationDate: Date? } private enum TabCommand: Sendable { case updateContent(tabID: UUID, mutation: TabContentMutation) case markSaved(tabID: UUID, fileURL: URL?, fingerprint: UInt64?, fileModificationDate: Date?) case setLanguage(tabID: UUID, language: String, lock: Bool) case closeTab(tabID: UUID) case addNewTab(name: String, language: String) case addPlaceholderTab( tabID: UUID, name: String, language: String, fileURL: URL?, languageLocked: Bool, isLargeCandidate: Bool ) case selectTab(tabID: UUID?) case resetTabs case restoreTabs(snapshots: [RestoredTabSnapshot], selectedIndex: Int?) case renameTab(tabID: UUID, name: String) case setLoading(tabID: UUID, isLoading: Bool) case setLargeFileCandidate(tabID: UUID, isLargeCandidate: Bool) case resetContentRevision(tabID: UUID) case applyLoadedTabState( tabID: UUID, content: String, language: String, languageLocked: Bool, fingerprint: UInt64?, fileModificationDate: Date?, isLargeCandidate: Bool ) } private struct TabCommandOutcome: Sendable { var index: Int? var tabID: UUID? var didChangeContent: Bool = false var contentRevision: Int? } private func dispatchTabCommandSerialized(_ command: TabCommand) async -> TabCommandOutcome { await tabCommandQueue.acquire() let outcome = applyTabCommand(command) await tabCommandQueue.release() return outcome } @discardableResult private func applyTabCommand(_ command: TabCommand) -> TabCommandOutcome { switch command { case let .updateContent(tabID, mutation): guard let index = tabIndex(for: tabID) else { return TabCommandOutcome() } var outcome = applyContentMutation(mutation, to: tabs[index]) outcome.index = index if outcome.didChangeContent { recordTabStateMutation() } return outcome case let .markSaved(tabID, fileURL, fingerprint, fileModificationDate): guard let index = tabIndex(for: tabID) else { return TabCommandOutcome() } let outcome = TabCommandOutcome(index: index) if let fileURL { tabs[index].fileURL = fileURL tabs[index].name = fileURL.lastPathComponent if let mapped = LanguageDetector.shared.preferredLanguage(for: fileURL) ?? languageMap[fileURL.pathExtension.lowercased()] { tabs[index].language = mapped tabs[index].languageLocked = true } } tabs[index].markClean(withFingerprint: fingerprint) tabs[index].updateLastKnownFileModificationDate(fileModificationDate) recordTabStateMutation(rebuildIndexes: true) return outcome case let .setLanguage(tabID, language, lock): guard let index = tabIndex(for: tabID) else { return TabCommandOutcome() } if tabs[index].language == language, tabs[index].languageLocked == lock { return TabCommandOutcome(index: index) } tabs[index].language = language tabs[index].languageLocked = lock recordTabStateMutation() return TabCommandOutcome(index: index) case let .closeTab(tabID): guard let index = tabIndex(for: tabID) else { return TabCommandOutcome() } cancelPendingLanguageDetection(for: tabID) tabs.remove(at: index) if tabs.isEmpty { let newTab = TabData( name: nextUntitledTabName(), content: "", language: defaultNewTabLanguage(), fileURL: nil, languageLocked: false ) tabs.append(newTab) selectedTabID = newTab.id } else if selectedTabID == tabID { selectedTabID = tabs.first?.id } recordTabStateMutation(rebuildIndexes: true) return TabCommandOutcome() case let .addNewTab(name, language): let newTab = TabData( name: name, content: "", language: language, fileURL: nil, languageLocked: false ) tabs.append(newTab) selectedTabID = newTab.id recordTabStateMutation(rebuildIndexes: true) return TabCommandOutcome(index: tabs.count - 1, tabID: newTab.id) case let .addPlaceholderTab(tabID, name, language, fileURL, languageLocked, isLargeCandidate): let tab = TabData( id: tabID, name: name, content: "", language: language, fileURL: fileURL, languageLocked: languageLocked, isDirty: false, lastSavedFingerprint: nil, isLoadingContent: true, isLargeFileCandidate: isLargeCandidate ) tabs.append(tab) selectedTabID = tab.id recordTabStateMutation(rebuildIndexes: true) return TabCommandOutcome(index: tabs.count - 1, tabID: tab.id) case let .selectTab(tabID): if selectedTabID == tabID { return TabCommandOutcome() } selectedTabID = tabID recordTabStateMutation() return TabCommandOutcome() case .resetTabs: for tab in tabs { cancelPendingLanguageDetection(for: tab.id) } tabs.removeAll(keepingCapacity: true) selectedTabID = nil recordTabStateMutation(rebuildIndexes: true) return TabCommandOutcome() case let .restoreTabs(snapshots, selectedIndex): for tab in tabs { cancelPendingLanguageDetection(for: tab.id) } tabs.removeAll(keepingCapacity: true) tabs.reserveCapacity(snapshots.count) for snapshot in snapshots { tabs.append( TabData( name: snapshot.name, content: snapshot.content, language: snapshot.language, fileURL: snapshot.fileURL, languageLocked: snapshot.languageLocked, isDirty: snapshot.isDirty, lastSavedFingerprint: snapshot.lastSavedFingerprint, lastKnownFileModificationDate: snapshot.lastKnownFileModificationDate ) ) } if let selectedIndex, tabs.indices.contains(selectedIndex) { selectedTabID = tabs[selectedIndex].id } else { selectedTabID = tabs.first?.id } recordTabStateMutation(rebuildIndexes: true) return TabCommandOutcome() case let .renameTab(tabID, name): guard let index = tabIndex(for: tabID) else { return TabCommandOutcome() } if tabs[index].name == name { return TabCommandOutcome(index: index) } tabs[index].name = name recordTabStateMutation() return TabCommandOutcome(index: index) case let .setLoading(tabID, isLoading): guard let index = tabIndex(for: tabID) else { return TabCommandOutcome() } if tabs[index].isLoadingContent == isLoading { return TabCommandOutcome(index: index) } tabs[index].isLoadingContent = isLoading recordTabStateMutation() return TabCommandOutcome(index: index) case let .setLargeFileCandidate(tabID, isLargeCandidate): guard let index = tabIndex(for: tabID) else { return TabCommandOutcome() } if tabs[index].isLargeFileCandidate == isLargeCandidate { return TabCommandOutcome(index: index) } tabs[index].isLargeFileCandidate = isLargeCandidate recordTabStateMutation() return TabCommandOutcome(index: index) case let .resetContentRevision(tabID): guard let index = tabIndex(for: tabID) else { return TabCommandOutcome() } if tabs[index].contentRevision == 0 { return TabCommandOutcome(index: index) } tabs[index].resetContentRevision() recordTabStateMutation() return TabCommandOutcome(index: index) case let .applyLoadedTabState(tabID, content, language, languageLocked, fingerprint, fileModificationDate, isLargeCandidate): guard let index = tabIndex(for: tabID) else { return TabCommandOutcome() } tabs[index].language = language tabs[index].languageLocked = languageLocked tabs[index].markClean(withFingerprint: fingerprint) tabs[index].updateLastKnownFileModificationDate(fileModificationDate) tabs[index].isLargeFileCandidate = isLargeCandidate let didChange = tabs[index].replaceContentStorage( with: content, markDirty: false, compareIfLengthAtMost: nil ) tabs[index].resetContentRevision() tabs[index].isLoadingContent = false recordTabStateMutation() return TabCommandOutcome(index: index, didChangeContent: didChange) } } private func applyContentMutation(_ mutation: TabContentMutation, to tab: TabData) -> TabCommandOutcome { switch mutation { case let .replaceAll(text, markDirty, compareIfLengthAtMost): let didChange = tab.replaceContentStorage( with: text, markDirty: markDirty, compareIfLengthAtMost: compareIfLengthAtMost ) return TabCommandOutcome( didChangeContent: didChange, contentRevision: didChange ? tab.contentRevision : nil ) case let .replaceRange(range, replacement, markDirty): let totalLength = tab.contentUTF16Length let safeLocation = min(max(0, range.location), totalLength) let maxLength = max(0, totalLength - safeLocation) let safeLength = min(max(0, range.length), maxLength) let safeRange = NSRange(location: safeLocation, length: safeLength) if safeRange.length == 0, replacement.isEmpty { return TabCommandOutcome() } let didChange = tab.replaceContent(in: safeRange, with: replacement, markDirty: markDirty) return TabCommandOutcome( didChangeContent: didChange, contentRevision: didChange ? tab.contentRevision : nil ) } } private let languageMap: [String: String] = [ "swift": "swift", "py": "python", "pyi": "python", "js": "javascript", "mjs": "javascript", "cjs": "javascript", "ts": "typescript", "tsx": "typescript", "php": "php", "phtml": "php", "csv": "csv", "tsv": "csv", "txt": "plain", "toml": "toml", "ini": "ini", "yaml": "yaml", "yml": "yaml", "xml": "xml", "sql": "sql", "log": "log", "vim": "vim", "ipynb": "ipynb", "java": "java", "kt": "kotlin", "kts": "kotlin", "go": "go", "rb": "ruby", "rs": "rust", "ps1": "powershell", "psm1": "powershell", "html": "html", "htm": "html", "ee": "expressionengine", "exp": "expressionengine", "tmpl": "expressionengine", "css": "css", "c": "c", "cpp": "cpp", "cc": "cpp", "hpp": "cpp", "hh": "cpp", "h": "cpp", "cs": "csharp", "m": "objective-c", "mm": "objective-c", "json": "json", "jsonc": "json", "json5": "json", "md": "markdown", "markdown": "markdown", "env": "dotenv", "proto": "proto", "graphql": "graphql", "gql": "graphql", "rst": "rst", "conf": "nginx", "nginx": "nginx", "cob": "cobol", "cbl": "cobol", "cobol": "cobol", "sh": "bash", "bash": "bash", "zsh": "zsh" ] init() { addNewTab() } private func nextUntitledTabName() -> String { "Untitled \(tabs.count + 1)" } // Creates and selects a new untitled tab. func addNewTab() { _ = applyTabCommand( .addNewTab( name: nextUntitledTabName(), language: defaultNewTabLanguage() ) ) } func selectTab(id: UUID?) { _ = applyTabCommand(.selectTab(tabID: id)) } func resetTabsForSessionRestore() { _ = applyTabCommand(.resetTabs) } func restoreTabsFromSnapshot(_ snapshots: [RestoredTabSnapshot], selectedIndex: Int?) { _ = applyTabCommand(.restoreTabs(snapshots: snapshots, selectedIndex: selectedIndex)) } // Renames an existing tab. func renameTab(tabID: UUID, newName: String) { _ = applyTabCommand(.renameTab(tabID: tabID, name: newName)) } func renameTab(tab: TabData, newName: String) { renameTab(tabID: tab.id, newName: newName) } // Updates tab text and applies language detection/locking heuristics. func updateTabContent(tab: TabData, content: String) { updateTabContent(tabID: tab.id, content: content) } // Tab-scoped content update API that centralizes dirty/idempotence behavior. func updateTabContent(tabID: UUID, content: String) { guard let index = tabIndex(for: tabID) else { return } if tabs[index].isLoadingContent { // During staged file load, content updates are system-driven; do not mark dirty. _ = applyTabCommand( .updateContent( tabID: tabID, mutation: .replaceAll( text: content, markDirty: false, compareIfLengthAtMost: nil ) ) ) return } let outcome = applyTabCommand( .updateContent( tabID: tabID, mutation: .replaceAll( text: content, markDirty: true, compareIfLengthAtMost: Self.deferredLanguageDetectionUTF16Length ) ) ) guard outcome.didChangeContent, let commandIndex = outcome.index, let contentRevision = outcome.contentRevision else { return } handleLanguageMetadataAfterMutation( tabID: tabID, tabIndex: commandIndex, contentRevision: contentRevision, contentSnapshot: content ) } // Incremental piece-table mutation path used by the editor delegates for large content responsiveness. func applyTabContentEdit(tabID: UUID, range: NSRange, replacement: String) { guard let index = tabIndex(for: tabID) else { return } guard !tabs[index].isLoadingContent else { return } let outcome = applyTabCommand( .updateContent( tabID: tabID, mutation: .replaceRange( range: range, replacement: replacement, markDirty: true ) ) ) guard outcome.didChangeContent, let commandIndex = outcome.index, let contentRevision = outcome.contentRevision else { return } handleLanguageMetadataAfterMutation( tabID: tabID, tabIndex: commandIndex, contentRevision: contentRevision, contentSnapshot: nil ) } // Manually sets language and locks automatic switching. func updateTabLanguage(tab: TabData, language: String) { updateTabLanguage(tabID: tab.id, language: language) } func setTabLanguage(tabID: UUID, language: String, lock: Bool) { _ = applyTabCommand(.setLanguage(tabID: tabID, language: language, lock: lock)) } func updateTabLanguage(tabID: UUID, language: String) { setTabLanguage(tabID: tabID, language: language, lock: true) } // Closes a tab while guaranteeing one tab remains open. func closeTab(tabID: UUID) { _ = applyTabCommand(.closeTab(tabID: tabID)) } func closeTab(tab: TabData) { closeTab(tabID: tab.id) } // Saves tab content to the existing file URL or falls back to Save As. func saveFile(tabID: UUID, allowExternalOverwrite: Bool = false) { guard let index = tabIndex(for: tabID) else { return } if !allowExternalOverwrite, let conflict = detectExternalConflict(for: tabs[index]) { pendingExternalFileConflict = conflict return } if let url = tabs[index].fileURL { enqueueSave(tabID: tabID, to: url, updateFileURLOnSuccess: nil, signpostName: "save_file") } else { saveFileAs(tabID: tabID) } } func saveFile(tab: TabData) { saveFile(tabID: tab.id) } func resolveExternalConflictByKeepingLocal(tabID: UUID) { pendingExternalFileConflict = nil saveFile(tabID: tabID, allowExternalOverwrite: true) } func resolveExternalConflictByReloadingDisk(tabID: UUID) { pendingExternalFileConflict = nil guard let index = tabIndex(for: tabID), let url = tabs[index].fileURL else { return } let isLargeCandidate = tabs[index].isLargeFileCandidate let extLangHint = LanguageDetector.shared.preferredLanguage(for: url) ?? languageMap[url.pathExtension.lowercased()] _ = applyTabCommand(.setLoading(tabID: tabID, isLoading: true)) EditorPerformanceMonitor.shared.beginFileOpen(tabID: tabID) Task { [weak self] in guard let self else { return } do { let loadResult = try await Self.loadFileResult( from: url, extLangHint: extLangHint, isLargeCandidate: isLargeCandidate ) await self.applyLoadedContent(tabID: tabID, result: loadResult) } catch { await self.markTabLoadFailed(tabID: tabID) } } } func externalConflictComparisonSnapshot(tabID: UUID) async -> ExternalFileComparisonSnapshot? { guard let index = tabIndex(for: tabID), let url = tabs[index].fileURL else { return nil } let fileName = tabs[index].name let localContent = tabs[index].content return await Task.detached(priority: .utility) { let data = (try? Data(contentsOf: url, options: [.mappedIfSafe])) ?? Data() let diskContent = String(decoding: data, as: UTF8.self) return ExternalFileComparisonSnapshot( fileName: fileName, localContent: localContent, diskContent: diskContent ) }.value } func refreshExternalConflictForTab(tabID: UUID) { guard let index = tabIndex(for: tabID) else { return } pendingExternalFileConflict = detectExternalConflict(for: tabs[index]) } // Saves tab content to a user-selected path on macOS. func saveFileAs(tabID: UUID) { guard let index = tabIndex(for: tabID) else { return } #if os(macOS) let panel = NSSavePanel() panel.nameFieldStringValue = tabs[index].name let mdType = UTType(filenameExtension: "md") ?? .plainText panel.allowedContentTypes = [ .text, .swiftSource, .pythonScript, .javaScript, .html, .css, .cSource, .json, mdType ] if panel.runModal() == .OK, let url = panel.url { enqueueSave(tabID: tabID, to: url, updateFileURLOnSuccess: url, signpostName: "save_file_as") } #else // iOS/iPadOS: explicit Save As panel is not available here yet. // Keep document dirty so user can export/share via future document APIs. debugLog("Save As is currently only available on macOS.") #endif } func saveFileAs(tab: TabData) { saveFileAs(tabID: tab.id) } private func enqueueSave(tabID: UUID, to destinationURL: URL, updateFileURLOnSuccess: URL?, signpostName: StaticString) { guard let index = tabIndex(for: tabID) else { return } let snapshotContent = tabs[index].content let snapshotRevision = tabs[index].contentRevision let snapshotLastSavedFingerprint = tabs[index].lastSavedFingerprint Task { [weak self] in guard let self else { return } let saveInterval = Self.saveSignposter.beginInterval(signpostName) defer { Self.saveSignposter.endInterval(signpostName, saveInterval) } let payload = await Self.prepareSavePayload(from: snapshotContent) guard let preflightIndex = self.tabIndex(for: tabID), self.tabs[preflightIndex].contentRevision == snapshotRevision else { return } let normalizationOutcome = self.applyTabCommand( .updateContent( tabID: tabID, mutation: .replaceAll( text: payload.content, markDirty: false, compareIfLengthAtMost: Self.deferredLanguageDetectionUTF16Length ) ) ) let expectedRevision = normalizationOutcome.contentRevision ?? snapshotRevision if snapshotLastSavedFingerprint == payload.fingerprint, FileManager.default.fileExists(atPath: destinationURL.path) { if let finalIndex = self.tabIndex(for: tabID), self.tabs[finalIndex].contentRevision == expectedRevision { let fileModificationDate = try? destinationURL.resourceValues(forKeys: [.contentModificationDateKey]).contentModificationDate _ = self.applyTabCommand( .markSaved( tabID: tabID, fileURL: updateFileURLOnSuccess, fingerprint: payload.fingerprint, fileModificationDate: fileModificationDate ) ) self.pendingExternalFileConflict = nil } return } do { try await Self.writeFileContent(payload.content, to: destinationURL) } catch { self.debugLog("Failed to save file.") return } guard let finalIndex = self.tabIndex(for: tabID), self.tabs[finalIndex].contentRevision == expectedRevision else { return } let fileModificationDate = try? destinationURL.resourceValues(forKeys: [.contentModificationDateKey]).contentModificationDate _ = self.applyTabCommand( .markSaved( tabID: tabID, fileURL: updateFileURLOnSuccess, fingerprint: payload.fingerprint, fileModificationDate: fileModificationDate ) ) self.pendingExternalFileConflict = nil } } private func detectExternalConflict(for tab: TabData) -> ExternalFileConflictState? { guard tab.isDirty, let fileURL = tab.fileURL else { return nil } guard let diskModifiedAt = try? fileURL.resourceValues(forKeys: [.contentModificationDateKey]).contentModificationDate else { return nil } guard let known = tab.lastKnownFileModificationDate else { return nil } if diskModifiedAt.timeIntervalSince(known) > 0.5 { return ExternalFileConflictState(tabID: tab.id, fileURL: fileURL, diskModifiedAt: diskModifiedAt) } return nil } // Opens file-picker UI on macOS. func openFile() { #if os(macOS) let panel = NSOpenPanel() // Allow opening any file type, including hidden dotfiles like .zshrc panel.allowedContentTypes = [] panel.allowsOtherFileTypes = true panel.allowsMultipleSelection = true panel.canChooseDirectories = false panel.showsHiddenFiles = true if panel.runModal() == .OK { let urls = panel.urls for url in urls { openFile(url: url) } } #else // iOS/iPadOS: document picker flow can be added here. debugLog("Open File panel is currently only available on macOS.") #endif } // Loads a file into a new tab unless the file is already open. func openFile(url: URL) { if focusTabIfOpen(for: url) { return } let extLangHint = LanguageDetector.shared.preferredLanguage(for: url) ?? languageMap[url.pathExtension.lowercased()] let fileSize = (try? url.resourceValues(forKeys: [.fileSizeKey]).fileSize) ?? 0 let isLargeCandidate = fileSize >= EditorLoadHelper.largeFileCandidateByteThreshold let tabID = UUID() _ = applyTabCommand( .addPlaceholderTab( tabID: tabID, name: url.lastPathComponent, language: extLangHint ?? "plain", fileURL: url, languageLocked: extLangHint != nil, isLargeCandidate: isLargeCandidate ) ) EditorPerformanceMonitor.shared.beginFileOpen(tabID: tabID) Task { [weak self] in guard let self else { return } do { let loadResult = try await Self.loadFileResult( from: url, extLangHint: extLangHint, isLargeCandidate: isLargeCandidate ) await self.applyLoadedContent(tabID: tabID, result: loadResult) } catch { await self.markTabLoadFailed(tabID: tabID) } } } private nonisolated static func contentFingerprintValue(_ text: String) -> UInt64 { var hasher = Hasher() hasher.combine(text) let value = hasher.finalize() return UInt64(bitPattern: Int64(value)) } private nonisolated static func loadFileResult( from url: URL, extLangHint: String?, isLargeCandidate: Bool ) async throws -> EditorFileLoadResult { try await Task.detached(priority: .userInitiated) { let didStartScopedAccess = url.startAccessingSecurityScopedResource() defer { if didStartScopedAccess { url.stopAccessingSecurityScopedResource() } } let initialModificationDate = try? url.resourceValues(forKeys: [.contentModificationDateKey]).contentModificationDate let data: Data if isLargeCandidate { // Prefer memory-mapped IO for very large files to reduce peak memory churn. // Fall back to streaming if mapping is unavailable for the provider. if let mapped = try? Data(contentsOf: url, options: [.mappedIfSafe]) { data = mapped } else { data = try EditorLoadHelper.streamFileData(from: url) } } else { data = try Data(contentsOf: url, options: [.mappedIfSafe]) } let raw = String(decoding: data, as: UTF8.self) let content = EditorLoadHelper.sanitizeTextForFileLoad( raw, useFastPath: data.count >= EditorLoadHelper.fastLoadSanitizeByteThreshold ) let detectedLanguage = extLangHint ?? "plain" let fingerprint: UInt64? = data.count >= EditorLoadHelper.skipFingerprintByteThreshold ? nil : Self.contentFingerprintValue(content) return EditorFileLoadResult( content: content, detectedLanguage: detectedLanguage, languageLocked: extLangHint != nil, fingerprint: fingerprint, fileModificationDate: initialModificationDate, isLargeCandidate: data.count >= EditorLoadHelper.largeFileCandidateByteThreshold ) }.value } private nonisolated static func prepareSavePayload(from content: String) async -> EditorFileSavePayload { await Task.detached(priority: .userInitiated) { // Keep save path non-destructive: only normalize line endings and strip NUL. let clean = content .replacingOccurrences(of: "\0", with: "") .replacingOccurrences(of: "\r\n", with: "\n") .replacingOccurrences(of: "\r", with: "\n") return EditorFileSavePayload( content: clean, fingerprint: Self.contentFingerprintValue(clean) ) }.value } private nonisolated static func writeFileContent(_ content: String, to url: URL) async throws { try await Task.detached(priority: .utility) { try content.write(to: url, atomically: true, encoding: .utf8) }.value } private func applyLoadedContent( tabID: UUID, result: EditorFileLoadResult ) async { cancelPendingLanguageDetection(for: tabID) _ = await dispatchTabCommandSerialized( .applyLoadedTabState( tabID: tabID, content: result.content, language: result.detectedLanguage, languageLocked: result.languageLocked, fingerprint: result.fingerprint, fileModificationDate: result.fileModificationDate, isLargeCandidate: result.isLargeCandidate ) ) EditorPerformanceMonitor.shared.endFileOpen( tabID: tabID, success: true, byteCount: result.content.lengthOfBytes(using: .utf8) ) } private func markTabLoadFailed(tabID: UUID) async { _ = await dispatchTabCommandSerialized(.setLoading(tabID: tabID, isLoading: false)) EditorPerformanceMonitor.shared.endFileOpen(tabID: tabID, success: false, byteCount: nil) debugLog("Failed to open file.") } private func contentFingerprint(_ text: String) -> UInt64 { Self.contentFingerprintValue(text) } private func cancelPendingLanguageDetection(for tabID: UUID) { pendingLanguageDetectionTasks[tabID]?.cancel() pendingLanguageDetectionTasks[tabID] = nil } private func handleLanguageMetadataAfterMutation( tabID: UUID, tabIndex index: Int, contentRevision: Int, contentSnapshot: String? ) { if tabs[index].contentUTF16Length >= Self.largeContentLanguageBypassUTF16Length { cancelPendingLanguageDetection(for: tabID) applyLargeContentLanguageHintIfNeeded(at: index) return } if tabs[index].contentUTF16Length >= Self.deferredLanguageDetectionUTF16Length { scheduleDeferredLanguageDetection(for: tabID, expectedContentRevision: contentRevision) return } cancelPendingLanguageDetection(for: tabID) let content = contentSnapshot ?? tabs[index].content applyLanguageDetectionHeuristics(at: index, content: content) } private func scheduleDeferredLanguageDetection(for tabID: UUID, expectedContentRevision: Int) { cancelPendingLanguageDetection(for: tabID) let task = Task { [weak self] in try? await Task.sleep(nanoseconds: Self.deferredLanguageDetectionDelayNanos) guard !Task.isCancelled else { return } await MainActor.run { self?.runDeferredLanguageDetection(tabID: tabID, expectedContentRevision: expectedContentRevision) } } pendingLanguageDetectionTasks[tabID] = task } private func runDeferredLanguageDetection(tabID: UUID, expectedContentRevision: Int) { guard let index = tabIndex(for: tabID) else { return } guard !tabs[index].isLoadingContent else { return } guard tabs[index].contentRevision == expectedContentRevision else { return } if tabs[index].contentUTF16Length >= Self.largeContentLanguageBypassUTF16Length { applyLargeContentLanguageHintIfNeeded(at: index) return } let content = sampledContentForLanguageDetection(tabs[index].content) applyLanguageDetectionHeuristics(at: index, content: content) } private func sampledContentForLanguageDetection(_ content: String) -> String { let ns = content as NSString if ns.length <= Self.deferredLanguageDetectionSampleUTF16Length { return content } return ns.substring(to: Self.deferredLanguageDetectionSampleUTF16Length) } private func applyLargeContentLanguageHintIfNeeded(at index: Int) { let tabID = tabs[index].id let nameExt = URL(fileURLWithPath: tabs[index].name).pathExtension.lowercased() if !tabs[index].languageLocked, let mapped = LanguageDetector.shared.preferredLanguage(for: tabs[index].fileURL) ?? languageMap[nameExt] { _ = applyTabCommand(.setLanguage(tabID: tabID, language: mapped, lock: false)) } } private func applyLanguageDetectionHeuristics(at index: Int, content: String) { let tabID = tabs[index].id // Early lock to Swift if clearly Swift-specific tokens are present. let lower = content.lowercased() let swiftStrongTokens: Bool = ( lower.contains(" import swiftui") || lower.hasPrefix("import swiftui") || lower.contains("@main") || lower.contains(" final class ") || lower.contains("public final class ") || lower.contains(": view") || lower.contains("@published") || lower.contains("@stateobject") || lower.contains("@mainactor") || lower.contains("protocol ") || lower.contains("extension ") || lower.contains("import appkit") || lower.contains("import uikit") || lower.contains("import foundationmodels") || lower.contains("guard ") || lower.contains("if let ") ) if swiftStrongTokens { _ = applyTabCommand(.setLanguage(tabID: tabID, language: "swift", lock: true)) return } guard !tabs[index].languageLocked else { return } let nameExt = URL(fileURLWithPath: tabs[index].name).pathExtension.lowercased() if let extLang = languageMap[nameExt], !extLang.isEmpty { // If extension says C# but content looks Swift-ish, prefer Swift. if extLang == "csharp" { let looksSwift = lower.contains("import swiftui") || lower.contains(": view") || lower.contains("@main") || lower.contains(" final class ") if looksSwift { _ = applyTabCommand(.setLanguage(tabID: tabID, language: "swift", lock: true)) } else { _ = applyTabCommand(.setLanguage(tabID: tabID, language: extLang, lock: true)) } } else { _ = applyTabCommand(.setLanguage(tabID: tabID, language: extLang, lock: true)) } return } let result = LanguageDetector.shared.detect(text: content, name: tabs[index].name, fileURL: tabs[index].fileURL) let detected = result.lang let scores = result.scores let current = tabs[index].language let swiftScore = scores["swift"] ?? 0 let csharpScore = scores["csharp"] ?? 0 let swiftStrongContext: Bool = ( lower.contains(" final class ") || lower.contains("public final class ") || lower.contains(": view") || lower.contains("@published") || lower.contains("@stateobject") || lower.contains("@mainactor") || lower.contains("protocol ") || lower.contains("extension ") || lower.contains("import swiftui") || lower.contains("import appkit") || lower.contains("import uikit") || lower.contains("import foundationmodels") || lower.contains("guard ") || lower.contains("if let ") ) let hasUsingSystem = lower.contains("\nusing system;") || lower.contains("\nusing system.") let hasNamespace = lower.contains("\nnamespace ") let hasMainMethod = lower.contains("static void main(") || lower.contains("static int main(") let hasCSharpAttributes = (lower.contains("\n[") && lower.contains("]\n") && !lower.contains("@")) let csharpContext = hasUsingSystem || hasNamespace || hasMainMethod || hasCSharpAttributes // Avoid switching from Swift to C# unless there is very strong C# evidence and margin. if current == "swift" && detected == "csharp" { let requireMargin = 25 if swiftStrongContext && !csharpContext { return } if !(csharpContext && csharpScore >= swiftScore + requireMargin) { return } _ = applyTabCommand(.setLanguage(tabID: tabID, language: "csharp", lock: false)) return } // Never downgrade to plain while typing when a concrete language is already active. if detected == "plain" && current != "plain" { return } _ = applyTabCommand(.setLanguage(tabID: tabID, language: detected, lock: false)) if detected == "swift" && (result.confidence >= 5 || swiftStrongContext) { _ = applyTabCommand(.setLanguage(tabID: tabID, language: detected, lock: true)) } } func hasOpenFile(url: URL) -> Bool { indexOfOpenTab(for: url) != nil } // Focuses an existing tab for URL if present. func focusTabIfOpen(for url: URL) -> Bool { if let existingIndex = indexOfOpenTab(for: url) { _ = applyTabCommand(.selectTab(tabID: tabs[existingIndex].id)) return true } return false } private func indexOfOpenTab(for url: URL) -> Int? { guard let key = Self.normalizedFilePathKey(for: url), let tabID = tabIDByStandardizedFilePath[key] else { return nil } return tabIndex(for: tabID) } // Marks a tab clean after successful save/export and updates URL-derived metadata. func markTabSaved(tabID: UUID, fileURL: URL? = nil) { guard let index = tabIndex(for: tabID) else { return } _ = applyTabCommand( .markSaved( tabID: tabID, fileURL: fileURL, fingerprint: contentFingerprint(tabs[index].content), fileModificationDate: fileURL.flatMap { try? $0.resourceValues(forKeys: [.contentModificationDateKey]).contentModificationDate } ) ) } // Returns whitespace-delimited word count for status display. func wordCount(for text: String) -> Int { text.split(whereSeparator: \.isWhitespace).count } private func debugLog(_ message: String) { #if DEBUG print(message) #endif } // Reads user preference for default language of newly created tabs. private func defaultNewTabLanguage() -> String { let stored = UserDefaults.standard.string(forKey: "SettingsDefaultNewFileLanguage") ?? "plain" let trimmed = stored.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() return trimmed.isEmpty ? "plain" : trimmed } }