// ContentView.swift // Main SwiftUI container for Neon Vision Editor. Hosts the single-document editor UI, // toolbar actions, AI integration, syntax highlighting, line numbers, and sidebar TOC. ///MARK: - Imports import SwiftUI import Foundation import Observation import UniformTypeIdentifiers import OSLog import Dispatch #if os(macOS) import AppKit #elseif canImport(UIKit) import UIKit #endif #if USE_FOUNDATION_MODELS && canImport(FoundationModels) import FoundationModels #endif #if os(macOS) private final class WindowCloseConfirmationDelegate: NSObject, NSWindowDelegate { nonisolated(unsafe) weak var forwardedDelegate: NSWindowDelegate? var shouldConfirm: (() -> Bool)? var hasDirtyTabs: (() -> Bool)? var saveAllDirtyTabs: (() -> Bool)? var dialogTitle: (() -> String)? var dialogMessage: (() -> String)? private var isPromptInFlight = false private var allowNextClose = false nonisolated override func responds(to selector: Selector!) -> Bool { super.responds(to: selector) || (forwardedDelegate?.responds(to: selector) ?? false) } nonisolated override func forwardingTarget(for selector: Selector!) -> Any? { if forwardedDelegate?.responds(to: selector) == true { return forwardedDelegate } return super.forwardingTarget(for: selector) } func windowShouldClose(_ sender: NSWindow) -> Bool { if allowNextClose { allowNextClose = false return forwardedDelegate?.windowShouldClose?(sender) ?? true } let needsPrompt = shouldConfirm?() == true && hasDirtyTabs?() == true if !needsPrompt { return forwardedDelegate?.windowShouldClose?(sender) ?? true } if isPromptInFlight { return false } isPromptInFlight = true let alert = NSAlert() alert.messageText = dialogTitle?() ?? "Save changes before closing?" alert.informativeText = dialogMessage?() ?? "One or more tabs have unsaved changes." alert.alertStyle = .warning alert.addButton(withTitle: "Save") alert.addButton(withTitle: "Don't Save") alert.addButton(withTitle: "Cancel") alert.beginSheetModal(for: sender) { [weak self] response in guard let self else { return } self.isPromptInFlight = false switch response { case .alertFirstButtonReturn: if self.saveAllDirtyTabs?() == true { self.allowNextClose = true sender.performClose(nil) } case .alertSecondButtonReturn: self.allowNextClose = true sender.performClose(nil) default: break } } return false } } #endif // Utility: quick width calculation for strings with a given font (AppKit-based) extension String { #if os(macOS) func width(usingFont font: NSFont) -> CGFloat { let attributes = [NSAttributedString.Key.font: font] let size = (self as NSString).size(withAttributes: attributes) return size.width } #endif } ///MARK: - Root View //Manages the editor area, toolbar, popovers, and bridges to the view model for file I/O and metrics. struct ContentView: View { enum StartupBehavior { case standard case forceBlankDocument case safeMode } enum ProjectNavigatorPlacement: String, CaseIterable, Identifiable { case leading case trailing var id: String { rawValue } } enum PerformancePreset: String, CaseIterable, Identifiable { case balanced case largeFiles case battery var id: String { rawValue } } enum DelimitedViewMode: String, CaseIterable, Identifiable { case table case text var id: String { rawValue } } struct DelimitedTableSnapshot: Sendable { let header: [String] let rows: [[String]] let totalRows: Int let displayedRows: Int let truncated: Bool } struct DelimitedTableParseError: LocalizedError { let message: String var errorDescription: String? { message } } let startupBehavior: StartupBehavior let safeModeMessage: String? init(startupBehavior: StartupBehavior = .standard, safeModeMessage: String? = nil) { self.startupBehavior = startupBehavior self.safeModeMessage = safeModeMessage } private enum EditorPerformanceThresholds { static let largeFileBytes = 12_000_000 static let largeFileBytesHTMLCSV = 4_000_000 static let largeFileBytesMobile = 8_000_000 static let largeFileBytesHTMLCSVMobile = 3_000_000 static let heavyFeatureUTF16Length = 450_000 static let largeFileLineBreaks = 40_000 static let largeFileLineBreaksHTMLCSV = 15_000 static let largeFileLineBreaksMobile = 25_000 static let largeFileLineBreaksHTMLCSVMobile = 10_000 } private static let completionSignposter = OSSignposter(subsystem: "h3p.Neon-Vision-Editor", category: "InlineCompletion") private struct CompletionCacheEntry { let suggestion: String let createdAt: Date } private struct SavedDraftTabSnapshot: Codable { let name: String let content: String let language: String let fileURLString: String? } private struct SavedDraftSnapshot: Codable { let tabs: [SavedDraftTabSnapshot] let selectedIndex: Int? let createdAt: Date } // Environment-provided view model and theme/error bindings @Environment(EditorViewModel.self) var viewModel @EnvironmentObject private var supportPurchaseManager: SupportPurchaseManager @EnvironmentObject var appUpdateManager: AppUpdateManager @Environment(\.colorScheme) var colorScheme #if os(iOS) @Environment(\.horizontalSizeClass) var horizontalSizeClass #endif #if os(macOS) @Environment(\.openWindow) var openWindow @Environment(\.openSettings) var openSettingsAction #endif @Environment(\.showGrokError) var showGrokError @Environment(\.grokErrorMessage) var grokErrorMessage // Single-document fallback state (used when no tab model is selected) @AppStorage("SelectedAIModel") private var selectedModelRaw: String = AIModel.appleIntelligence.rawValue @State var singleContent: String = "" @State var singleLanguage: String = "plain" @State var caretStatus: String = "Ln 1, Col 1" @AppStorage("SettingsEditorFontSize") var editorFontSize: Double = 14 @AppStorage("SettingsEditorFontName") var editorFontName: String = "" @AppStorage("SettingsLineHeight") var editorLineHeight: Double = 1.0 @AppStorage("SettingsShowLineNumbers") var showLineNumbers: Bool = true @AppStorage("SettingsHighlightCurrentLine") var highlightCurrentLine: Bool = false @AppStorage("SettingsHighlightMatchingBrackets") var highlightMatchingBrackets: Bool = false @AppStorage("SettingsShowScopeGuides") var showScopeGuides: Bool = false @AppStorage("SettingsHighlightScopeBackground") var highlightScopeBackground: Bool = false @AppStorage("SettingsLineWrapEnabled") var settingsLineWrapEnabled: Bool = false // Removed showHorizontalRuler and showVerticalRuler AppStorage properties @AppStorage("SettingsIndentStyle") var indentStyle: String = "spaces" @AppStorage("SettingsIndentWidth") var indentWidth: Int = 4 @AppStorage("SettingsAutoIndent") var autoIndentEnabled: Bool = true @AppStorage("SettingsAutoCloseBrackets") var autoCloseBracketsEnabled: Bool = false @AppStorage("SettingsTrimTrailingWhitespace") var trimTrailingWhitespaceEnabled: Bool = false @AppStorage("SettingsCompletionEnabled") var isAutoCompletionEnabled: Bool = false @AppStorage("SettingsCompletionFromDocument") var completionFromDocument: Bool = false @AppStorage("SettingsCompletionFromSyntax") var completionFromSyntax: Bool = false @AppStorage("SettingsReopenLastSession") var reopenLastSession: Bool = true @AppStorage("SettingsOpenWithBlankDocument") var openWithBlankDocument: Bool = true @AppStorage("SettingsConfirmCloseDirtyTab") var confirmCloseDirtyTab: Bool = true @AppStorage("SettingsConfirmClearEditor") var confirmClearEditor: Bool = true @AppStorage("SettingsActiveTab") var settingsActiveTab: String = "general" @AppStorage("SettingsAppearance") var appearance: String = "system" @AppStorage("SettingsTemplateLanguage") private var settingsTemplateLanguage: String = "swift" @AppStorage("SettingsThemeName") private var settingsThemeName: String = "Neon Glow" @AppStorage("SettingsThemeBoldKeywords") private var settingsThemeBoldKeywords: Bool = false @AppStorage("SettingsThemeItalicComments") private var settingsThemeItalicComments: Bool = false @AppStorage("SettingsThemeUnderlineLinks") private var settingsThemeUnderlineLinks: Bool = false @AppStorage("SettingsThemeBoldMarkdownHeadings") private var settingsThemeBoldMarkdownHeadings: Bool = false @State var lastProviderUsed: String = "Apple" @State private var highlightRefreshToken: Int = 0 @State var editorExternalMutationRevision: Int = 0 // Persisted API tokens for external providers @State var grokAPIToken: String = "" @State var openAIAPIToken: String = "" @State var geminiAPIToken: String = "" @State var anthropicAPIToken: String = "" // Debounce/cancellation handles for inline completion @State private var completionDebounceTask: Task? @State private var completionTask: Task? @State private var lastCompletionTriggerSignature: String = "" @State private var isApplyingCompletion: Bool = false @State private var completionCache: [String: CompletionCacheEntry] = [:] @State private var pendingHighlightRefresh: DispatchWorkItem? #if os(iOS) @AppStorage("EnableTranslucentWindow") var enableTranslucentWindow: Bool = true #else @AppStorage("EnableTranslucentWindow") var enableTranslucentWindow: Bool = false #endif #if os(iOS) @State private var previousKeyboardAccessoryVisibility: Bool? = nil @State var markdownPreviewSheetDetent: PresentationDetent = .medium #endif #if os(macOS) @AppStorage("SettingsMacTranslucencyMode") private var macTranslucencyModeRaw: String = "balanced" #endif @State var showFindReplace: Bool = false @State var showSettingsSheet: Bool = false @State var showUpdateDialog: Bool = false @State var findQuery: String = "" @State var replaceQuery: String = "" @State var findUsesRegex: Bool = false @State var findCaseSensitive: Bool = false @State var findStatusMessage: String = "" @State var iOSFindCursorLocation: Int = 0 @State var iOSLastFindFingerprint: String = "" @State var showProjectStructureSidebar: Bool = false @State var showCompactSidebarSheet: Bool = false @State var showCompactProjectSidebarSheet: Bool = false @State var projectRootFolderURL: URL? = nil @State var projectTreeNodes: [ProjectTreeNode] = [] @State var projectTreeRefreshGeneration: Int = 0 @AppStorage("SettingsShowSupportedProjectFilesOnly") var showSupportedProjectFilesOnly: Bool = true @State var projectOverrideIndentWidth: Int? = nil @State var projectOverrideLineWrapEnabled: Bool? = nil @State var showProjectFolderPicker: Bool = false @State var projectFolderSecurityURL: URL? = nil @State var pendingCloseTabID: UUID? = nil @State var showUnsavedCloseDialog: Bool = false @State var showCloseAllTabsDialog: Bool = false @State private var showExternalConflictDialog: Bool = false @State private var showExternalConflictCompareSheet: Bool = false @State private var externalConflictCompareSnapshot: EditorViewModel.ExternalFileComparisonSnapshot? @State var showClearEditorConfirmDialog: Bool = false @State var showIOSFileImporter: Bool = false @State var showIOSFileExporter: Bool = false @State var showUnsupportedFileAlert: Bool = false @State var unsupportedFileName: String = "" @State var iosExportDocument: PlainTextDocument = PlainTextDocument(text: "") @State var iosExportFilename: String = "Untitled.txt" @State var iosExportTabID: UUID? = nil @State var showMarkdownPDFExporter: Bool = false @State var markdownPDFExportDocument: PDFExportDocument = PDFExportDocument() @State var markdownPDFExportFilename: String = "Markdown-Preview.pdf" @State var markdownPDFExportErrorMessage: String? @State var showQuickSwitcher: Bool = false @State var quickSwitcherQuery: String = "" @State var quickSwitcherProjectFileURLs: [URL] = [] @State var projectFileIndexSnapshot: ProjectFileIndex.Snapshot = .empty @State var isProjectFileIndexing: Bool = false @State var projectFileIndexRefreshGeneration: Int = 0 @State var projectFileIndexTask: Task? = nil @State var projectFolderMonitorSource: DispatchSourceFileSystemObject? = nil @State var pendingProjectFolderRefreshWorkItem: DispatchWorkItem? = nil @State private var quickSwitcherRecentItemIDs: [String] = [] @State var recentFilesRefreshToken: UUID = UUID() @State private var currentSelectionSnapshotText: String = "" @State private var codeSnapshotPayload: CodeSnapshotPayload? @State var showFindInFiles: Bool = false @State var findInFilesQuery: String = "" @State var findInFilesCaseSensitive: Bool = false @State var findInFilesResults: [FindInFilesMatch] = [] @State var findInFilesStatusMessage: String = "" @State private var findInFilesTask: Task? @State private var statusWordCount: Int = 0 @State private var statusLineCount: Int = 1 @State private var wordCountTask: Task? @AppStorage("EditorVimModeEnabled") var vimModeEnabled: Bool = false @State var vimInsertMode: Bool = true @State var safeModeRecoveryPreparedForNextLaunch: Bool = false @State var droppedFileLoadInProgress: Bool = false @State var droppedFileProgressDeterminate: Bool = true @State var droppedFileLoadProgress: Double = 0 @State var droppedFileLoadLabel: String = "" @State var largeFileModeEnabled: Bool = false @SceneStorage("ProjectSidebarWidth") private var projectSidebarWidth: Double = 260 @State private var projectSidebarResizeStartWidth: CGFloat? = nil @State private var delimitedViewMode: DelimitedViewMode = .table @State private var delimitedTableSnapshot: DelimitedTableSnapshot? = nil @State private var isBuildingDelimitedTable: Bool = false @State private var delimitedTableStatus: String = "" @State private var delimitedParseTask: Task? = nil @AppStorage("SettingsProjectNavigatorPlacement") var projectNavigatorPlacementRaw: String = ProjectNavigatorPlacement.trailing.rawValue @AppStorage("SettingsPerformancePreset") var performancePresetRaw: String = PerformancePreset.balanced.rawValue @AppStorage("SettingsLargeFileOpenMode") private var largeFileOpenModeRaw: String = "deferred" @AppStorage("SettingsRemoteSessionsEnabled") private var remoteSessionsEnabled: Bool = false @AppStorage("SettingsRemotePreparedTarget") private var remotePreparedTarget: String = "" @State private var remoteSessionStore = RemoteSessionStore.shared #if os(iOS) @AppStorage("SettingsForceLargeFileMode") var forceLargeFileMode: Bool = false @AppStorage("SettingsShowKeyboardAccessoryBarIOS") var showKeyboardAccessoryBarIOS: Bool = false @AppStorage("SettingsShowBottomActionBarIOS") var showBottomActionBarIOS: Bool = true @AppStorage("SettingsUseLiquidGlassToolbarIOS") var shouldUseLiquidGlass: Bool = true @AppStorage("SettingsToolbarIconsBlueIOS") var toolbarIconsBlueIOS: Bool = false #endif @AppStorage("HasSeenWelcomeTourV1") var hasSeenWelcomeTourV1: Bool = false @AppStorage("WelcomeTourSeenRelease") var welcomeTourSeenRelease: String = "" @AppStorage("AppLaunchCountV1") var appLaunchCount: Int = 0 @AppStorage("HasShownSupportPromptV1") var hasShownSupportPromptV1: Bool = false @State var showWelcomeTour: Bool = false @State var showEditorHelp: Bool = false @State var showSupportPromptSheet: Bool = false #if os(macOS) @State private var hostWindowNumber: Int? = nil @AppStorage("ShowBracketHelperBarMac") var showBracketHelperBarMac: Bool = false @AppStorage("SettingsToolbarSymbolsColorMac") var toolbarSymbolsColorMacRaw: String = "blue" @State private var windowCloseConfirmationDelegate: WindowCloseConfirmationDelegate? = nil #endif @State var showMarkdownPreviewPane: Bool = false #if os(macOS) @AppStorage("MarkdownPreviewTemplateMac") var markdownPreviewTemplateRaw: String = "default" #elseif os(iOS) @AppStorage("MarkdownPreviewTemplateIOS") var markdownPreviewTemplateRaw: String = "default" #endif @AppStorage("MarkdownPreviewBackgroundStyle") var markdownPreviewBackgroundStyleRaw: String = "automatic" @AppStorage("MarkdownPreviewPDFExportMode") var markdownPDFExportModeRaw: String = "paginated-fit" @State private var showLanguageSetupPrompt: Bool = false @State private var languagePromptSelection: String = "plain" @State private var languagePromptInsertTemplate: Bool = false @State private var showLanguageSearchSheet: Bool = false @State private var languageSearchQuery: String = "" @State private var whitespaceInspectorMessage: String? = nil @State private var didApplyStartupBehavior: Bool = false @State private var didRunInitialWindowLayoutSetup: Bool = false @State private var pendingLargeFileModeReevaluation: DispatchWorkItem? = nil @State private var recoverySnapshotIdentifier: String = UUID().uuidString @State private var lastCaretLocation: Int = 0 @State private var sessionCaretByFileURL: [String: Int] = [:] #if os(macOS) @State private var isProjectSidebarResizeHandleHovered: Bool = false #endif private let quickSwitcherRecentsDefaultsKey = "QuickSwitcherRecentItemsV1" #if USE_FOUNDATION_MODELS && canImport(FoundationModels) var appleModelAvailable: Bool { true } #else var appleModelAvailable: Bool { false } #endif var activeProviderName: String { let trimmed = lastProviderUsed.trimmingCharacters(in: .whitespacesAndNewlines) if trimmed.isEmpty || trimmed == "Apple" { return selectedModel.displayName } return trimmed } private var projectNavigatorPlacement: ProjectNavigatorPlacement { ProjectNavigatorPlacement(rawValue: projectNavigatorPlacementRaw) ?? .trailing } private var performancePreset: PerformancePreset { PerformancePreset(rawValue: performancePresetRaw) ?? .balanced } private var clampedProjectSidebarWidth: CGFloat { let clamped = min(max(projectSidebarWidth, 220), 520) return CGFloat(clamped) } private var isDelimitedFileLanguage: Bool { let lower = currentLanguage.lowercased() return lower == "csv" || lower == "tsv" } private var delimitedSeparator: Character { currentLanguage.lowercased() == "tsv" ? "\t" : "," } private var shouldShowDelimitedTable: Bool { isDelimitedFileLanguage && delimitedViewMode == .table } #if os(macOS) private enum MacTranslucencyMode: String { case subtle case balanced case vibrant var material: Material { switch self { case .subtle, .balanced: return .thickMaterial case .vibrant: return .regularMaterial } } var opacity: Double { switch self { case .subtle: return 0.84 case .balanced: return 0.76 case .vibrant: return 0.68 } } var toolbarOpacity: Double { switch self { case .subtle: return 0.72 case .balanced: return 0.64 case .vibrant: return 0.56 } } } private var macTranslucencyMode: MacTranslucencyMode { MacTranslucencyMode(rawValue: macTranslucencyModeRaw) ?? .balanced } private let bracketHelperTokens: [String] = ["(", ")", "{", "}", "[", "]", "<", ">", "'", "\"", "`", "()", "{}", "[]", "\"\"", "''"] private var macUnifiedTranslucentMaterialStyle: AnyShapeStyle { AnyShapeStyle(macTranslucencyMode.material.opacity(macTranslucencyMode.opacity)) } private var macSolidSurfaceColor: Color { currentEditorTheme(colorScheme: colorScheme).background } private var macChromeBackgroundStyle: AnyShapeStyle { if enableTranslucentWindow { return macUnifiedTranslucentMaterialStyle } return AnyShapeStyle(macSolidSurfaceColor) } private var macToolbarBackgroundStyle: AnyShapeStyle { if enableTranslucentWindow { return AnyShapeStyle(macTranslucencyMode.material.opacity(macTranslucencyMode.toolbarOpacity)) } return AnyShapeStyle(macSolidSurfaceColor) } #elseif os(iOS) var primaryGlassMaterial: Material { colorScheme == .dark ? .regularMaterial : .ultraThinMaterial } var toolbarFallbackColor: Color { colorScheme == .dark ? Color.black.opacity(0.34) : Color.white.opacity(0.86) } private var iOSNonTranslucentSurfaceColor: Color { currentEditorTheme(colorScheme: colorScheme).background } private var useIOSUnifiedSolidSurfaces: Bool { !enableTranslucentWindow } var toolbarDensityScale: CGFloat { 1.0 } var toolbarDensityOpacity: Double { 1.0 } private var canShowMarkdownPreviewOnCurrentDevice: Bool { horizontalSizeClass == .regular } #endif private var editorSurfaceBackgroundStyle: AnyShapeStyle { #if os(macOS) if enableTranslucentWindow { return macUnifiedTranslucentMaterialStyle } return AnyShapeStyle(macSolidSurfaceColor) #else if useIOSUnifiedSolidSurfaces { return AnyShapeStyle(iOSNonTranslucentSurfaceColor) } return enableTranslucentWindow ? AnyShapeStyle(.ultraThinMaterial) : AnyShapeStyle(Color.clear) #endif } var canShowMarkdownPreviewPane: Bool { #if os(iOS) true #else true #endif } private var canShowMarkdownPreviewSplitPane: Bool { #if os(iOS) canShowMarkdownPreviewOnCurrentDevice #else true #endif } #if os(iOS) private var shouldPresentMarkdownPreviewSheetOnIPhone: Bool { UIDevice.current.userInterfaceIdiom == .phone && showMarkdownPreviewPane && currentLanguage == "markdown" && !brainDumpLayoutEnabled } private var markdownPreviewSheetPresentationBinding: Binding { Binding( get: { shouldPresentMarkdownPreviewSheetOnIPhone }, set: { isPresented in if !isPresented { showMarkdownPreviewPane = false } } ) } #endif private var settingsSheetDetents: Set { #if os(iOS) if UIDevice.current.userInterfaceIdiom == .pad { return [.fraction(0.985)] } return [.large] #else return [.large] #endif } #if os(macOS) private var macTabBarStripHeight: CGFloat { 36 } #endif private var useIPhoneUnifiedTopHost: Bool { #if os(iOS) UIDevice.current.userInterfaceIdiom == .phone #else false #endif } private var tabBarLeadingPadding: CGFloat { #if os(iOS) if UIDevice.current.userInterfaceIdiom == .pad { // Keep tabs clear of iPad window controls in narrow/multitasking layouts. return horizontalSizeClass == .compact ? 112 : 96 } #endif return 10 } var selectedModel: AIModel { get { AIModel(rawValue: selectedModelRaw) ?? .appleIntelligence } set { selectedModelRaw = newValue.rawValue } } private func promptForGrokTokenIfNeeded() -> Bool { if !grokAPIToken.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { return true } #if os(macOS) let alert = NSAlert() alert.messageText = "Grok API Token Required" alert.informativeText = "Enter your Grok API token to enable suggestions. You can obtain this from your Grok account." alert.alertStyle = .informational alert.addButton(withTitle: "Save") alert.addButton(withTitle: "Cancel") let input = NSSecureTextField(frame: NSRect(x: 0, y: 0, width: 280, height: 24)) input.placeholderString = "sk-..." alert.accessoryView = input let response = alert.runModal() if response == .alertFirstButtonReturn { let token = input.stringValue.trimmingCharacters(in: .whitespacesAndNewlines) if token.isEmpty { return false } grokAPIToken = token SecureTokenStore.setToken(token, for: .grok) return true } #endif return false } private func promptForOpenAITokenIfNeeded() -> Bool { if !openAIAPIToken.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { return true } #if os(macOS) let alert = NSAlert() alert.messageText = "OpenAI API Token Required" alert.informativeText = "Enter your OpenAI API token to enable suggestions." alert.alertStyle = .informational alert.addButton(withTitle: "Save") alert.addButton(withTitle: "Cancel") let input = NSSecureTextField(frame: NSRect(x: 0, y: 0, width: 280, height: 24)) input.placeholderString = "sk-..." alert.accessoryView = input let response = alert.runModal() if response == .alertFirstButtonReturn { let token = input.stringValue.trimmingCharacters(in: .whitespacesAndNewlines) if token.isEmpty { return false } openAIAPIToken = token SecureTokenStore.setToken(token, for: .openAI) return true } #endif return false } private func promptForGeminiTokenIfNeeded() -> Bool { if !geminiAPIToken.isEmpty { return true } #if os(macOS) let alert = NSAlert() alert.messageText = "Gemini API Key Required" alert.informativeText = "Enter your Gemini API key to enable suggestions." alert.alertStyle = .informational alert.addButton(withTitle: "Save") alert.addButton(withTitle: "Cancel") let input = NSSecureTextField(frame: NSRect(x: 0, y: 0, width: 280, height: 24)) input.placeholderString = "AIza..." alert.accessoryView = input let response = alert.runModal() if response == .alertFirstButtonReturn { let token = input.stringValue.trimmingCharacters(in: .whitespacesAndNewlines) if token.isEmpty { return false } geminiAPIToken = token SecureTokenStore.setToken(token, for: .gemini) return true } #endif return false } private func promptForAnthropicTokenIfNeeded() -> Bool { if !anthropicAPIToken.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { return true } #if os(macOS) let alert = NSAlert() alert.messageText = "Anthropic API Token Required" alert.informativeText = "Enter your Anthropic API token to enable suggestions." alert.alertStyle = .informational alert.addButton(withTitle: "Save") alert.addButton(withTitle: "Cancel") let input = NSSecureTextField(frame: NSRect(x: 0, y: 0, width: 280, height: 24)) input.placeholderString = "sk-ant-..." alert.accessoryView = input let response = alert.runModal() if response == .alertFirstButtonReturn { let token = input.stringValue.trimmingCharacters(in: .whitespacesAndNewlines) if token.isEmpty { return false } anthropicAPIToken = token SecureTokenStore.setToken(token, for: .anthropic) return true } #endif return false } #if os(macOS) @MainActor private func performInlineCompletion(for textView: NSTextView) { completionTask?.cancel() completionTask = Task(priority: .utility) { await performInlineCompletionAsync(for: textView) } } @MainActor private func performInlineCompletionAsync(for textView: NSTextView) async { let completionInterval = Self.completionSignposter.beginInterval("inline_completion") defer { Self.completionSignposter.endInterval("inline_completion", completionInterval) } let sel = textView.selectedRange() guard sel.length == 0 else { return } let loc = sel.location guard loc > 0, loc <= (textView.string as NSString).length else { return } let nsText = textView.string as NSString if Task.isCancelled { return } if shouldThrottleHeavyEditorFeatures(in: nsText) { return } let prevChar = nsText.substring(with: NSRange(location: loc - 1, length: 1)) var nextChar: String? = nil if loc < nsText.length { nextChar = nsText.substring(with: NSRange(location: loc, length: 1)) } // Auto-close braces/brackets/parens if not already closed let pairs: [String: String] = ["{": "}", "(": ")", "[": "]"] if let closing = pairs[prevChar] { if nextChar != closing { // Insert closing and move caret back between pair let insertion = closing textView.insertText(insertion, replacementRange: sel) textView.setSelectedRange(NSRange(location: loc, length: 0)) return } } // If previous char is '{' and language is swift, javascript, c, or cpp, insert code block scaffold if prevChar == "{" && ["swift", "javascript", "c", "cpp"].contains(currentLanguage) { // Get current line indentation let fullText = textView.string as NSString let lineRange = fullText.lineRange(for: NSRange(location: loc - 1, length: 0)) let lineText = fullText.substring(with: lineRange) let indentPrefix = lineText.prefix(while: { $0 == " " || $0 == "\t" }) let indentString = String(indentPrefix) let indentLevel = indentString.count let indentSpaces = " " // 4 spaces // Build scaffold string let scaffold = "\n\(indentString)\(indentSpaces)\n\(indentString)}" // Insert scaffold at caret position textView.insertText(scaffold, replacementRange: NSRange(location: loc, length: 0)) // Move caret to indented empty line let newCaretLocation = loc + 1 + indentLevel + indentSpaces.count textView.setSelectedRange(NSRange(location: newCaretLocation, length: 0)) return } // Prefer cheap local matches before model-backed completion. let doc = textView.string let nsDoc = doc as NSString if let localSuggestion = CompletionHeuristics.localSuggestion( in: nsDoc, caretLocation: loc, language: currentLanguage, includeDocumentWords: completionFromDocument, includeSyntaxKeywords: completionFromSyntax ) { applyInlineSuggestion(localSuggestion, textView: textView, selection: sel) return } // Limit completion context by both recent lines and UTF-16 length for lower latency. let tokenContext = CompletionHeuristics.tokenContext(in: nsDoc, caretLocation: loc) let contextPrefix = completionContextPrefix(in: nsDoc, caretLocation: loc) let cacheKey = completionCacheKey(prefix: contextPrefix, language: currentLanguage, caretLocation: loc) if let cached = cachedCompletion(for: cacheKey) { Self.completionSignposter.emitEvent("completion_cache_hit") applyInlineSuggestion(cached, textView: textView, selection: sel) return } let modelInterval = Self.completionSignposter.beginInterval("model_completion") let suggestion = await generateModelCompletion(prefix: contextPrefix, language: currentLanguage) Self.completionSignposter.endInterval("model_completion", modelInterval) if Task.isCancelled { return } let sanitizedSuggestion = CompletionHeuristics.sanitizeModelSuggestion( suggestion, currentTokenPrefix: tokenContext.prefix, nextDocumentText: tokenContext.nextDocumentText ) storeCompletionInCache(sanitizedSuggestion, for: cacheKey) applyInlineSuggestion(sanitizedSuggestion, textView: textView, selection: sel) } private func completionContextPrefix(in nsDoc: NSString, caretLocation: Int, maxUTF16: Int = 3000, maxLines: Int = 120) -> String { let startByChars = max(0, caretLocation - maxUTF16) var cursor = caretLocation var seenLines = 0 while cursor > 0 && seenLines < maxLines { let searchRange = NSRange(location: 0, length: cursor) let found = nsDoc.range(of: "\n", options: .backwards, range: searchRange) if found.location == NSNotFound { cursor = 0 break } cursor = found.location seenLines += 1 } let startByLines = cursor let start = max(startByChars, startByLines) return nsDoc.substring(with: NSRange(location: start, length: caretLocation - start)) } private func completionCacheKey(prefix: String, language: String, caretLocation: Int) -> String { let normalizedPrefix = String(prefix.suffix(320)) var hasher = Hasher() hasher.combine(language) hasher.combine(caretLocation / 32) hasher.combine(normalizedPrefix) return "\(language):\(caretLocation / 32):\(hasher.finalize())" } private func cachedCompletion(for key: String) -> String? { pruneCompletionCacheIfNeeded() guard let entry = completionCache[key] else { return nil } if Date().timeIntervalSince(entry.createdAt) > 20 { completionCache.removeValue(forKey: key) return nil } return entry.suggestion } private func storeCompletionInCache(_ suggestion: String, for key: String) { completionCache[key] = CompletionCacheEntry(suggestion: suggestion, createdAt: Date()) pruneCompletionCacheIfNeeded() } private func pruneCompletionCacheIfNeeded() { if completionCache.count <= 220 { return } let cutoff = Date().addingTimeInterval(-20) completionCache = completionCache.filter { $0.value.createdAt >= cutoff } if completionCache.count <= 200 { return } let sorted = completionCache.sorted { $0.value.createdAt > $1.value.createdAt } completionCache = Dictionary(uniqueKeysWithValues: sorted.prefix(200).map { ($0.key, $0.value) }) } private func applyInlineSuggestion(_ suggestion: String, textView: NSTextView, selection: NSRange) { guard let accepting = textView as? AcceptingTextView else { return } let currentText = textView.string as NSString let currentSelection = textView.selectedRange() guard currentSelection.length == 0, currentSelection.location == selection.location else { return } let nextRangeLength = min(suggestion.count, currentText.length - selection.location) let nextText = nextRangeLength > 0 ? currentText.substring(with: NSRange(location: selection.location, length: nextRangeLength)) : "" if suggestion.isEmpty || nextText.starts(with: suggestion) { accepting.clearInlineSuggestion() return } accepting.showInlineSuggestion(suggestion, at: selection.location) } private func shouldThrottleHeavyEditorFeatures(in nsText: NSString? = nil) -> Bool { if effectiveLargeFileModeEnabled { return true } let length = nsText?.length ?? currentDocumentUTF16Length return length >= EditorPerformanceThresholds.heavyFeatureUTF16Length } private func shouldScheduleCompletion(for textView: NSTextView) -> Bool { let nsText = textView.string as NSString let selection = textView.selectedRange() guard selection.length == 0 else { return false } let location = selection.location guard location > 0, location <= nsText.length else { return false } if shouldThrottleHeavyEditorFeatures(in: nsText) { return false } let prevChar = nsText.substring(with: NSRange(location: location - 1, length: 1)) let triggerChars: Set = [".", "(", ")", "{", "}", "[", "]", ":", ",", "\n", "\t"] if triggerChars.contains(prevChar) { return true } if prevChar == " " { return CompletionHeuristics.shouldTriggerAfterWhitespace( in: nsText, caretLocation: location, language: currentLanguage ) } let wordChars = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "_")) if prevChar.rangeOfCharacter(from: wordChars) == nil { return false } if location >= nsText.length { return true } let nextChar = nsText.substring(with: NSRange(location: location, length: 1)) let separator = CharacterSet.whitespacesAndNewlines.union(.punctuationCharacters) return nextChar.rangeOfCharacter(from: separator) != nil } private func completionDebounceInterval(for textView: NSTextView) -> TimeInterval { let docLength = (textView.string as NSString).length if docLength >= 80_000 { return 0.9 } if docLength >= 25_000 { return 0.7 } return 0.45 } private func completionTriggerSignature(for textView: NSTextView) -> String { let nsText = textView.string as NSString let selection = textView.selectedRange() guard selection.length == 0 else { return "" } let location = selection.location guard location > 0, location <= nsText.length else { return "" } let prevChar = nsText.substring(with: NSRange(location: location - 1, length: 1)) let nextChar: String if location < nsText.length { nextChar = nsText.substring(with: NSRange(location: location, length: 1)) } else { nextChar = "" } // Keep signature cheap while specific enough to skip duplicate notifications. return "\(location)|\(prevChar)|\(nextChar)|\(nsText.length)" } #endif private func externalModelCompletion(prefix: String, language: String) async -> String { // Try Grok if !grokAPIToken.isEmpty { do { guard let url = URL(string: "https://api.x.ai/v1/chat/completions") else { return "" } var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("Bearer \(grokAPIToken)", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Content-Type") let prompt = """ Continue the following \(language) code snippet with a few lines or tokens of code only. Do not add prose or explanations. \(prefix) Completion: """ let body: [String: Any] = [ "model": "grok-2-latest", "messages": [["role": "user", "content": prompt]], "temperature": 0.5, "max_tokens": 64, "n": 1, "stop": [""] ] request.httpBody = try JSONSerialization.data(withJSONObject: body, options: []) let (data, _) = try await URLSession.shared.data(for: request) if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any], let choices = json["choices"] as? [[String: Any]], let message = choices.first?["message"] as? [String: Any], let content = message["content"] as? String { return sanitizeCompletion(content) } } catch { debugLog("[Completion][Fallback][Grok] request failed") } } // Try OpenAI if !openAIAPIToken.isEmpty { do { guard let url = URL(string: "https://api.openai.com/v1/chat/completions") else { return "" } var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("Bearer \(openAIAPIToken)", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Content-Type") let prompt = """ Continue the following \(language) code snippet with a few lines or tokens of code only. Do not add prose or explanations. \(prefix) Completion: """ let body: [String: Any] = [ "model": "gpt-4o-mini", "messages": [["role": "user", "content": prompt]], "temperature": 0.5, "max_tokens": 64, "n": 1, "stop": [""] ] request.httpBody = try JSONSerialization.data(withJSONObject: body, options: []) let (data, _) = try await URLSession.shared.data(for: request) if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any], let choices = json["choices"] as? [[String: Any]], let message = choices.first?["message"] as? [String: Any], let content = message["content"] as? String { return sanitizeCompletion(content) } } catch { debugLog("[Completion][Fallback][OpenAI] request failed") } } // Try Gemini if !geminiAPIToken.isEmpty { do { let model = "gemini-1.5-flash-latest" let endpoint = "https://generativelanguage.googleapis.com/v1beta/models/\(model):generateContent" guard let url = URL(string: endpoint) else { return "" } var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue(geminiAPIToken, forHTTPHeaderField: "x-goog-api-key") request.setValue("application/json", forHTTPHeaderField: "Content-Type") let prompt = """ Continue the following \(language) code snippet with a few lines or tokens of code only. Do not add prose or explanations. \(prefix) Completion: """ let body: [String: Any] = [ "contents": [["parts": [["text": prompt]]]], "generationConfig": ["temperature": 0.5, "maxOutputTokens": 64] ] request.httpBody = try JSONSerialization.data(withJSONObject: body, options: []) let (data, _) = try await URLSession.shared.data(for: request) if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any], let candidates = json["candidates"] as? [[String: Any]], let first = candidates.first, let content = first["content"] as? [String: Any], let parts = content["parts"] as? [[String: Any]], let text = parts.first?["text"] as? String { return sanitizeCompletion(text) } } catch { debugLog("[Completion][Fallback][Gemini] request failed") } } // Try Anthropic if !anthropicAPIToken.isEmpty { do { guard let url = URL(string: "https://api.anthropic.com/v1/messages") else { return "" } var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue(anthropicAPIToken, forHTTPHeaderField: "x-api-key") request.setValue("2023-06-01", forHTTPHeaderField: "anthropic-version") request.setValue("application/json", forHTTPHeaderField: "Content-Type") let prompt = """ Continue the following \(language) code snippet with a few lines or tokens of code only. Do not add prose or explanations. \(prefix) Completion: """ let body: [String: Any] = [ "model": "claude-3-5-haiku-latest", "max_tokens": 64, "temperature": 0.5, "messages": [["role": "user", "content": prompt]] ] request.httpBody = try JSONSerialization.data(withJSONObject: body, options: []) let (data, _) = try await URLSession.shared.data(for: request) if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any], let contentArr = json["content"] as? [[String: Any]], let first = contentArr.first, let text = first["text"] as? String { return sanitizeCompletion(text) } if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any], let message = json["message"] as? [String: Any], let contentArr = message["content"] as? [[String: Any]], let first = contentArr.first, let text = first["text"] as? String { return sanitizeCompletion(text) } } catch { debugLog("[Completion][Fallback][Anthropic] request failed") } } return "" } private func appleModelCompletion(prefix: String, language: String) async -> String { let client = AppleIntelligenceAIClient() var aggregated = "" var firstChunk: String? for await chunk in client.streamSuggestions(prompt: "Continue the following \(language) code snippet with a few lines or tokens of code only. Do not add prose or explanations.\n\n\(prefix)\n\nCompletion:") { if firstChunk == nil, !chunk.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { firstChunk = chunk break } else { aggregated += chunk } } let candidate = sanitizeCompletion((firstChunk ?? aggregated)) await MainActor.run { lastProviderUsed = "Apple" } return candidate } private func generateModelCompletion(prefix: String, language: String) async -> String { switch selectedModel { case .appleIntelligence: return await appleModelCompletion(prefix: prefix, language: language) case .grok: if grokAPIToken.isEmpty { let res = await appleModelCompletion(prefix: prefix, language: language) await MainActor.run { lastProviderUsed = "Grok (fallback to Apple)" } return res } do { guard let url = URL(string: "https://api.x.ai/v1/chat/completions") else { let res = await appleModelCompletion(prefix: prefix, language: language) await MainActor.run { lastProviderUsed = "Grok (fallback to Apple)" } return res } var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("Bearer \(grokAPIToken)", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Content-Type") let prompt = """ Continue the following \(language) code snippet with a few lines or tokens of code only. Do not add prose or explanations. \(prefix) Completion: """ let body: [String: Any] = [ "model": "grok-2-latest", "messages": [["role": "user", "content": prompt]], "temperature": 0.5, "max_tokens": 64, "n": 1, "stop": [""] ] request.httpBody = try JSONSerialization.data(withJSONObject: body, options: []) let (data, _) = try await URLSession.shared.data(for: request) if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any], let choices = json["choices"] as? [[String: Any]], let message = choices.first?["message"] as? [String: Any], let content = message["content"] as? String { await MainActor.run { lastProviderUsed = "Grok" } return sanitizeCompletion(content) } // If no content, fallback to Apple let res = await appleModelCompletion(prefix: prefix, language: language) await MainActor.run { lastProviderUsed = "Grok (fallback to Apple)" } return res } catch { debugLog("[Completion][Grok] request failed") let res = await appleModelCompletion(prefix: prefix, language: language) await MainActor.run { lastProviderUsed = "Grok (fallback to Apple)" } return res } case .openAI: if openAIAPIToken.isEmpty { let res = await appleModelCompletion(prefix: prefix, language: language) await MainActor.run { lastProviderUsed = "OpenAI (fallback to Apple)" } return res } do { guard let url = URL(string: "https://api.openai.com/v1/chat/completions") else { let res = await appleModelCompletion(prefix: prefix, language: language) await MainActor.run { lastProviderUsed = "OpenAI (fallback to Apple)" } return res } var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("Bearer \(openAIAPIToken)", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Content-Type") let prompt = """ Continue the following \(language) code snippet with a few lines or tokens of code only. Do not add prose or explanations. \(prefix) Completion: """ let body: [String: Any] = [ "model": "gpt-4o-mini", "messages": [["role": "user", "content": prompt]], "temperature": 0.5, "max_tokens": 64, "n": 1, "stop": [""] ] request.httpBody = try JSONSerialization.data(withJSONObject: body, options: []) let (data, _) = try await URLSession.shared.data(for: request) if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any], let choices = json["choices"] as? [[String: Any]], let message = choices.first?["message"] as? [String: Any], let content = message["content"] as? String { await MainActor.run { lastProviderUsed = "OpenAI" } return sanitizeCompletion(content) } let res = await appleModelCompletion(prefix: prefix, language: language) await MainActor.run { lastProviderUsed = "OpenAI (fallback to Apple)" } return res } catch { debugLog("[Completion][OpenAI] request failed") let res = await appleModelCompletion(prefix: prefix, language: language) await MainActor.run { lastProviderUsed = "OpenAI (fallback to Apple)" } return res } case .gemini: if geminiAPIToken.isEmpty { let res = await appleModelCompletion(prefix: prefix, language: language) await MainActor.run { lastProviderUsed = "Gemini (fallback to Apple)" } return res } do { let model = "gemini-1.5-flash-latest" let endpoint = "https://generativelanguage.googleapis.com/v1beta/models/\(model):generateContent" guard let url = URL(string: endpoint) else { let res = await appleModelCompletion(prefix: prefix, language: language) await MainActor.run { lastProviderUsed = "Gemini (fallback to Apple)" } return res } var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue(geminiAPIToken, forHTTPHeaderField: "x-goog-api-key") request.setValue("application/json", forHTTPHeaderField: "Content-Type") let prompt = """ Continue the following \(language) code snippet with a few lines or tokens of code only. Do not add prose or explanations. \(prefix) Completion: """ let body: [String: Any] = [ "contents": [["parts": [["text": prompt]]]], "generationConfig": ["temperature": 0.5, "maxOutputTokens": 64] ] request.httpBody = try JSONSerialization.data(withJSONObject: body, options: []) let (data, _) = try await URLSession.shared.data(for: request) if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any], let candidates = json["candidates"] as? [[String: Any]], let first = candidates.first, let content = first["content"] as? [String: Any], let parts = content["parts"] as? [[String: Any]], let text = parts.first?["text"] as? String { await MainActor.run { lastProviderUsed = "Gemini" } return sanitizeCompletion(text) } let res = await appleModelCompletion(prefix: prefix, language: language) await MainActor.run { lastProviderUsed = "Gemini (fallback to Apple)" } return res } catch { debugLog("[Completion][Gemini] request failed") let res = await appleModelCompletion(prefix: prefix, language: language) await MainActor.run { lastProviderUsed = "Gemini (fallback to Apple)" } return res } case .anthropic: if anthropicAPIToken.isEmpty { let res = await appleModelCompletion(prefix: prefix, language: language) await MainActor.run { lastProviderUsed = "Anthropic (fallback to Apple)" } return res } do { guard let url = URL(string: "https://api.anthropic.com/v1/messages") else { let res = await appleModelCompletion(prefix: prefix, language: language) await MainActor.run { lastProviderUsed = "Anthropic (fallback to Apple)" } return res } var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue(anthropicAPIToken, forHTTPHeaderField: "x-api-key") request.setValue("2023-06-01", forHTTPHeaderField: "anthropic-version") request.setValue("application/json", forHTTPHeaderField: "Content-Type") let prompt = """ Continue the following \(language) code snippet with a few lines or tokens of code only. Do not add prose or explanations. \(prefix) Completion: """ let body: [String: Any] = [ "model": "claude-3-5-haiku-latest", "max_tokens": 64, "temperature": 0.5, "messages": [["role": "user", "content": prompt]] ] request.httpBody = try JSONSerialization.data(withJSONObject: body, options: []) let (data, _) = try await URLSession.shared.data(for: request) if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any], let contentArr = json["content"] as? [[String: Any]], let first = contentArr.first, let text = first["text"] as? String { await MainActor.run { lastProviderUsed = "Anthropic" } return sanitizeCompletion(text) } if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any], let message = json["message"] as? [String: Any], let contentArr = message["content"] as? [[String: Any]], let first = contentArr.first, let text = first["text"] as? String { await MainActor.run { lastProviderUsed = "Anthropic" } return sanitizeCompletion(text) } let res = await appleModelCompletion(prefix: prefix, language: language) await MainActor.run { lastProviderUsed = "Anthropic (fallback to Apple)" } return res } catch { debugLog("[Completion][Anthropic] request failed") let res = await appleModelCompletion(prefix: prefix, language: language) await MainActor.run { lastProviderUsed = "Anthropic (fallback to Apple)" } return res } } } private func sanitizeCompletion(_ raw: String) -> String { CompletionHeuristics.sanitizeModelSuggestion(raw, currentTokenPrefix: "", nextDocumentText: "") } private func debugLog(_ message: String) { if message.contains("[Completion]") || message.contains("AI ") || message.contains("[AI]") { AIActivityLog.record(message, source: "Completion") } #if DEBUG print(message) #endif } #if os(macOS) private func matchesCurrentWindow(_ notif: Notification) -> Bool { guard let target = notif.userInfo?[EditorCommandUserInfo.windowNumber] as? Int else { return true } guard let hostWindowNumber else { return false } return target == hostWindowNumber } private func updateWindowRegistration(_ window: NSWindow?) { let number = window?.windowNumber if hostWindowNumber != number, let old = hostWindowNumber { WindowViewModelRegistry.shared.unregister(windowNumber: old) } hostWindowNumber = number installWindowCloseConfirmationDelegate(window) updateWindowChrome(window) if let number { WindowViewModelRegistry.shared.register(viewModel, for: number) } } private func updateWindowChrome(_ window: NSWindow? = nil) { guard let targetWindow = window ?? hostWindowNumber.flatMap({ NSApp.window(withWindowNumber: $0) }) else { return } targetWindow.subtitle = windowSubtitleText } private func saveAllDirtyTabsForWindowClose() -> Bool { let dirtyTabIDs = viewModel.tabs.filter(\.isDirty).map(\.id) guard !dirtyTabIDs.isEmpty else { return true } for tabID in dirtyTabIDs { guard viewModel.tabs.contains(where: { $0.id == tabID }) else { continue } viewModel.saveFile(tabID: tabID) guard let updated = viewModel.tabs.first(where: { $0.id == tabID }), !updated.isDirty else { return false } } return true } private func windowCloseDialogMessage() -> String { let dirtyCount = viewModel.tabs.filter(\.isDirty).count if dirtyCount <= 1 { return "You have unsaved changes in one tab." } return "You have unsaved changes in \(dirtyCount) tabs." } private func installWindowCloseConfirmationDelegate(_ window: NSWindow?) { guard let window else { windowCloseConfirmationDelegate = nil return } let delegate: WindowCloseConfirmationDelegate if let existing = windowCloseConfirmationDelegate { delegate = existing } else { delegate = WindowCloseConfirmationDelegate() windowCloseConfirmationDelegate = delegate } if window.delegate !== delegate { if let current = window.delegate, current !== delegate { delegate.forwardedDelegate = current } window.delegate = delegate } delegate.shouldConfirm = { confirmCloseDirtyTab } delegate.hasDirtyTabs = { viewModel.tabs.contains(where: \.isDirty) } delegate.saveAllDirtyTabs = { saveAllDirtyTabsForWindowClose() } delegate.dialogTitle = { "Save changes before closing?" } delegate.dialogMessage = { windowCloseDialogMessage() } } private func requestBracketHelperInsert(_ token: String) { let targetWindow = hostWindowNumber ?? NSApp.keyWindow?.windowNumber ?? NSApp.mainWindow?.windowNumber var userInfo: [String: Any] = [EditorCommandUserInfo.bracketToken: token] if let targetWindow { userInfo[EditorCommandUserInfo.windowNumber] = targetWindow } NotificationCenter.default.post( name: .insertBracketHelperTokenRequested, object: nil, userInfo: userInfo ) } #else private func matchesCurrentWindow(_ notif: Notification) -> Bool { true } #endif #if os(macOS) private var bracketHelperBar: some View { ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 8) { ForEach(bracketHelperTokens, id: \.self) { token in Button(action: { requestBracketHelperInsert(token) }) { Text(token) .font(.system(size: 13, weight: .semibold, design: .monospaced)) .padding(.horizontal, 10) .padding(.vertical, 5) .contentShape(Rectangle()) .background( RoundedRectangle(cornerRadius: 8, style: .continuous) .fill(Color.accentColor.opacity(0.14)) ) } .buttonStyle(.plain) .accessibilityLabel(Text("Insert \(token)")) } } .padding(.horizontal, 10) .padding(.vertical, 6) } .background(editorSurfaceBackgroundStyle) } #endif private func withBaseEditorEvents(_ view: Content) -> some View { let viewWithClipboardEvents = view .onReceive(NotificationCenter.default.publisher(for: .caretPositionDidChange)) { notif in if let location = notif.userInfo?["location"] as? Int, location >= 0 { lastCaretLocation = location if let selectedURL = viewModel.selectedTab?.fileURL?.standardizedFileURL { sessionCaretByFileURL[selectedURL.absoluteString] = location } } if let line = notif.userInfo?["line"] as? Int, let col = notif.userInfo?["column"] as? Int { if line <= 0 { caretStatus = "Pos \(col)" } else { caretStatus = "Ln \(line), Col \(col)" } } #if os(iOS) // Keep floating status pill word count in sync with live buffer while typing. let liveText = liveEditorBufferText() ?? currentContent scheduleWordCountRefresh(for: liveText) #endif } .onReceive(NotificationCenter.default.publisher(for: .editorSelectionDidChange)) { notif in let selection = (notif.object as? String) ?? "" currentSelectionSnapshotText = selection } .onReceive(NotificationCenter.default.publisher(for: .editorRequestCodeSnapshotFromSelection)) { _ in presentCodeSnapshotComposer() } .onReceive(NotificationCenter.default.publisher(for: .pastedText)) { notif in handlePastedTextNotification(notif) } .onReceive(NotificationCenter.default.publisher(for: .pastedFileURL)) { notif in handlePastedFileNotification(notif) } .onReceive(NotificationCenter.default.publisher(for: .zoomEditorFontRequested)) { notif in let delta: Double = { if let d = notif.object as? Double { return d } if let n = notif.object as? NSNumber { return n.doubleValue } return 1 }() adjustEditorFontSize(delta) } .onReceive(NotificationCenter.default.publisher(for: .droppedFileURL)) { notif in handleDroppedFileNotification(notif) } return viewWithClipboardEvents .onReceive(NotificationCenter.default.publisher(for: .droppedFileLoadStarted)) { notif in droppedFileLoadInProgress = true droppedFileProgressDeterminate = (notif.userInfo?["isDeterminate"] as? Bool) ?? true droppedFileLoadProgress = 0 droppedFileLoadLabel = "Reading file" largeFileModeEnabled = (notif.userInfo?["largeFileMode"] as? Bool) ?? false } .onReceive(NotificationCenter.default.publisher(for: .droppedFileLoadProgress)) { notif in // Recover even if "started" was missed. droppedFileLoadInProgress = true if let determinate = notif.userInfo?["isDeterminate"] as? Bool { droppedFileProgressDeterminate = determinate } let fraction: Double = { if let v = notif.userInfo?["fraction"] as? Double { return v } if let v = notif.userInfo?["fraction"] as? NSNumber { return v.doubleValue } if let v = notif.userInfo?["fraction"] as? Float { return Double(v) } if let v = notif.userInfo?["fraction"] as? CGFloat { return Double(v) } return droppedFileLoadProgress }() droppedFileLoadProgress = min(max(fraction, 0), 1) if (notif.userInfo?["largeFileMode"] as? Bool) == true { largeFileModeEnabled = true } if let name = notif.userInfo?["fileName"] as? String, !name.isEmpty { droppedFileLoadLabel = name } } .onReceive(NotificationCenter.default.publisher(for: .droppedFileLoadFinished)) { notif in let success = (notif.userInfo?["success"] as? Bool) ?? true droppedFileLoadProgress = success ? 1 : 0 droppedFileProgressDeterminate = true if (notif.userInfo?["largeFileMode"] as? Bool) == true { largeFileModeEnabled = true } if !success, let message = notif.userInfo?["message"] as? String, !message.isEmpty { findStatusMessage = "Drop failed: \(message)" droppedFileLoadLabel = "Import failed" } DispatchQueue.main.asyncAfter(deadline: .now() + (success ? 0.35 : 2.5)) { droppedFileLoadInProgress = false } } .onChange(of: viewModel.selectedTab?.id) { _, _ in editorExternalMutationRevision &+= 1 updateLargeFileModeForCurrentContext() scheduleLargeFileModeReevaluation(after: 0.9) scheduleHighlightRefresh() if let selectedID = viewModel.selectedTab?.id { viewModel.refreshExternalConflictForTab(tabID: selectedID) } restoreCaretForSelectedSessionFileIfAvailable() persistSessionIfReady() } .onChange(of: viewModel.selectedTab?.isLoadingContent ?? false) { _, isLoading in if isLoading { let shouldPreEnableLargeMode = droppedFileLoadInProgress || viewModel.selectedTab?.isLargeFileCandidate == true || currentDocumentUTF16Length >= 300_000 if shouldPreEnableLargeMode, !largeFileModeEnabled { largeFileModeEnabled = true } } else { scheduleLargeFileModeReevaluation(after: 0.8) } scheduleHighlightRefresh() } .onChange(of: currentLanguage) { _, newValue in settingsTemplateLanguage = newValue } .onChange(of: viewModel.pendingExternalFileConflict?.tabID) { _, conflictTabID in if conflictTabID != nil { showExternalConflictDialog = true } } } private func handlePastedTextNotification(_ notif: Notification) { guard let pasted = notif.object as? String else { DispatchQueue.main.async { updateLargeFileModeForCurrentContext() scheduleHighlightRefresh() } return } let result = LanguageDetector.shared.detect(text: pasted, name: nil, fileURL: nil) if let tab = viewModel.selectedTab, !tab.languageLocked, tab.language == "plain", result.lang != "plain" { viewModel.setTabLanguage(tabID: tab.id, language: result.lang, lock: false) } else if singleLanguage == "plain", result.lang != "plain" { singleLanguage = result.lang } DispatchQueue.main.async { updateLargeFileModeForCurrentContext() scheduleHighlightRefresh() } } private func handlePastedFileNotification(_ notif: Notification) { var urls: [URL] = [] if let url = notif.object as? URL { urls = [url] } else if let list = notif.object as? [URL] { urls = list } guard !urls.isEmpty else { return } for url in urls { viewModel.openFile(url: url) } DispatchQueue.main.async { updateLargeFileModeForCurrentContext() scheduleHighlightRefresh() } } private func handleDroppedFileNotification(_ notif: Notification) { guard let fileURL = notif.object as? URL else { return } if let preferred = LanguageDetector.shared.preferredLanguage(for: fileURL) { if let tab = viewModel.selectedTab, !tab.languageLocked, tab.language == "plain" { viewModel.setTabLanguage(tabID: tab.id, language: preferred, lock: false) } else if singleLanguage == "plain" { singleLanguage = preferred } } DispatchQueue.main.async { updateLargeFileModeForCurrentContext() scheduleHighlightRefresh() } } func updateLargeFileMode(for text: String) { if droppedFileLoadInProgress { if !largeFileModeEnabled { largeFileModeEnabled = true scheduleHighlightRefresh() } return } if viewModel.selectedTab?.isLoadingContent == true { if (viewModel.selectedTab?.isLargeFileCandidate == true || currentDocumentUTF16Length >= 300_000), !largeFileModeEnabled { largeFileModeEnabled = true scheduleHighlightRefresh() } return } if viewModel.selectedTab?.isLargeFileCandidate == true { if !largeFileModeEnabled { largeFileModeEnabled = true scheduleHighlightRefresh() } return } let lowerLanguage = currentLanguage.lowercased() let isHTMLLike = ["html", "htm", "xml", "svg", "xhtml"].contains(lowerLanguage) let isCSVLike = ["csv", "tsv"].contains(lowerLanguage) let useAggressiveThresholds = isHTMLLike || isCSVLike #if os(iOS) var byteThreshold = useAggressiveThresholds ? EditorPerformanceThresholds.largeFileBytesHTMLCSVMobile : EditorPerformanceThresholds.largeFileBytesMobile var lineThreshold = useAggressiveThresholds ? EditorPerformanceThresholds.largeFileLineBreaksHTMLCSVMobile : EditorPerformanceThresholds.largeFileLineBreaksMobile #else var byteThreshold = useAggressiveThresholds ? EditorPerformanceThresholds.largeFileBytesHTMLCSV : EditorPerformanceThresholds.largeFileBytes var lineThreshold = useAggressiveThresholds ? EditorPerformanceThresholds.largeFileLineBreaksHTMLCSV : EditorPerformanceThresholds.largeFileLineBreaks #endif switch performancePreset { case .balanced: break case .largeFiles: byteThreshold = max(1_000_000, Int(Double(byteThreshold) * 0.75)) lineThreshold = max(5_000, Int(Double(lineThreshold) * 0.75)) case .battery: byteThreshold = max(750_000, Int(Double(byteThreshold) * 0.55)) lineThreshold = max(3_000, Int(Double(lineThreshold) * 0.55)) } let byteCount = text.utf8.count let exceedsByteThreshold = byteCount >= byteThreshold let exceedsLineThreshold: Bool = { if exceedsByteThreshold { return true } var lineBreaks = 0 var currentLineLength = 0 let csvLongLineThreshold = 16_000 for codeUnit in text.utf16 { if codeUnit == 10 { // '\n' lineBreaks += 1 currentLineLength = 0 if lineBreaks >= lineThreshold { return true } continue } if isCSVLike { currentLineLength += 1 if currentLineLength >= csvLongLineThreshold { return true } } } return false }() #if os(iOS) let isLarge = forceLargeFileMode || exceedsByteThreshold || exceedsLineThreshold #else let isLarge = exceedsByteThreshold || exceedsLineThreshold #endif if largeFileModeEnabled != isLarge { largeFileModeEnabled = isLarge scheduleHighlightRefresh() } } private func updateLargeFileModeForCurrentContext() { if droppedFileLoadInProgress { if !largeFileModeEnabled { largeFileModeEnabled = true scheduleHighlightRefresh() } return } if viewModel.selectedTab?.isLoadingContent == true { if (viewModel.selectedTab?.isLargeFileCandidate == true || currentDocumentUTF16Length >= 300_000), !largeFileModeEnabled { largeFileModeEnabled = true scheduleHighlightRefresh() } return } if viewModel.selectedTab?.isLargeFileCandidate == true || currentDocumentUTF16Length >= 300_000 { if !largeFileModeEnabled { largeFileModeEnabled = true scheduleHighlightRefresh() } return } guard let snapshot = currentContentSnapshot(maxUTF16Length: 280_000) else { return } updateLargeFileMode(for: snapshot) } private func currentContentSnapshot(maxUTF16Length: Int) -> String? { guard currentDocumentUTF16Length <= maxUTF16Length else { return nil } return liveEditorBufferText() ?? currentContentBinding.wrappedValue } private func refreshSecondaryContentViewsIfNeeded() { guard let snapshot = currentContentSnapshot(maxUTF16Length: 280_000) else { scheduleWordCountRefreshForLargeContent() if shouldShowDelimitedTable { delimitedParseTask?.cancel() isBuildingDelimitedTable = false delimitedTableSnapshot = nil } return } scheduleWordCountRefresh(for: snapshot) if shouldShowDelimitedTable { scheduleDelimitedTableRebuild(for: snapshot) } } private func scheduleWordCountRefreshForLargeContent() { wordCountTask?.cancel() if statusWordCount != 0 { statusWordCount = 0 } if let liveText = liveEditorBufferText() { let snapshot = liveText wordCountTask = Task(priority: .utility) { let lineCount = Self.lineCount(for: snapshot) await MainActor.run { statusLineCount = lineCount } } } } private nonisolated static func lineCount(for text: String) -> Int { guard !text.isEmpty else { return 1 } var lineCount = 1 for codeUnit in text.utf16 where codeUnit == 10 { lineCount += 1 } return lineCount } private func scheduleLargeFileModeReevaluation(after delay: TimeInterval) { pendingLargeFileModeReevaluation?.cancel() let work = DispatchWorkItem { updateLargeFileModeForCurrentContext() } pendingLargeFileModeReevaluation = work DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: work) } func recordDiagnostic(_ message: String) { #if DEBUG print("[NVE] \(message)") #endif } func adjustEditorFontSize(_ delta: Double) { let clamped = min(28, max(10, editorFontSize + delta)) if clamped != editorFontSize { editorFontSize = clamped scheduleHighlightRefresh() } } private func pastedFileURL(from text: String) -> URL? { let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines) if trimmed.hasPrefix("file://"), let url = URL(string: trimmed), FileManager.default.fileExists(atPath: url.path) { return url } if trimmed.hasPrefix("/") && FileManager.default.fileExists(atPath: trimmed) { return URL(fileURLWithPath: trimmed) } return nil } private func withCommandEvents(_ view: Content) -> some View { let viewWithEditorActions = view .onReceive(NotificationCenter.default.publisher(for: .clearEditorRequested)) { notif in guard matchesCurrentWindow(notif) else { return } requestClearEditorContent() } .onChange(of: isAutoCompletionEnabled) { _, enabled in if enabled && viewModel.isBrainDumpMode { viewModel.isBrainDumpMode = false UserDefaults.standard.set(false, forKey: "BrainDumpModeEnabled") } syncAppleCompletionAvailability() if enabled && currentLanguage == "plain" && !showLanguageSetupPrompt { showLanguageSetupPrompt = true } } .onReceive(NotificationCenter.default.publisher(for: .toggleVimModeRequested)) { notif in guard matchesCurrentWindow(notif) else { return } vimModeEnabled.toggle() UserDefaults.standard.set(vimModeEnabled, forKey: "EditorVimModeEnabled") UserDefaults.standard.set(vimModeEnabled, forKey: "EditorVimInterceptionEnabled") vimInsertMode = !vimModeEnabled NotificationCenter.default.post( name: .vimModeStateDidChange, object: nil, userInfo: ["insertMode": vimInsertMode] ) } .onReceive(NotificationCenter.default.publisher(for: .toggleSidebarRequested)) { notif in guard matchesCurrentWindow(notif) else { return } toggleSidebarFromToolbar() } .onReceive(NotificationCenter.default.publisher(for: .toggleBrainDumpModeRequested)) { notif in guard matchesCurrentWindow(notif) else { return } #if os(iOS) viewModel.isBrainDumpMode = false UserDefaults.standard.set(false, forKey: "BrainDumpModeEnabled") #else viewModel.isBrainDumpMode.toggle() UserDefaults.standard.set(viewModel.isBrainDumpMode, forKey: "BrainDumpModeEnabled") #endif } .onReceive(NotificationCenter.default.publisher(for: .toggleTranslucencyRequested)) { notif in guard matchesCurrentWindow(notif) else { return } if let enabled = notif.object as? Bool { enableTranslucentWindow = enabled UserDefaults.standard.set(enabled, forKey: "EnableTranslucentWindow") } } .onReceive(NotificationCenter.default.publisher(for: .vimModeStateDidChange)) { notif in if let isInsert = notif.userInfo?["insertMode"] as? Bool { vimInsertMode = isInsert } } let viewWithPanelTriggers = viewWithEditorActions .onReceive(NotificationCenter.default.publisher(for: .showFindReplaceRequested)) { notif in guard matchesCurrentWindow(notif) else { return } showFindReplace = true } .onReceive(NotificationCenter.default.publisher(for: .findNextRequested)) { notif in guard matchesCurrentWindow(notif) else { return } findNext() } .onReceive(NotificationCenter.default.publisher(for: .showQuickSwitcherRequested)) { notif in guard matchesCurrentWindow(notif) else { return } quickSwitcherQuery = "" showQuickSwitcher = true } .onReceive(NotificationCenter.default.publisher(for: .openRecentFileRequested)) { notif in guard matchesCurrentWindow(notif) else { return } guard let url = notif.object as? URL else { return } _ = viewModel.openFile(url: url) } .onReceive(NotificationCenter.default.publisher(for: .recentFilesDidChange)) { _ in recentFilesRefreshToken = UUID() } .onReceive(NotificationCenter.default.publisher(for: .addNextMatchRequested)) { notif in guard matchesCurrentWindow(notif) else { return } addNextMatchSelection() } .onReceive(NotificationCenter.default.publisher(for: .showFindInFilesRequested)) { notif in guard matchesCurrentWindow(notif) else { return } if findInFilesQuery.isEmpty { findInFilesQuery = findQuery } showFindInFiles = true } .onReceive(NotificationCenter.default.publisher(for: .showWelcomeTourRequested)) { notif in guard matchesCurrentWindow(notif) else { return } showWelcomeTour = true } .onReceive(NotificationCenter.default.publisher(for: .showEditorHelpRequested)) { notif in guard matchesCurrentWindow(notif) else { return } showEditorHelp = true } .onReceive(NotificationCenter.default.publisher(for: .showSupportPromptRequested)) { notif in guard matchesCurrentWindow(notif) else { return } showSupportPromptSheet = true } let viewWithPanels = viewWithPanelTriggers .onReceive(NotificationCenter.default.publisher(for: .toggleProjectStructureSidebarRequested)) { notif in guard matchesCurrentWindow(notif) else { return } toggleProjectSidebarFromToolbar() } .onReceive(NotificationCenter.default.publisher(for: .openProjectFolderRequested)) { notif in guard matchesCurrentWindow(notif) else { return } openProjectFolder() } .onReceive(NotificationCenter.default.publisher(for: .showAPISettingsRequested)) { notif in guard matchesCurrentWindow(notif) else { return } openAPISettings() } .onReceive(NotificationCenter.default.publisher(for: .showSettingsRequested)) { notif in guard matchesCurrentWindow(notif) else { return } if let tab = notif.object as? String, !tab.isEmpty { openSettings(tab: tab) } else { openSettings() } } .onReceive(NotificationCenter.default.publisher(for: .closeSelectedTabRequested)) { notif in guard matchesCurrentWindow(notif) else { return } guard let tab = viewModel.selectedTab else { return } requestCloseTab(tab) } .onReceive(NotificationCenter.default.publisher(for: .showUpdaterRequested)) { notif in guard matchesCurrentWindow(notif) else { return } let shouldCheckNow = (notif.object as? Bool) ?? true showUpdaterDialog(checkNow: shouldCheckNow) } .onReceive(NotificationCenter.default.publisher(for: .selectAIModelRequested)) { notif in guard matchesCurrentWindow(notif) else { return } guard let modelRawValue = notif.object as? String, let model = AIModel(rawValue: modelRawValue) else { return } selectedModelRaw = model.rawValue } return viewWithPanels } private func withTypingEvents(_ view: Content) -> some View { #if os(macOS) view .onReceive(NotificationCenter.default.publisher(for: NSText.didChangeNotification)) { notif in guard isAutoCompletionEnabled && !viewModel.isBrainDumpMode && !isApplyingCompletion else { return } guard let changedTextView = notif.object as? NSTextView else { return } guard let activeTextView = NSApp.keyWindow?.firstResponder as? NSTextView, changedTextView === activeTextView else { return } if let hostWindowNumber, let changedWindowNumber = changedTextView.window?.windowNumber, changedWindowNumber != hostWindowNumber { return } guard shouldScheduleCompletion(for: changedTextView) else { return } let signature = completionTriggerSignature(for: changedTextView) guard !signature.isEmpty else { return } if signature == lastCompletionTriggerSignature { return } lastCompletionTriggerSignature = signature completionDebounceTask?.cancel() completionTask?.cancel() let debounce = completionDebounceInterval(for: changedTextView) completionDebounceTask = Task { @MainActor [weak changedTextView] in let delay = UInt64((debounce * 1_000_000_000).rounded()) try? await Task.sleep(nanoseconds: delay) guard !Task.isCancelled, let changedTextView else { return } lastCompletionTriggerSignature = "" performInlineCompletion(for: changedTextView) } } #else view #endif } @ViewBuilder private var platformLayout: some View { #if os(macOS) Group { if shouldUseSplitView { NavigationSplitView { sidebarView } detail: { editorView } .navigationSplitViewColumnWidth(min: 200, ideal: 250, max: 600) .background(editorSurfaceBackgroundStyle) } else { editorView } } .frame(minWidth: 600, minHeight: 400) #else NavigationStack { Group { if shouldUseSplitView { NavigationSplitView { sidebarView } detail: { editorView } .navigationSplitViewColumnWidth(min: 200, ideal: 250, max: 600) .background(editorSurfaceBackgroundStyle) } else { editorView } } } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) #endif } // Layout: NavigationSplitView with optional sidebar and the primary code editor. var body: some View { lifecycleConfiguredRootView #if os(macOS) .background( WindowAccessor { window in updateWindowRegistration(window) } .frame(width: 0, height: 0) ) .onDisappear { handleWindowDisappear() } .onChange(of: viewModel.tabsObservationToken) { _, _ in updateWindowChrome() } .onChange(of: largeFileOpenModeRaw) { _, _ in updateWindowChrome() } .onChange(of: remoteSessionsEnabled) { _, _ in updateWindowChrome() } .onChange(of: remotePreparedTarget) { _, _ in updateWindowChrome() } #endif } private var basePlatformRootView: some View { AnyView(platformLayout) .alert("AI Error", isPresented: showGrokError) { Button("OK") { } } message: { Text(grokErrorMessage.wrappedValue) } .alert( "Whitespace Scalars", isPresented: Binding( get: { whitespaceInspectorMessage != nil }, set: { if !$0 { whitespaceInspectorMessage = nil } } ) ) { Button("OK", role: .cancel) { } } message: { Text(whitespaceInspectorMessage ?? "") } .alert( "PDF Export Failed", isPresented: Binding( get: { markdownPDFExportErrorMessage != nil }, set: { if !$0 { markdownPDFExportErrorMessage = nil } } ) ) { Button("OK", role: .cancel) { } } message: { Text(markdownPDFExportErrorMessage ?? "") } .navigationTitle("Neon Vision Editor") #if os(iOS) .navigationBarTitleDisplayMode(.inline) .background( IPadKeyboardShortcutBridge( onNewTab: { viewModel.addNewTab() }, onOpenFile: { openFileFromToolbar() }, onSave: { saveCurrentTabFromToolbar() }, onFind: { showFindReplace = true }, onFindInFiles: { showFindInFiles = true }, onQuickOpen: { quickSwitcherQuery = "" showQuickSwitcher = true } ) .frame(width: 0, height: 0) ) #endif } private var rootViewWithStateObservers: some View { basePlatformRootView .onAppear { handleSettingsAndEditorDefaultsOnAppear() } .onChange(of: settingsLineWrapEnabled) { _, enabled in let target = projectOverrideLineWrapEnabled ?? enabled if viewModel.isLineWrapEnabled != target { viewModel.isLineWrapEnabled = target } } .onReceive(NotificationCenter.default.publisher(for: .whitespaceScalarInspectionResult)) { notif in guard matchesCurrentWindow(notif) else { return } if let msg = notif.userInfo?[EditorCommandUserInfo.inspectionMessage] as? String { whitespaceInspectorMessage = msg } } .onChange(of: viewModel.isLineWrapEnabled) { _, enabled in guard projectOverrideLineWrapEnabled == nil else { return } if settingsLineWrapEnabled != enabled { settingsLineWrapEnabled = enabled } } .onChange(of: appUpdateManager.automaticPromptToken) { _, _ in if appUpdateManager.consumeAutomaticPromptIfNeeded() { showUpdaterDialog(checkNow: false) } } .onChange(of: settingsThemeName) { _, _ in scheduleHighlightRefresh() } .onChange(of: themeFormattingRefreshSignature) { _, _ in scheduleHighlightRefresh() } .onChange(of: highlightMatchingBrackets) { _, _ in scheduleHighlightRefresh() } .onChange(of: showScopeGuides) { _, _ in scheduleHighlightRefresh() } .onChange(of: highlightScopeBackground) { _, _ in scheduleHighlightRefresh() } .onChange(of: viewModel.isLineWrapEnabled) { _, _ in scheduleHighlightRefresh() } .onChange(of: viewModel.tabsObservationToken) { _, _ in persistSessionIfReady() persistUnsavedDraftSnapshotIfNeeded() } .onChange(of: viewModel.showSidebar) { _, _ in persistSessionIfReady() } .onChange(of: showProjectStructureSidebar) { _, _ in persistSessionIfReady() } .onChange(of: showSupportedProjectFilesOnly) { _, _ in refreshProjectBrowserState() } .onChange(of: showMarkdownPreviewPane) { _, _ in persistSessionIfReady() } } private var rootViewWithPlatformLifecycleObservers: some View { rootViewWithStateObservers #if os(iOS) .onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) { _ in handleAppDidBecomeActive() } #elseif os(macOS) .onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)) { _ in handleAppDidBecomeActive() } .onReceive(NotificationCenter.default.publisher(for: NSApplication.willResignActiveNotification)) { _ in handleAppWillResignActive() } .onReceive(NotificationCenter.default.publisher(for: NSApplication.willTerminateNotification)) { _ in handleAppWillResignActive() } #endif } private var lifecycleConfiguredRootView: some View { rootViewWithPlatformLifecycleObservers .onOpenURL { url in viewModel.openFile(url: url) } #if os(iOS) .onReceive(NotificationCenter.default.publisher(for: UIApplication.willResignActiveNotification)) { _ in persistSessionIfReady() persistUnsavedDraftSnapshotIfNeeded() } #endif .modifier(ModalPresentationModifier(contentView: self)) .onAppear { handleStartupOnAppear() } } private func handleSettingsAndEditorDefaultsOnAppear() { let defaults = UserDefaults.standard if let saved = defaults.stringArray(forKey: quickSwitcherRecentsDefaultsKey) { quickSwitcherRecentItemIDs = saved } if UserDefaults.standard.object(forKey: "SettingsAutoIndent") == nil { autoIndentEnabled = true } #if os(iOS) if defaults.object(forKey: "SettingsShowKeyboardAccessoryBarIOS") == nil { showKeyboardAccessoryBarIOS = false } #endif #if os(macOS) if defaults.object(forKey: "ShowBracketHelperBarMac") == nil { showBracketHelperBarMac = false } #endif let completionResetMigrationKey = "SettingsMigrationCompletionResetV1" if !defaults.bool(forKey: completionResetMigrationKey) { defaults.set(false, forKey: "SettingsCompletionEnabled") defaults.set(true, forKey: completionResetMigrationKey) isAutoCompletionEnabled = false } else { isAutoCompletionEnabled = defaults.bool(forKey: "SettingsCompletionEnabled") } viewModel.isLineWrapEnabled = effectiveLineWrapEnabled syncAppleCompletionAvailability() } private func handleStartupOnAppear() { EditorPerformanceMonitor.shared.markFirstPaint() if !didRunInitialWindowLayoutSetup { // Start with sidebars collapsed only once; otherwise toggles can get reset on layout transitions. viewModel.showSidebar = false showProjectStructureSidebar = false didRunInitialWindowLayoutSetup = true } applyStartupBehaviorIfNeeded() // Keep iOS tab/editor layout stable by forcing Brain Dump off on mobile. #if os(iOS) viewModel.isBrainDumpMode = false UserDefaults.standard.set(false, forKey: "BrainDumpModeEnabled") #else if UserDefaults.standard.object(forKey: "BrainDumpModeEnabled") != nil { viewModel.isBrainDumpMode = UserDefaults.standard.bool(forKey: "BrainDumpModeEnabled") } #endif applyWindowTranslucency(enableTranslucentWindow) if !hasSeenWelcomeTourV1 || welcomeTourSeenRelease != WelcomeTourView.releaseID { DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { showWelcomeTour = true } } if appLaunchCount >= 5 && !hasShownSupportPromptV1 { DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) { guard !showWelcomeTour, !hasShownSupportPromptV1 else { return } hasShownSupportPromptV1 = true showSupportPromptSheet = true } } } #if os(macOS) private func handleWindowDisappear() { completionDebounceTask?.cancel() completionTask?.cancel() lastCompletionTriggerSignature = "" pendingHighlightRefresh?.cancel() pendingLargeFileModeReevaluation?.cancel() completionCache.removeAll(keepingCapacity: false) if let number = hostWindowNumber, let window = NSApp.window(withWindowNumber: number), let delegate = windowCloseConfirmationDelegate, window.delegate === delegate { window.delegate = delegate.forwardedDelegate } windowCloseConfirmationDelegate = nil if let number = hostWindowNumber { WindowViewModelRegistry.shared.unregister(windowNumber: number) } } #endif private func scheduleHighlightRefresh(delay: TimeInterval = 0.05) { pendingHighlightRefresh?.cancel() let work = DispatchWorkItem { highlightRefreshToken &+= 1 } pendingHighlightRefresh = work DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: work) } #if !os(macOS) private func shouldThrottleHeavyEditorFeatures(in nsText: NSString? = nil) -> Bool { if effectiveLargeFileModeEnabled { return true } let length = nsText?.length ?? currentDocumentUTF16Length return length >= EditorPerformanceThresholds.heavyFeatureUTF16Length } #endif private struct ModalPresentationModifier: ViewModifier { let contentView: ContentView func body(content: Content) -> some View { content .sheet(isPresented: contentView.$showFindReplace) { FindReplacePanel( findQuery: contentView.$findQuery, replaceQuery: contentView.$replaceQuery, useRegex: contentView.$findUsesRegex, caseSensitive: contentView.$findCaseSensitive, statusMessage: contentView.$findStatusMessage, onFindNext: { contentView.findNext() }, onReplace: { contentView.replaceSelection() }, onReplaceAll: { contentView.replaceAll() } ) #if canImport(UIKit) .frame(maxWidth: 420) #if os(iOS) .presentationDetents([.height(280), .medium]) .presentationDragIndicator(.visible) .presentationContentInteraction(.scrolls) #endif #else .frame(width: 420) #endif } #if canImport(UIKit) .sheet(isPresented: contentView.$showSettingsSheet) { ConfiguredSettingsView( supportsOpenInTabs: false, supportsTranslucency: false, supportPurchaseManager: contentView.supportPurchaseManager, appUpdateManager: contentView.appUpdateManager ) #if os(iOS) .presentationDetents(contentView.settingsSheetDetents) .presentationDragIndicator(.visible) .presentationContentInteraction(.scrolls) #endif } #endif #if os(iOS) .sheet(isPresented: contentView.$showCompactSidebarSheet) { NavigationStack { SidebarView( content: contentView.currentContent, language: contentView.currentLanguage, translucentBackgroundEnabled: false ) .navigationTitle("Sidebar") .toolbar { ToolbarItem(placement: .topBarTrailing) { Button("Done") { contentView.$showCompactSidebarSheet.wrappedValue = false } } } } .presentationDetents([.medium, .large]) } .sheet(isPresented: contentView.$showCompactProjectSidebarSheet) { NavigationStack { ProjectStructureSidebarView( rootFolderURL: contentView.projectRootFolderURL, nodes: contentView.projectTreeNodes, selectedFileURL: contentView.viewModel.selectedTab?.fileURL, showSupportedFilesOnly: contentView.showSupportedProjectFilesOnly, translucentBackgroundEnabled: false, onOpenFile: { contentView.openFileFromToolbar() }, onOpenFolder: { contentView.openProjectFolder() }, onToggleSupportedFilesOnly: { contentView.showSupportedProjectFilesOnly = $0 }, onOpenProjectFile: { contentView.openProjectFile(url: $0) }, onRefreshTree: { contentView.refreshProjectBrowserState() } ) .navigationTitle("Project Structure") .toolbar { ToolbarItem(placement: .topBarTrailing) { Button("Done") { contentView.$showCompactProjectSidebarSheet.wrappedValue = false } } } } .presentationDetents([.medium, .large]) } .sheet(isPresented: contentView.markdownPreviewSheetPresentationBinding) { NavigationStack { contentView.markdownPreviewPane .navigationTitle("Markdown Preview") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .topBarTrailing) { Button("Done") { contentView.showMarkdownPreviewPane = false } } } } .presentationDetents([.fraction(0.35), .medium, .large], selection: contentView.$markdownPreviewSheetDetent) .presentationDragIndicator(.visible) .presentationContentInteraction(.scrolls) } #endif #if canImport(UIKit) .sheet(isPresented: contentView.$showProjectFolderPicker) { ProjectFolderPicker( onPick: { url in contentView.setProjectFolder(url) contentView.$showProjectFolderPicker.wrappedValue = false }, onCancel: { contentView.$showProjectFolderPicker.wrappedValue = false } ) } #endif .sheet(isPresented: contentView.$showQuickSwitcher) { QuickFileSwitcherPanel( query: contentView.$quickSwitcherQuery, items: contentView.quickSwitcherItems, onSelect: { contentView.selectQuickSwitcherItem($0) }, onTogglePin: { contentView.toggleQuickSwitcherPin($0) } ) } .sheet(item: contentView.$codeSnapshotPayload) { payload in CodeSnapshotComposerView(payload: payload) } .sheet(isPresented: contentView.$showFindInFiles) { FindInFilesPanel( query: contentView.$findInFilesQuery, caseSensitive: contentView.$findInFilesCaseSensitive, results: contentView.findInFilesResults, statusMessage: contentView.findInFilesStatusMessage, onSearch: { contentView.startFindInFiles() }, onSelect: { contentView.selectFindInFilesMatch($0) } ) } .sheet(isPresented: contentView.$showLanguageSetupPrompt) { contentView.languageSetupSheet } .sheet(isPresented: contentView.$showLanguageSearchSheet) { contentView.languageSearchSheet } #if os(macOS) .background( WelcomeTourWindowPresenter( isPresented: contentView.$showWelcomeTour, makeContent: { WelcomeTourView { contentView.$hasSeenWelcomeTourV1.wrappedValue = true contentView.$welcomeTourSeenRelease.wrappedValue = WelcomeTourView.releaseID contentView.$showWelcomeTour.wrappedValue = false } } ) .frame(width: 0, height: 0) ) #else .sheet(isPresented: contentView.$showWelcomeTour) { WelcomeTourView { contentView.$hasSeenWelcomeTourV1.wrappedValue = true contentView.$welcomeTourSeenRelease.wrappedValue = WelcomeTourView.releaseID contentView.$showWelcomeTour.wrappedValue = false } } #endif .sheet(isPresented: contentView.$showEditorHelp) { EditorHelpView { contentView.$showEditorHelp.wrappedValue = false } } .sheet(isPresented: contentView.$showSupportPromptSheet) { SupportPromptSheetView { contentView.$showSupportPromptSheet.wrappedValue = false } .environmentObject(contentView.supportPurchaseManager) } .sheet(isPresented: contentView.$showUpdateDialog) { AppUpdaterDialog(isPresented: contentView.$showUpdateDialog) .environmentObject(contentView.appUpdateManager) } .confirmationDialog("Save changes before closing?", isPresented: contentView.$showUnsavedCloseDialog, titleVisibility: .visible) { Button("Save") { contentView.saveAndClosePendingTab() } Button("Don't Save", role: .destructive) { contentView.discardAndClosePendingTab() } Button("Cancel", role: .cancel) { contentView.$pendingCloseTabID.wrappedValue = nil } } message: { if let pendingCloseTabID = contentView.pendingCloseTabID, let tab = contentView.viewModel.tabs.first(where: { $0.id == pendingCloseTabID }) { Text("\"\(tab.name)\" has unsaved changes.") } else { Text("This file has unsaved changes.") } } .confirmationDialog("Are you sure you want to close all tabs?", isPresented: contentView.$showCloseAllTabsDialog, titleVisibility: .visible) { Button("Close All Tabs", role: .destructive) { contentView.closeAllTabsFromToolbar() } Button("Cancel", role: .cancel) { } } .confirmationDialog("File changed on disk", isPresented: contentView.$showExternalConflictDialog, titleVisibility: .visible) { if let conflict = contentView.viewModel.pendingExternalFileConflict { Button("Reload from Disk", role: .destructive) { contentView.viewModel.resolveExternalConflictByReloadingDisk(tabID: conflict.tabID) } Button("Keep Local and Save") { contentView.viewModel.resolveExternalConflictByKeepingLocal(tabID: conflict.tabID) } Button("Compare") { Task { if let snapshot = await contentView.viewModel.externalConflictComparisonSnapshot(tabID: conflict.tabID) { await MainActor.run { contentView.externalConflictCompareSnapshot = snapshot contentView.showExternalConflictCompareSheet = true } } } } } Button("Cancel", role: .cancel) { } } message: { if let conflict = contentView.viewModel.pendingExternalFileConflict { if let modified = conflict.diskModifiedAt { Text("\"\(conflict.fileURL.lastPathComponent)\" changed on disk at \(modified.formatted(date: .abbreviated, time: .shortened)).") } else { Text("\"\(conflict.fileURL.lastPathComponent)\" changed on disk.") } } else { Text("The file changed on disk while you had unsaved edits.") } } .sheet(isPresented: contentView.$showExternalConflictCompareSheet, onDismiss: { contentView.externalConflictCompareSnapshot = nil }) { if let snapshot = contentView.externalConflictCompareSnapshot { NavigationStack { VStack(spacing: 12) { Text("Compare Local vs Disk: \(snapshot.fileName)") .font(.headline) HStack(spacing: 10) { VStack(alignment: .leading, spacing: 6) { Text("Local") .font(.subheadline.weight(.semibold)) TextEditor(text: .constant(snapshot.localContent)) .font(.system(.footnote, design: .monospaced)) .disabled(true) } VStack(alignment: .leading, spacing: 6) { Text("Disk") .font(.subheadline.weight(.semibold)) TextEditor(text: .constant(snapshot.diskContent)) .font(.system(.footnote, design: .monospaced)) .disabled(true) } } .frame(maxHeight: .infinity) HStack { Button("Use Disk", role: .destructive) { if let conflict = contentView.viewModel.pendingExternalFileConflict { contentView.viewModel.resolveExternalConflictByReloadingDisk(tabID: conflict.tabID) } contentView.showExternalConflictCompareSheet = false } Spacer() Button("Keep Local and Save") { if let conflict = contentView.viewModel.pendingExternalFileConflict { contentView.viewModel.resolveExternalConflictByKeepingLocal(tabID: conflict.tabID) } contentView.showExternalConflictCompareSheet = false } } } .padding(16) .navigationTitle("External Change") } } } .confirmationDialog("Clear editor content?", isPresented: contentView.$showClearEditorConfirmDialog, titleVisibility: .visible) { Button("Clear", role: .destructive) { contentView.clearEditorContent() } Button("Cancel", role: .cancel) {} } message: { Text("This will remove all text in the current editor.") } .alert("Can’t Open File", isPresented: contentView.$showUnsupportedFileAlert) { Button("OK", role: .cancel) { } } message: { Text(String( format: NSLocalizedString( "The file \"%@\" is not supported and can’t be opened.", comment: "Unsupported file alert message" ), contentView.unsupportedFileName )) } #if canImport(UIKit) .fileImporter( isPresented: contentView.$showIOSFileImporter, allowedContentTypes: [.item], allowsMultipleSelection: true ) { result in contentView.handleIOSImportResult(result) } .fileExporter( isPresented: contentView.$showIOSFileExporter, document: contentView.iosExportDocument, contentType: .plainText, defaultFilename: contentView.iosExportFilename ) { result in contentView.handleIOSExportResult(result) } #endif } } private var shouldUseSplitView: Bool { #if os(macOS) return viewModel.showSidebar && !brainDumpLayoutEnabled #else // Keep iPhone layout single-column to avoid horizontal clipping. return viewModel.showSidebar && !brainDumpLayoutEnabled && horizontalSizeClass == .regular #endif } private var themeFormattingRefreshSignature: Int { var signature = 0 if settingsThemeBoldKeywords { signature |= 1 << 0 } if settingsThemeItalicComments { signature |= 1 << 1 } if settingsThemeUnderlineLinks { signature |= 1 << 2 } if settingsThemeBoldMarkdownHeadings { signature |= 1 << 3 } return signature } private var effectiveIndentWidth: Int { projectOverrideIndentWidth ?? indentWidth } private var effectiveLineWrapEnabled: Bool { projectOverrideLineWrapEnabled ?? settingsLineWrapEnabled } private func applyStartupBehaviorIfNeeded() { guard !didApplyStartupBehavior else { return } if startupBehavior == .forceBlankDocument || startupBehavior == .safeMode { viewModel.resetTabsForSessionRestore() viewModel.addNewTab() projectRootFolderURL = nil clearProjectEditorOverrides() projectTreeNodes = [] quickSwitcherProjectFileURLs = [] stopProjectFolderObservation() projectFileIndexSnapshot = .empty isProjectFileIndexing = false projectFileIndexTask?.cancel() projectFileIndexTask = nil didApplyStartupBehavior = true if startupBehavior != .safeMode { persistSessionIfReady() } return } if viewModel.tabs.contains(where: { $0.fileURL != nil }) { didApplyStartupBehavior = true persistSessionIfReady() return } // If both startup toggles are enabled (legacy/default mismatch), prefer session restore. let shouldOpenBlankOnStartup = openWithBlankDocument && !reopenLastSession if shouldOpenBlankOnStartup { viewModel.resetTabsForSessionRestore() viewModel.addNewTab() projectRootFolderURL = nil clearProjectEditorOverrides() projectTreeNodes = [] quickSwitcherProjectFileURLs = [] stopProjectFolderObservation() projectFileIndexSnapshot = .empty isProjectFileIndexing = false projectFileIndexTask?.cancel() projectFileIndexTask = nil didApplyStartupBehavior = true persistSessionIfReady() return } var restoredSessionTabs = false // Restore last session first when enabled. if reopenLastSession { if projectRootFolderURL == nil, let restoredProjectFolderURL = restoredLastSessionProjectFolderURL() { setProjectFolder(restoredProjectFolderURL) } let urls = restoredLastSessionFileURLs() let selectedURL = restoredLastSessionSelectedFileURL() if !urls.isEmpty { viewModel.resetTabsForSessionRestore() for url in urls { viewModel.openFile(url: url) } if let selectedURL { _ = viewModel.focusTabIfOpen(for: selectedURL) } restoredSessionTabs = !viewModel.tabs.isEmpty if viewModel.tabs.isEmpty { viewModel.addNewTab() } } } // Restore unsaved drafts only as fallback when no file session tabs were restored. if !restoredSessionTabs, restoreUnsavedDraftSnapshotIfAvailable() { didApplyStartupBehavior = true persistSessionIfReady() return } #if os(iOS) // Keep mobile layout in a valid tab state so the file tab bar always has content. if viewModel.tabs.isEmpty { viewModel.addNewTab() } #endif restoreLastSessionViewContextIfAvailable() restoreCaretForSelectedSessionFileIfAvailable() didApplyStartupBehavior = true persistSessionIfReady() } func persistSessionIfReady() { guard didApplyStartupBehavior else { return } guard startupBehavior != .safeMode else { return } let fileURLs = viewModel.tabs.compactMap { $0.fileURL } UserDefaults.standard.set(fileURLs.map(\.absoluteString), forKey: "LastSessionFileURLs") UserDefaults.standard.set(viewModel.selectedTab?.fileURL?.absoluteString, forKey: "LastSessionSelectedFileURL") persistLastSessionViewContext() persistLastSessionProjectFolderURL(projectRootFolderURL) #if os(iOS) persistLastSessionSecurityScopedBookmarks(fileURLs: fileURLs, selectedURL: viewModel.selectedTab?.fileURL) #elseif os(macOS) persistLastSessionSecurityScopedBookmarksMac(fileURLs: fileURLs, selectedURL: viewModel.selectedTab?.fileURL) #endif } private func restoredLastSessionFileURLs() -> [URL] { #if os(macOS) let bookmarked = restoreSessionURLsFromSecurityScopedBookmarksMac() if !bookmarked.isEmpty { return bookmarked } #elseif os(iOS) let bookmarked = restoreSessionURLsFromSecurityScopedBookmarks() if !bookmarked.isEmpty { return bookmarked } #endif let stored = UserDefaults.standard.stringArray(forKey: "LastSessionFileURLs") ?? [] var urls: [URL] = [] var seen: Set = [] for raw in stored { guard let parsed = restoredSessionURL(from: raw) else { continue } let standardized = parsed.standardizedFileURL // Only restore files that still exist; avoids empty placeholder tabs on launch. guard FileManager.default.fileExists(atPath: standardized.path) else { continue } let key = standardized.absoluteString if seen.insert(key).inserted { urls.append(standardized) } } return urls } private func restoredLastSessionSelectedFileURL() -> URL? { #if os(macOS) if let bookmarked = restoreSelectedURLFromSecurityScopedBookmarkMac() { return bookmarked } #elseif os(iOS) if let bookmarked = restoreSelectedURLFromSecurityScopedBookmark() { return bookmarked } #endif guard let selectedPath = UserDefaults.standard.string(forKey: "LastSessionSelectedFileURL"), let selectedURL = restoredSessionURL(from: selectedPath) else { return nil } let standardized = selectedURL.standardizedFileURL return FileManager.default.fileExists(atPath: standardized.path) ? standardized : nil } private func restoredSessionURL(from raw: String) -> URL? { // Support both absolute URL strings ("file:///...") and legacy plain paths. if let url = URL(string: raw), url.isFileURL { return url } if raw.hasPrefix("/") { return URL(fileURLWithPath: raw) } return nil } private var lastSessionShowSidebarKey: String { "LastSessionShowSidebarV1" } private var lastSessionShowProjectSidebarKey: String { "LastSessionShowProjectSidebarV1" } private var lastSessionShowMarkdownPreviewKey: String { "LastSessionShowMarkdownPreviewV1" } private var lastSessionCaretByFileURLKey: String { "LastSessionCaretByFileURLV1" } private var lastSessionProjectFolderURLKey: String { "LastSessionProjectFolderURL" } private func persistLastSessionViewContext() { let defaults = UserDefaults.standard defaults.set(viewModel.showSidebar, forKey: lastSessionShowSidebarKey) defaults.set(showProjectStructureSidebar, forKey: lastSessionShowProjectSidebarKey) defaults.set(showMarkdownPreviewPane, forKey: lastSessionShowMarkdownPreviewKey) if let selectedURL = viewModel.selectedTab?.fileURL { let key = selectedURL.standardizedFileURL.absoluteString if !key.isEmpty { sessionCaretByFileURL[key] = max(0, lastCaretLocation) } } defaults.set(sessionCaretByFileURL, forKey: lastSessionCaretByFileURLKey) } private func restoreLastSessionViewContextIfAvailable() { let defaults = UserDefaults.standard if defaults.object(forKey: lastSessionShowSidebarKey) != nil { viewModel.showSidebar = defaults.bool(forKey: lastSessionShowSidebarKey) } if defaults.object(forKey: lastSessionShowProjectSidebarKey) != nil { showProjectStructureSidebar = defaults.bool(forKey: lastSessionShowProjectSidebarKey) } if defaults.object(forKey: lastSessionShowMarkdownPreviewKey) != nil { showMarkdownPreviewPane = defaults.bool(forKey: lastSessionShowMarkdownPreviewKey) } sessionCaretByFileURL = defaults.dictionary(forKey: lastSessionCaretByFileURLKey) as? [String: Int] ?? [:] } private func restoreCaretForSelectedSessionFileIfAvailable() { guard let selectedURL = viewModel.selectedTab?.fileURL?.standardizedFileURL else { return } guard let location = sessionCaretByFileURL[selectedURL.absoluteString], location >= 0 else { return } var userInfo: [String: Any] = [ EditorCommandUserInfo.rangeLocation: location, EditorCommandUserInfo.rangeLength: 0 ] #if os(macOS) if let hostWindowNumber { userInfo[EditorCommandUserInfo.windowNumber] = hostWindowNumber } #endif DispatchQueue.main.asyncAfter(deadline: .now() + 0.12) { NotificationCenter.default.post(name: .moveCursorToRange, object: nil, userInfo: userInfo) } } private func persistLastSessionProjectFolderURL(_ folderURL: URL?) { guard let folderURL else { UserDefaults.standard.removeObject(forKey: lastSessionProjectFolderURLKey) #if os(macOS) UserDefaults.standard.removeObject(forKey: macLastSessionProjectFolderBookmarkKey) #elseif os(iOS) UserDefaults.standard.removeObject(forKey: lastSessionProjectFolderBookmarkKey) #endif return } UserDefaults.standard.set(folderURL.absoluteString, forKey: lastSessionProjectFolderURLKey) #if os(macOS) if let bookmark = makeSecurityScopedBookmarkDataMac(for: folderURL) { UserDefaults.standard.set(bookmark, forKey: macLastSessionProjectFolderBookmarkKey) } else { UserDefaults.standard.removeObject(forKey: macLastSessionProjectFolderBookmarkKey) } #elseif os(iOS) if let bookmark = makeSecurityScopedBookmarkData(for: folderURL) { UserDefaults.standard.set(bookmark, forKey: lastSessionProjectFolderBookmarkKey) } else { UserDefaults.standard.removeObject(forKey: lastSessionProjectFolderBookmarkKey) } #endif } private func restoredLastSessionProjectFolderURL() -> URL? { #if os(macOS) if let bookmarked = restoreProjectFolderURLFromSecurityScopedBookmarkMac() { return bookmarked } #elseif os(iOS) if let bookmarked = restoreProjectFolderURLFromSecurityScopedBookmark() { return bookmarked } #endif guard let raw = UserDefaults.standard.string(forKey: lastSessionProjectFolderURLKey), let parsed = restoredSessionURL(from: raw) else { return nil } let standardized = parsed.standardizedFileURL return FileManager.default.fileExists(atPath: standardized.path) ? standardized : nil } #if os(macOS) private var macLastSessionBookmarksKey: String { "MacLastSessionFileBookmarks" } private var macLastSessionSelectedBookmarkKey: String { "MacLastSessionSelectedFileBookmark" } private var macLastSessionProjectFolderBookmarkKey: String { "MacLastSessionProjectFolderBookmark" } private func persistLastSessionSecurityScopedBookmarksMac(fileURLs: [URL], selectedURL: URL?) { let bookmarkData = fileURLs.compactMap { makeSecurityScopedBookmarkDataMac(for: $0) } UserDefaults.standard.set(bookmarkData, forKey: macLastSessionBookmarksKey) if let selectedURL, let selectedData = makeSecurityScopedBookmarkDataMac(for: selectedURL) { UserDefaults.standard.set(selectedData, forKey: macLastSessionSelectedBookmarkKey) } else { UserDefaults.standard.removeObject(forKey: macLastSessionSelectedBookmarkKey) } } private func restoreSessionURLsFromSecurityScopedBookmarksMac() -> [URL] { guard let saved = UserDefaults.standard.array(forKey: macLastSessionBookmarksKey) as? [Data], !saved.isEmpty else { return [] } var urls: [URL] = [] var seen: Set = [] for data in saved { guard let url = resolveSecurityScopedBookmarkMac(data) else { continue } let standardized = url.standardizedFileURL guard FileManager.default.fileExists(atPath: standardized.path) else { continue } let key = standardized.absoluteString if seen.insert(key).inserted { urls.append(standardized) } } return urls } private func restoreSelectedURLFromSecurityScopedBookmarkMac() -> URL? { guard let data = UserDefaults.standard.data(forKey: macLastSessionSelectedBookmarkKey), let resolved = resolveSecurityScopedBookmarkMac(data) else { return nil } let standardized = resolved.standardizedFileURL return FileManager.default.fileExists(atPath: standardized.path) ? standardized : nil } private func restoreProjectFolderURLFromSecurityScopedBookmarkMac() -> URL? { guard let data = UserDefaults.standard.data(forKey: macLastSessionProjectFolderBookmarkKey), let resolved = resolveSecurityScopedBookmarkMac(data) else { return nil } let standardized = resolved.standardizedFileURL return FileManager.default.fileExists(atPath: standardized.path) ? standardized : nil } private func makeSecurityScopedBookmarkDataMac(for url: URL) -> Data? { let didStartScopedAccess = url.startAccessingSecurityScopedResource() defer { if didStartScopedAccess { url.stopAccessingSecurityScopedResource() } } do { return try url.bookmarkData( options: [.withSecurityScope], includingResourceValuesForKeys: nil, relativeTo: nil ) } catch { return nil } } private func resolveSecurityScopedBookmarkMac(_ data: Data) -> URL? { var isStale = false guard let resolved = try? URL( resolvingBookmarkData: data, options: [.withSecurityScope, .withoutUI], relativeTo: nil, bookmarkDataIsStale: &isStale ) else { return nil } return resolved } #endif private var unsavedDraftSnapshotRegistryKey: String { "UnsavedDraftSnapshotRegistryV1" } private var unsavedDraftSnapshotKey: String { "UnsavedDraftSnapshotV2.\(recoverySnapshotIdentifier)" } private var maxPersistedDraftTabs: Int { 20 } private var maxPersistedDraftUTF16Length: Int { 2_000_000 } private func persistUnsavedDraftSnapshotIfNeeded() { let defaults = UserDefaults.standard let dirtyTabs = viewModel.tabs.filter(\.isDirty) var registry = defaults.stringArray(forKey: unsavedDraftSnapshotRegistryKey) ?? [] guard !dirtyTabs.isEmpty else { defaults.removeObject(forKey: unsavedDraftSnapshotKey) registry.removeAll { $0 == unsavedDraftSnapshotKey } defaults.set(registry, forKey: unsavedDraftSnapshotRegistryKey) return } var savedTabs: [SavedDraftTabSnapshot] = [] savedTabs.reserveCapacity(min(dirtyTabs.count, maxPersistedDraftTabs)) for tab in dirtyTabs.prefix(maxPersistedDraftTabs) { let content = tab.content let nsContent = content as NSString let clampedContent = nsContent.length > maxPersistedDraftUTF16Length ? nsContent.substring(to: maxPersistedDraftUTF16Length) : content savedTabs.append( SavedDraftTabSnapshot( name: tab.name, content: clampedContent, language: tab.language, fileURLString: tab.fileURL?.absoluteString ) ) } let selectedIndex: Int? = { guard let selectedID = viewModel.selectedTabID else { return nil } return dirtyTabs.firstIndex(where: { $0.id == selectedID }) }() let snapshot = SavedDraftSnapshot(tabs: savedTabs, selectedIndex: selectedIndex, createdAt: Date()) guard let encoded = try? JSONEncoder().encode(snapshot) else { return } defaults.set(encoded, forKey: unsavedDraftSnapshotKey) if !registry.contains(unsavedDraftSnapshotKey) { registry.append(unsavedDraftSnapshotKey) defaults.set(registry, forKey: unsavedDraftSnapshotRegistryKey) } } private func restoreUnsavedDraftSnapshotIfAvailable() -> Bool { let defaults = UserDefaults.standard let keys = defaults.stringArray(forKey: unsavedDraftSnapshotRegistryKey) ?? [] guard !keys.isEmpty else { return false } var snapshots: [SavedDraftSnapshot] = [] for key in keys { guard let data = defaults.data(forKey: key), let snapshot = try? JSONDecoder().decode(SavedDraftSnapshot.self, from: data), !snapshot.tabs.isEmpty else { continue } snapshots.append(snapshot) } guard !snapshots.isEmpty else { return false } snapshots.sort { $0.createdAt < $1.createdAt } let mergedTabs = snapshots.flatMap(\.tabs) guard !mergedTabs.isEmpty else { return false } let restoredTabs = mergedTabs.map { saved in EditorViewModel.RestoredTabSnapshot( name: saved.name, content: saved.content, language: saved.language, fileURL: saved.fileURLString.flatMap(URL.init(string:)), languageLocked: true, isDirty: true, lastSavedFingerprint: nil, lastKnownFileModificationDate: nil ) } viewModel.restoreTabsFromSnapshot(restoredTabs, selectedIndex: nil) for key in keys { defaults.removeObject(forKey: key) } defaults.removeObject(forKey: unsavedDraftSnapshotRegistryKey) return true } #if os(iOS) private var lastSessionBookmarksKey: String { "LastSessionFileBookmarks" } private var lastSessionSelectedBookmarkKey: String { "LastSessionSelectedFileBookmark" } private var lastSessionProjectFolderBookmarkKey: String { "LastSessionProjectFolderBookmark" } private func persistLastSessionSecurityScopedBookmarks(fileURLs: [URL], selectedURL: URL?) { let bookmarkData = fileURLs.compactMap { makeSecurityScopedBookmarkData(for: $0) } UserDefaults.standard.set(bookmarkData, forKey: lastSessionBookmarksKey) if let selectedURL, let selectedData = makeSecurityScopedBookmarkData(for: selectedURL) { UserDefaults.standard.set(selectedData, forKey: lastSessionSelectedBookmarkKey) } else { UserDefaults.standard.removeObject(forKey: lastSessionSelectedBookmarkKey) } } private func restoreSessionURLsFromSecurityScopedBookmarks() -> [URL] { guard let saved = UserDefaults.standard.array(forKey: lastSessionBookmarksKey) as? [Data], !saved.isEmpty else { return [] } var urls: [URL] = [] var seen: Set = [] for data in saved { guard let url = resolveSecurityScopedBookmark(data) else { continue } let key = url.standardizedFileURL.absoluteString if seen.insert(key).inserted { urls.append(url) } } return urls } private func restoreSelectedURLFromSecurityScopedBookmark() -> URL? { guard let data = UserDefaults.standard.data(forKey: lastSessionSelectedBookmarkKey) else { return nil } return resolveSecurityScopedBookmark(data) } private func restoreProjectFolderURLFromSecurityScopedBookmark() -> URL? { guard let data = UserDefaults.standard.data(forKey: lastSessionProjectFolderBookmarkKey), let resolved = resolveSecurityScopedBookmark(data) else { return nil } let standardized = resolved.standardizedFileURL return FileManager.default.fileExists(atPath: standardized.path) ? standardized : nil } private func makeSecurityScopedBookmarkData(for url: URL) -> Data? { do { return try url.bookmarkData( options: [], includingResourceValuesForKeys: nil, relativeTo: nil ) } catch { return nil } } private func resolveSecurityScopedBookmark(_ data: Data) -> URL? { var isStale = false guard let resolved = try? URL( resolvingBookmarkData: data, options: [.withoutUI], relativeTo: nil, bookmarkDataIsStale: &isStale ) else { return nil } return resolved } #endif // Sidebar shows a lightweight table of contents (TOC) derived from the current document. @ViewBuilder var sidebarView: some View { if viewModel.showSidebar && !brainDumpLayoutEnabled { SidebarView( content: sidebarTOCContent, language: currentLanguage, translucentBackgroundEnabled: enableTranslucentWindow ) .frame(minWidth: 200, idealWidth: 250, maxWidth: 600) .safeAreaInset(edge: .bottom) { #if os(iOS) iOSHorizontalSurfaceDivider #else Divider() #endif } .background(editorSurfaceBackgroundStyle) } else { EmptyView() } } // Bindings that resolve to the active tab (if present) or fallback single-document state. var currentContentBinding: Binding { if let selectedID = viewModel.selectedTabID, viewModel.selectedTab != nil { return Binding( get: { viewModel.selectedTab?.content ?? singleContent }, set: { newValue in guard viewModel.selectedTab?.isReadOnlyPreview != true else { return } viewModel.updateTabContent(tabID: selectedID, content: newValue) } ) } else { return $singleContent } } var currentLanguageBinding: Binding { if let selectedID = viewModel.selectedTabID, viewModel.selectedTab != nil { return Binding( get: { viewModel.selectedTab?.language ?? singleLanguage }, set: { newValue in viewModel.updateTabLanguage(tabID: selectedID, language: newValue) } ) } else { return $singleLanguage } } var currentLanguagePickerBinding: Binding { Binding( get: { currentLanguageBinding.wrappedValue }, set: { newValue in if let tab = viewModel.selectedTab { viewModel.updateTabLanguage(tabID: tab.id, language: newValue) } else { singleLanguage = newValue } } ) } var currentContent: String { currentContentBinding.wrappedValue } var currentLanguage: String { currentLanguageBinding.wrappedValue } private var currentDocumentUTF16Length: Int { if let tab = viewModel.selectedTab { return tab.contentUTF16Length } return (singleContent as NSString).length } private var effectiveLargeFileModeEnabled: Bool { if largeFileModeEnabled { return true } if droppedFileLoadInProgress { return true } if viewModel.selectedTab?.isLargeFileCandidate == true { return true } return currentDocumentUTF16Length >= 300_000 } private var isSelectedTabReadOnlyPreview: Bool { viewModel.selectedTab?.isReadOnlyPreview == true } private var shouldUseDeferredLargeFileOpenMode: Bool { largeFileOpenModeRaw == "deferred" || largeFileOpenModeRaw == "plainText" } private var currentLargeFileOpenModeLabel: String { switch largeFileOpenModeRaw { case "standard": return "Standard" case "plainText": return "Plain Text" default: return "Deferred" } } private var largeFileStatusBadgeText: String { guard effectiveLargeFileModeEnabled else { return "" } return "Large File • \(currentLargeFileOpenModeLabel)" } private var remoteSessionStatusBadgeText: String { guard remoteSessionsEnabled else { return "" } if remoteSessionStore.isRemotePreviewConnecting { return "Local Workspace • Remote Connecting" } if remoteSessionStore.isRemotePreviewConnected { return "Local Workspace • Remote Session Active" } if remoteSessionStore.isRemotePreviewReady { return "Local Workspace • Remote Selected" } return remotePreparedTarget.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? "Local Workspace • Remote Enabled" : "Local Workspace • Remote Ready" } private var windowSubtitleText: String { [largeFileStatusBadgeText, remoteSessionStatusBadgeText] .filter { !$0.isEmpty } .joined(separator: " • ") } private var sidebarTOCContent: String { if effectiveLargeFileModeEnabled || currentDocumentUTF16Length >= 400_000 { return "" } return currentContent } var brainDumpLayoutEnabled: Bool { #if os(macOS) return viewModel.isBrainDumpMode #else return false #endif } func toggleAutoCompletion() { let willEnable = !isAutoCompletionEnabled if willEnable && viewModel.isBrainDumpMode { viewModel.isBrainDumpMode = false UserDefaults.standard.set(false, forKey: "BrainDumpModeEnabled") } isAutoCompletionEnabled.toggle() syncAppleCompletionAvailability() if willEnable { maybePromptForLanguageSetup() } } private func maybePromptForLanguageSetup() { guard currentLanguage == "plain" else { return } languagePromptSelection = currentLanguage == "plain" ? "plain" : currentLanguage languagePromptInsertTemplate = false showLanguageSetupPrompt = true } private func syncAppleCompletionAvailability() { #if USE_FOUNDATION_MODELS && canImport(FoundationModels) // Keep Apple Foundation Models in sync with the completion master toggle. AppleFM.isEnabled = isAutoCompletionEnabled #endif } private func applyLanguageSelection(language: String, insertTemplate: Bool) { let contentIsEmpty = currentContent.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty if let tab = viewModel.selectedTab { viewModel.updateTabLanguage(tabID: tab.id, language: language) if insertTemplate, contentIsEmpty, let template = starterTemplate(for: language) { viewModel.updateTabContent(tabID: tab.id, content: template) } } else { singleLanguage = language if insertTemplate, contentIsEmpty, let template = starterTemplate(for: language) { singleContent = template } } } private var languageSetupSheet: some View { let contentIsEmpty = currentContent.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty let canInsertTemplate = contentIsEmpty return VStack(alignment: .leading, spacing: 16) { Text("Choose a language for code completion") .font(.headline) Text("You can change this later from the Language picker.") .font(.subheadline) .foregroundColor(.secondary) Picker("Language", selection: $languagePromptSelection) { ForEach(languageOptions, id: \.self) { lang in Text(languageLabel(for: lang)).tag(lang) } } .labelsHidden() .frame(maxWidth: 240) if canInsertTemplate { Toggle("Insert starter template", isOn: $languagePromptInsertTemplate) } HStack { Button("Use Plain Text") { applyLanguageSelection(language: "plain", insertTemplate: false) showLanguageSetupPrompt = false } Spacer() Button("Use Selected Language") { applyLanguageSelection(language: languagePromptSelection, insertTemplate: languagePromptInsertTemplate) showLanguageSetupPrompt = false } .keyboardShortcut(.defaultAction) } } .padding(20) .frame(minWidth: 340) } var languageOptions: [String] { ["swift", "python", "javascript", "typescript", "php", "java", "kotlin", "go", "ruby", "rust", "cobol", "dotenv", "proto", "graphql", "rst", "nginx", "sql", "html", "expressionengine", "css", "c", "cpp", "csharp", "objective-c", "json", "xml", "yaml", "toml", "csv", "ini", "vim", "log", "ipynb", "markdown", "tex", "bash", "zsh", "powershell", "standard", "plain"] } func languageLabel(for lang: String) -> String { switch lang { case "php": return "PHP" case "cobol": return "COBOL" case "dotenv": return "Dotenv" case "proto": return "Proto" case "graphql": return "GraphQL" case "rst": return "reStructuredText" case "nginx": return "Nginx" case "objective-c": return "Objective-C" case "csharp": return "C#" case "c": return "C" case "cpp": return "C++" case "json": return "JSON" case "xml": return "XML" case "yaml": return "YAML" case "toml": return "TOML" case "csv": return "CSV" case "ini": return "INI" case "sql": return "SQL" case "vim": return "Vim" case "log": return "Log" case "ipynb": return "Jupyter Notebook" case "tex": return "TeX" case "html": return "HTML" case "expressionengine": return "ExpressionEngine" case "css": return "CSS" case "standard": return "Standard" default: return lang.capitalized } } var filteredLanguageOptions: [String] { let query = languageSearchQuery.trimmingCharacters(in: .whitespacesAndNewlines) guard !query.isEmpty else { return languageOptions } let lower = query.lowercased() return languageOptions.filter { lang in lang.localizedCaseInsensitiveContains(lower) || languageLabel(for: lang).localizedCaseInsensitiveContains(lower) } } func presentLanguageSearchSheet() { languageSearchQuery = "" showLanguageSearchSheet = true } private var languageSearchSheet: some View { NavigationStack { List(filteredLanguageOptions, id: \.self) { lang in Button { currentLanguagePickerBinding.wrappedValue = lang showLanguageSearchSheet = false } label: { HStack { Text(languageLabel(for: lang)) .foregroundStyle(.primary) Spacer(minLength: 8) if currentLanguage == lang { Image(systemName: "checkmark") .foregroundStyle(NeonUIStyle.accentBlue) } } .contentShape(Rectangle()) } .buttonStyle(.plain) .accessibilityLabel(languageLabel(for: lang)) } .navigationTitle("Select Language") #if os(iOS) .navigationBarTitleDisplayMode(.inline) #endif .searchable(text: $languageSearchQuery, prompt: "Search language") .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Close") { showLanguageSearchSheet = false } } } } #if os(iOS) .presentationDetents([.medium, .large]) #endif } private func starterTemplate(for language: String) -> String? { if let override = UserDefaults.standard.string(forKey: templateOverrideKey(for: language)), !override.isEmpty { return override } switch language { case "swift": return "import Foundation\n\n// TODO: Add code here\n" case "python": return "def main():\n pass\n\n\nif __name__ == \"__main__\":\n main()\n" case "javascript": return "\"use strict\";\n\nfunction main() {\n // TODO: Add code here\n}\n\nmain();\n" case "typescript": return "function main(): void {\n // TODO: Add code here\n}\n\nmain();\n" case "java": return "public class Main {\n public static void main(String[] args) {\n // TODO: Add code here\n }\n}\n" case "kotlin": return "fun main() {\n // TODO: Add code here\n}\n" case "go": return "package main\n\nimport \"fmt\"\n\nfunc main() {\n fmt.Println(\"Hello\")\n}\n" case "ruby": return "def main\n # TODO: Add code here\nend\n\nmain\n" case "rust": return "fn main() {\n // TODO: Add code here\n}\n" case "php": return "\n\nint main(void) {\n // TODO: Add code here\n return 0;\n}\n" case "cpp": return "#include \n\nint main() {\n // TODO: Add code here\n return 0;\n}\n" case "csharp": return "using System;\n\npublic class Program {\n public static void Main(string[] args) {\n // TODO: Add code here\n }\n}\n" case "objective-c": return "#import \n\nint main(int argc, const char * argv[]) {\n @autoreleasepool {\n // TODO: Add code here\n }\n return 0;\n}\n" case "html": return "\n\n\n \n \n Document\n\n\n\n\n\n" case "expressionengine": return "{exp:channel:entries channel=\"news\" limit=\"10\"}\n
\n

{title}

\n

{summary}

\n
\n{/exp:channel:entries}\n" case "css": return "/* TODO: Add styles here */\n\nbody {\n margin: 0;\n}\n" case "sql": return "-- TODO: Add queries here\n" case "markdown": return "# Title\n\nWrite here.\n" case "tex": return "\\documentclass{article}\n\\usepackage[utf8]{inputenc}\n\n\\begin{document}\n\\section{Title}\n\nTODO\n\n\\end{document}\n" case "yaml": return "# TODO: Add config here\n" case "json": return "{\n \"todo\": true\n}\n" case "xml": return "\n\n true\n\n" case "toml": return "# TODO = \"value\"\n" case "csv": return "col1,col2\nvalue1,value2\n" case "ini": return "[section]\nkey=value\n" case "vim": return "\" TODO: Add vim config here\n" case "log": return "INFO: TODO\n" case "ipynb": return "{\n \"cells\": [],\n \"metadata\": {},\n \"nbformat\": 4,\n \"nbformat_minor\": 5\n}\n" case "bash": return "#!/usr/bin/env bash\n\nset -euo pipefail\n\n# TODO: Add script here\n" case "zsh": return "#!/usr/bin/env zsh\n\nset -euo pipefail\n\n# TODO: Add script here\n" case "powershell": return "# TODO: Add script here\n" case "standard": return "// TODO: Add code here\n" case "plain": return "TODO\n" default: return "TODO\n" } } private func templateOverrideKey(for language: String) -> String { "TemplateOverride_\(language)" } func insertTemplateForCurrentLanguage() { let language = currentLanguage guard let template = starterTemplate(for: language) else { return } editorExternalMutationRevision &+= 1 let sourceContent = liveEditorBufferText() ?? currentContentBinding.wrappedValue let updated: String if sourceContent.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { updated = template } else { updated = sourceContent + (sourceContent.hasSuffix("\n") ? "\n" : "\n\n") + template } currentContentBinding.wrappedValue = updated } private func detectLanguageWithAppleIntelligence(_ text: String) async -> String { // Supported languages in our picker let supported = ["swift", "python", "javascript", "typescript", "php", "java", "kotlin", "go", "ruby", "rust", "cobol", "dotenv", "proto", "graphql", "rst", "nginx", "sql", "html", "expressionengine", "css", "c", "cpp", "objective-c", "csharp", "json", "xml", "yaml", "toml", "csv", "ini", "vim", "log", "ipynb", "markdown", "tex", "bash", "zsh", "powershell", "standard", "plain"] #if USE_FOUNDATION_MODELS && canImport(FoundationModels) // Attempt a lightweight model-based detection via AppleIntelligenceAIClient if available do { let client = AppleIntelligenceAIClient() var response = "" for await chunk in client.streamSuggestions(prompt: "Detect the programming or markup language of the following snippet and answer with one of: \(supported.joined(separator: ", ")). If none match, reply with 'swift'.\n\nSnippet:\n\n\(text)\n\nAnswer:") { response += chunk } let detectedRaw = response.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines).lowercased() if let match = supported.first(where: { detectedRaw.contains($0) }) { return match } } #endif // Heuristic fallback let lower = text.lowercased() // Normalize common C# indicators to "csharp" to ensure the picker has a matching tag if lower.contains("c#") || lower.contains("c sharp") || lower.range(of: #"\bcs\b"#, options: .regularExpression) != nil || lower.contains(".cs") { return "csharp" } if lower.contains("") || lower.contains("$_get") || lower.contains("$_post") || lower.contains("$_server") { return "php" } if lower.range(of: #"\{/?exp:[A-Za-z0-9_:-]+[^}]*\}"#, options: .regularExpression) != nil || lower.range(of: #"\{if(?::elseif)?\b[^}]*\}|\{\/if\}|\{:else\}"#, options: .regularExpression) != nil || lower.range(of: #"\{!--[\s\S]*?--\}"#, options: .regularExpression) != nil { return "expressionengine" } if lower.contains("syntax = \"proto") || lower.contains("message ") || (lower.contains("enum ") && lower.contains("rpc ")) { return "proto" } if lower.contains("type query") || lower.contains("schema {") || (lower.contains("interface ") && lower.contains("implements ")) { return "graphql" } if lower.contains("server {") || lower.contains("http {") || lower.contains("location /") { return "nginx" } if lower.contains(".. code-block::") || lower.contains(".. toctree::") || (lower.contains("::") && lower.contains("\n====")) { return "rst" } if lower.contains("\\documentclass") || lower.contains("\\usepackage") || lower.contains("\\begin{document}") || lower.contains("\\end{document}") { return "tex" } if lower.contains("\n") && lower.range(of: #"(?m)^[A-Z_][A-Z0-9_]*=.*$"#, options: .regularExpression) != nil { return "dotenv" } if lower.contains("identification division") || lower.contains("procedure division") || lower.contains("working-storage section") || lower.contains("environment division") { return "cobol" } if text.contains(",") && text.contains("\n") { let lines = text.split(separator: "\n", omittingEmptySubsequences: true) if lines.count >= 2 { let commaCounts = lines.prefix(6).map { line in line.filter { $0 == "," }.count } if let firstCount = commaCounts.first, firstCount > 0 && commaCounts.dropFirst().allSatisfy({ $0 == firstCount || abs($0 - firstCount) <= 1 }) { return "csv" } } } // C# strong heuristic if lower.contains("using system") || lower.contains("namespace ") || lower.contains("public class") || lower.contains("public static void main") || lower.contains("static void main") || lower.contains("console.writeline") || lower.contains("console.readline") || lower.contains("class program") || lower.contains("get; set;") || lower.contains("list<") || lower.contains("dictionary<") || lower.contains("ienumerable<") || lower.range(of: #"\[[A-Za-z_][A-Za-z0-9_]*\]"#, options: .regularExpression) != nil { return "csharp" } if lower.contains("import swift") || lower.contains("struct ") || lower.contains("func ") { return "swift" } if lower.contains("def ") || (lower.contains("class ") && lower.contains(":")) { return "python" } if lower.contains("function ") || lower.contains("const ") || lower.contains("let ") || lower.contains("=>") { return "javascript" } // XML if lower.contains("")) { return "xml" } // YAML if lower.contains(": ") && (lower.contains("- ") || lower.contains("\n ")) && !lower.contains(";") { return "yaml" } // TOML / INI if lower.range(of: #"^\[[^\]]+\]"#, options: [.regularExpression, .anchored]) != nil || (lower.contains("=") && lower.contains("\n[")) { return lower.contains("toml") ? "toml" : "ini" } // SQL if lower.range(of: #"\b(select|insert|update|delete|create\s+table|from|where|join)\b"#, options: .regularExpression) != nil { return "sql" } // Go if lower.contains("package ") && lower.contains("func ") { return "go" } // Java if lower.contains("public class") || lower.contains("public static void main") { return "java" } // Kotlin if (lower.contains("fun ") || lower.contains("val ")) || (lower.contains("var ") && lower.contains(":")) { return "kotlin" } // TypeScript if lower.contains("interface ") || (lower.contains("type ") && lower.contains(":")) || lower.contains(": string") { return "typescript" } // Ruby if lower.contains("def ") || (lower.contains("end") && lower.contains("class ")) { return "ruby" } // Rust if lower.contains("fn ") || lower.contains("let mut ") || lower.contains("pub struct") { return "rust" } // Objective-C if lower.contains("@interface") || lower.contains("@implementation") || lower.contains("#import ") { return "objective-c" } // INI if lower.range(of: #"^;.*$"#, options: .regularExpression) != nil || lower.range(of: #"^\w+\s*=\s*.*$"#, options: .regularExpression) != nil { return "ini" } if lower.contains(" some View { HStack(spacing: 0) { ForEach(Array(cells.enumerated()), id: \.offset) { _, cell in Text(cell) .font(.system(size: 12, weight: isHeader ? .semibold : .regular, design: .monospaced)) .lineLimit(1) .truncationMode(.tail) .frame(width: 220, alignment: .leading) .padding(.horizontal, 8) .padding(.vertical, isHeader ? 7 : 6) .overlay(alignment: .trailing) { Rectangle() .fill(Color.secondary.opacity(0.16)) .frame(width: 1) } } } .background( isHeader ? Color.secondary.opacity(0.12) : ((rowIndex ?? 0).isMultiple(of: 2) ? Color.secondary.opacity(0.04) : Color.clear) ) } private func scheduleDelimitedTableRebuild(for text: String) { guard isDelimitedFileLanguage else { delimitedParseTask?.cancel() isBuildingDelimitedTable = false delimitedTableSnapshot = nil delimitedTableStatus = "" return } guard shouldShowDelimitedTable else { return } delimitedParseTask?.cancel() isBuildingDelimitedTable = true delimitedTableStatus = "Parsing…" let separator = delimitedSeparator delimitedParseTask = Task { let source = text let parsed = await Task.detached(priority: .utility) { Self.buildDelimitedTableSnapshot(from: source, separator: separator, maxRows: 5000, maxColumns: 60) }.value guard !Task.isCancelled else { return } isBuildingDelimitedTable = false switch parsed { case .success(let snapshot): delimitedTableSnapshot = snapshot delimitedTableStatus = "" case .failure(let error): delimitedTableSnapshot = nil delimitedTableStatus = error.localizedDescription } } } private nonisolated static func buildDelimitedTableSnapshot( from text: String, separator: Character, maxRows: Int, maxColumns: Int ) -> Result { guard !text.isEmpty else { return .failure(DelimitedTableParseError(message: "No data in file.")) } var rows: [[String]] = [] rows.reserveCapacity(min(maxRows, 512)) var totalRows = 0 for line in text.split(separator: "\n", omittingEmptySubsequences: false) { totalRows += 1 if rows.count < maxRows { rows.append(parseDelimitedLine(String(line), separator: separator, maxColumns: maxColumns)) } } guard !rows.isEmpty else { return .failure(DelimitedTableParseError(message: "No rows found.")) } let rawHeader = rows.removeFirst() let visibleColumns = max(rawHeader.count, rows.first?.count ?? 0) let header: [String] = { if rawHeader.isEmpty { return (0..= visibleColumns { return row } return row + Array(repeating: "", count: visibleColumns - row.count) } return .success( DelimitedTableSnapshot( header: header, rows: normalizedRows, totalRows: totalRows, displayedRows: rows.count, truncated: totalRows > maxRows ) ) } private nonisolated static func parseDelimitedLine( _ line: String, separator: Character, maxColumns: Int ) -> [String] { if line.isEmpty { return [""] } var result: [String] = [] result.reserveCapacity(min(32, maxColumns)) var field = "" var inQuotes = false var iterator = line.makeIterator() while let char = iterator.next() { if char == "\"" { if inQuotes { if let next = iterator.next() { if next == "\"" { field.append("\"") } else { inQuotes = false if next == separator { result.append(field) field.removeAll(keepingCapacity: true) } else { field.append(next) } } } else { inQuotes = false } } else { inQuotes = true } continue } if char == separator && !inQuotes { result.append(field) field.removeAll(keepingCapacity: true) if result.count >= maxColumns { return result } continue } field.append(char) } result.append(field) if result.count > maxColumns { return Array(result.prefix(maxColumns)) } return result } var editorView: some View { @Bindable var bindableViewModel = viewModel let shouldThrottleFeatures = shouldThrottleHeavyEditorFeatures() let effectiveHighlightCurrentLine = highlightCurrentLine && !shouldThrottleFeatures let effectiveBracketHighlight = highlightMatchingBrackets && !shouldThrottleFeatures let effectiveScopeGuides = showScopeGuides && !shouldThrottleFeatures let effectiveScopeBackground = highlightScopeBackground && !shouldThrottleFeatures let content = HStack(spacing: 0) { if showProjectStructureSidebar && projectNavigatorPlacement == .leading && !brainDumpLayoutEnabled { projectStructureSidebarPanel projectSidebarResizeHandle } VStack(spacing: 0) { if !useIPhoneUnifiedTopHost && !brainDumpLayoutEnabled { tabBarView } #if os(macOS) if showBracketHelperBarMac { bracketHelperBar } #endif if isDelimitedFileLanguage && !brainDumpLayoutEnabled { delimitedModeControl } Group { if shouldShowDelimitedTable && !brainDumpLayoutEnabled { delimitedTableView } else if shouldUseDeferredLargeFileOpenMode, viewModel.selectedTab?.isLoadingContent == true, (viewModel.selectedTab?.isLargeFileCandidate == true || currentDocumentUTF16Length >= 300_000 || largeFileModeEnabled) { largeFileLoadingPlaceholder } else { // Single editor (no TabView) CustomTextEditor( text: currentContentBinding, documentID: viewModel.selectedTabID, externalEditRevision: editorExternalMutationRevision, language: currentLanguage, colorScheme: colorScheme, fontSize: editorFontSize, isLineWrapEnabled: $bindableViewModel.isLineWrapEnabled, isLargeFileMode: effectiveLargeFileModeEnabled, translucentBackgroundEnabled: enableTranslucentWindow, showKeyboardAccessoryBar: { #if os(iOS) showKeyboardAccessoryBarIOS #else true #endif }(), showLineNumbers: showLineNumbers, showInvisibleCharacters: false, highlightCurrentLine: effectiveHighlightCurrentLine, highlightMatchingBrackets: effectiveBracketHighlight, showScopeGuides: effectiveScopeGuides, highlightScopeBackground: effectiveScopeBackground, indentStyle: indentStyle, indentWidth: effectiveIndentWidth, autoIndentEnabled: autoIndentEnabled, autoCloseBracketsEnabled: autoCloseBracketsEnabled, highlightRefreshToken: highlightRefreshToken, isTabLoadingContent: viewModel.selectedTab?.isLoadingContent ?? false, isReadOnly: isSelectedTabReadOnlyPreview, onTextMutation: { mutation in viewModel.applyTabContentEdit( tabID: mutation.documentID, range: mutation.range, replacement: mutation.replacement ) } ) .id(currentLanguage) .overlay { if shouldShowStartupOverlay { startupOverlay } } } } .frame(maxWidth: brainDumpLayoutEnabled ? 920 : .infinity) .frame(maxHeight: .infinity) .padding(.horizontal, brainDumpLayoutEnabled ? 24 : 0) .padding(.vertical, brainDumpLayoutEnabled ? 40 : 0) .background( Group { if enableTranslucentWindow { Color.clear.background(editorSurfaceBackgroundStyle) } else { #if os(iOS) iOSNonTranslucentSurfaceColor #else Color.clear #endif } } ) .overlay(alignment: .topTrailing) { if effectiveLargeFileModeEnabled && !brainDumpLayoutEnabled { largeFileSessionBadge .padding(.top, 10) .padding(.trailing, 12) .zIndex(5) } } if !brainDumpLayoutEnabled { #if os(macOS) wordCountView #endif } } .frame( maxWidth: .infinity, maxHeight: .infinity, alignment: brainDumpLayoutEnabled ? .top : .topLeading ) if canShowMarkdownPreviewSplitPane && showMarkdownPreviewPane && currentLanguage == "markdown" && !brainDumpLayoutEnabled { #if os(iOS) iOSPaneDivider #else Divider() #endif markdownPreviewPane .frame(minWidth: 280, idealWidth: 420, maxWidth: 680, maxHeight: .infinity) } if showProjectStructureSidebar && projectNavigatorPlacement == .trailing && !brainDumpLayoutEnabled { projectSidebarResizeHandle projectStructureSidebarPanel } } .background( Group { if brainDumpLayoutEnabled && enableTranslucentWindow { Color.clear.background(editorSurfaceBackgroundStyle) } else { #if os(iOS) Color.clear.background(editorSurfaceBackgroundStyle) #else Color.clear #endif } } ) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) #if os(iOS) let contentWithTopChrome = useIPhoneUnifiedTopHost ? AnyView( content.safeAreaInset(edge: .top, spacing: 0) { iPhoneUnifiedTopChromeHost } ) : AnyView(content) #else let contentWithTopChrome = AnyView(content) #endif let withEvents = withTypingEvents( withCommandEvents( withBaseEditorEvents(contentWithTopChrome) ) ) return withEvents .onAppear { if isDelimitedFileLanguage { delimitedViewMode = .table } else { delimitedViewMode = .text } refreshSecondaryContentViewsIfNeeded() } .onChange(of: viewModel.tabsObservationToken) { _, _ in refreshSecondaryContentViewsIfNeeded() } .onChange(of: delimitedViewMode) { _, newValue in if newValue == .table { refreshSecondaryContentViewsIfNeeded() } else { delimitedParseTask?.cancel() isBuildingDelimitedTable = false } } .onChange(of: currentLanguage) { _, _ in if isDelimitedFileLanguage { if delimitedViewMode == .text { // Keep explicit user choice when already in text mode. } else { delimitedViewMode = .table } if shouldShowDelimitedTable { refreshSecondaryContentViewsIfNeeded() } } else { delimitedViewMode = .text delimitedParseTask?.cancel() isBuildingDelimitedTable = false delimitedTableSnapshot = nil delimitedTableStatus = "" } } .onDisappear { wordCountTask?.cancel() delimitedParseTask?.cancel() } .onChange(of: enableTranslucentWindow) { _, newValue in applyWindowTranslucency(newValue) // Force immediate recolor when translucency changes so syntax highlighting stays visible. highlightRefreshToken &+= 1 } #if os(iOS) .onChange(of: showKeyboardAccessoryBarIOS) { _, isVisible in NotificationCenter.default.post( name: .keyboardAccessoryBarVisibilityChanged, object: isVisible ) } .onChange(of: horizontalSizeClass) { _, newClass in if UIDevice.current.userInterfaceIdiom == .pad && newClass != .regular && showMarkdownPreviewPane { showMarkdownPreviewPane = false } } .onChange(of: showSettingsSheet) { _, isPresented in if isPresented { if previousKeyboardAccessoryVisibility == nil { previousKeyboardAccessoryVisibility = showKeyboardAccessoryBarIOS } showKeyboardAccessoryBarIOS = false } else if let previousKeyboardAccessoryVisibility { showKeyboardAccessoryBarIOS = previousKeyboardAccessoryVisibility self.previousKeyboardAccessoryVisibility = nil } } #endif #if os(macOS) .onChange(of: macTranslucencyModeRaw) { _, _ in // Keep all chrome/background surfaces in lockstep when mode changes. applyWindowTranslucency(enableTranslucentWindow) highlightRefreshToken &+= 1 } .onChange(of: colorScheme) { _, _ in applyWindowTranslucency(enableTranslucentWindow) highlightRefreshToken &+= 1 } #endif .toolbar { editorToolbarContent } .overlay(alignment: Alignment.topTrailing) { if droppedFileLoadInProgress { HStack(spacing: 8) { if droppedFileProgressDeterminate { ProgressView(value: droppedFileLoadProgress) .progressViewStyle(.linear) .frame(width: 120) } else { ProgressView() .frame(width: 16) } Text(droppedFileProgressDeterminate ? "\(droppedFileLoadLabel) \(importProgressPercentText)" : "\(droppedFileLoadLabel) Loading…") .font(.system(size: 11, weight: .medium)) .lineLimit(1) } .padding(.horizontal, 10) .padding(.vertical, 7) .background(.ultraThinMaterial, in: Capsule(style: .continuous)) .padding(.top, brainDumpLayoutEnabled ? 12 : 50) .padding(.trailing, 12) } } #if os(iOS) .overlay(alignment: .bottomTrailing) { if !brainDumpLayoutEnabled { floatingStatusPill .padding(.trailing, 12) .padding(.bottom, 12) } } #endif .onChange(of: currentLanguage) { _, newLanguage in if newLanguage != "markdown", showMarkdownPreviewPane { showMarkdownPreviewPane = false } } #if os(macOS) .toolbarBackground( macToolbarBackgroundStyle, for: ToolbarPlacement.windowToolbar ) .toolbarBackgroundVisibility(Visibility.visible, for: ToolbarPlacement.windowToolbar) .tint(NeonUIStyle.accentBlue) #else .toolbarBackground( enableTranslucentWindow ? AnyShapeStyle(.ultraThinMaterial) : AnyShapeStyle(Color(.systemBackground)), for: ToolbarPlacement.navigationBar ) #endif } private var largeFileLoadingPlaceholder: some View { VStack(spacing: 14) { ProgressView() .controlSize(.large) Text("Preparing large file") .font(.headline) Text("Deferred open mode keeps first paint lightweight and installs the document in chunks.") .font(.subheadline) .foregroundStyle(.secondary) .multilineTextAlignment(.center) } .frame(maxWidth: .infinity, maxHeight: .infinity) .padding(24) .accessibilityElement(children: .combine) .accessibilityLabel("Preparing large file") .accessibilityValue("Deferred open mode is loading the editor content") } #if os(macOS) || os(iOS) @ViewBuilder private var markdownPreviewPane: some View { VStack(alignment: .leading, spacing: 0) { markdownPreviewHeader .padding(.horizontal, 12) .padding(.vertical, 10) .background(editorSurfaceBackgroundStyle) MarkdownPreviewWebView( html: markdownPreviewHTML( from: currentContent, preferDarkMode: markdownPreviewPreferDarkMode ) ) .frame(maxWidth: .infinity, maxHeight: .infinity) .accessibilityLabel("Markdown Preview Content") } .frame(maxWidth: .infinity, maxHeight: .infinity) .background(editorSurfaceBackgroundStyle) #if canImport(UIKit) .fileExporter( isPresented: $showMarkdownPDFExporter, document: markdownPDFExportDocument, contentType: .pdf, defaultFilename: markdownPDFExportFilename ) { result in if case .failure(let error) = result { markdownPDFExportErrorMessage = error.localizedDescription } } #endif } #endif @ViewBuilder private var markdownPreviewHeader: some View { #if os(iOS) if UIDevice.current.userInterfaceIdiom == .phone { VStack(spacing: 16) { VStack(spacing: 10) { markdownPreviewCombinedPickerCard markdownPreviewPrimaryActionRow .padding(.top, 4) } .padding(16) .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 18, style: .continuous)) } .frame(maxWidth: .infinity) } else if UIDevice.current.userInterfaceIdiom == .pad { markdownPreviewIPadHeader } else { markdownPreviewRegularHeader } #else markdownPreviewRegularHeader #endif } private var markdownPreviewRegularHeader: some View { VStack(spacing: 16) { Text("Markdown Preview") .font(.headline) VStack(spacing: 10) { markdownPreviewCombinedPickerCard markdownPreviewPrimaryActionRow .padding(.top, 2) markdownPreviewSecondaryActionRow .padding(.top, 2) } #if os(iOS) .frame(minWidth: 320, maxWidth: 420) #else .frame(minWidth: 520, idealWidth: 640, maxWidth: 760) #endif .padding(16) .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 18, style: .continuous)) } .frame(maxWidth: .infinity, alignment: .center) } private var markdownPreviewIPadHeader: some View { VStack(spacing: 16) { Text("Markdown Preview") .font(.headline) .frame(maxWidth: .infinity, alignment: .center) VStack(spacing: 10) { markdownPreviewCombinedPickerCard markdownPreviewPrimaryActionRow .padding(.top, 2) markdownPreviewSecondaryActionRow .padding(.top, 2) } .frame(maxWidth: 460) .padding(16) .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 18, style: .continuous)) } .frame(maxWidth: .infinity, alignment: .center) } private var markdownPreviewTemplatePicker: some View { Picker("Template", selection: $markdownPreviewTemplateRaw) { Text("Default").tag("default") Text("Docs").tag("docs") Text("Article").tag("article") Text("Compact").tag("compact") Text("GitHub Docs").tag("github-docs") Text("Academic Paper").tag("academic-paper") Text("Terminal Notes").tag("terminal-notes") Text("Magazine").tag("magazine") Text("Minimal Reader").tag("minimal-reader") Text("Presentation").tag("presentation") Text("Night Contrast").tag("night-contrast") Text("Warm Sepia").tag("warm-sepia") Text("Dense Compact").tag("dense-compact") Text("Developer Spec").tag("developer-spec") } .labelsHidden() .pickerStyle(.menu) #if os(iOS) .frame(maxWidth: .infinity, alignment: .center) #else .frame(minWidth: 120, idealWidth: 190, maxWidth: 220) #endif } private var markdownPreviewPDFModePicker: some View { Picker("PDF Mode", selection: $markdownPDFExportModeRaw) { Text("Paginated Fit").tag(MarkdownPDFExportMode.paginatedFit.rawValue) Text("One Page Fit").tag(MarkdownPDFExportMode.onePageFit.rawValue) } .labelsHidden() .pickerStyle(.menu) #if os(iOS) .frame(maxWidth: .infinity, alignment: .center) #else .frame(minWidth: 128, idealWidth: 160, maxWidth: 180) #endif } private var markdownPreviewExportButton: some View { Button { exportMarkdownPreviewPDF() } label: { Label("Export PDF", systemImage: "doc.badge.arrow.down") .lineLimit(1) .fixedSize(horizontal: true, vertical: false) } .buttonStyle(.borderedProminent) .tint(NeonUIStyle.accentBlue) .controlSize(.regular) .layoutPriority(1) .accessibilityLabel("Export Markdown preview as PDF") } private var markdownPreviewShareButton: some View { ShareLink( item: markdownPreviewShareHTML, preview: SharePreview("\(suggestedMarkdownPreviewBaseName()).html") ) { Label("Share", systemImage: "square.and.arrow.up") .lineLimit(1) .fixedSize(horizontal: true, vertical: false) } .buttonStyle(.bordered) .controlSize(.regular) .layoutPriority(1) .accessibilityLabel("Share Markdown preview HTML") } private var markdownPreviewCopyHTMLButton: some View { Button { copyMarkdownPreviewHTML() } label: { Label("Copy HTML", systemImage: "doc.on.doc") .lineLimit(1) .fixedSize(horizontal: true, vertical: false) } .buttonStyle(.bordered) .controlSize(.regular) .layoutPriority(1) .accessibilityLabel("Copy Markdown preview HTML") } private var markdownPreviewCopyMarkdownButton: some View { Button { copyMarkdownPreviewMarkdown() } label: { Label("Copy Markdown", systemImage: "doc.on.clipboard") .lineLimit(1) .fixedSize(horizontal: true, vertical: false) } .buttonStyle(.bordered) .controlSize(.regular) .layoutPriority(1) .accessibilityLabel("Copy Markdown source") } @ViewBuilder private var markdownPreviewCombinedPickerCard: some View { HStack(alignment: .top, spacing: markdownPreviewPickerCardSpacing) { markdownPreviewPickerColumn("Template") { markdownPreviewTemplatePicker } markdownPreviewPickerColumn("PDF Mode") { markdownPreviewPDFModePicker } } .padding(.horizontal, markdownPreviewPickerCardHorizontalPadding) .padding(.vertical, 16) #if os(iOS) .frame(maxWidth: markdownPreviewPickerCardMaxWidth, alignment: .center) #else .frame(minWidth: 460, maxWidth: 560, alignment: .center) #endif .background(.regularMaterial, in: RoundedRectangle(cornerRadius: 16, style: .continuous)) .overlay { RoundedRectangle(cornerRadius: 16, style: .continuous) .strokeBorder(Color.white.opacity(0.08), lineWidth: 1) } } #if os(iOS) private var markdownPreviewPickerCardSpacing: CGFloat { UIDevice.current.userInterfaceIdiom == .pad ? 12 : 18 } private var markdownPreviewPickerCardHorizontalPadding: CGFloat { UIDevice.current.userInterfaceIdiom == .pad ? 12 : 18 } private var markdownPreviewPickerCardMaxWidth: CGFloat? { UIDevice.current.userInterfaceIdiom == .phone ? nil : 420 } #else private var markdownPreviewPickerCardSpacing: CGFloat { 18 } private var markdownPreviewPickerCardHorizontalPadding: CGFloat { 18 } #endif @ViewBuilder private func markdownPreviewPickerColumn(_ title: String, @ViewBuilder content: () -> Content) -> some View { VStack(spacing: 10) { Text(title) .font(.system(size: 13, weight: .semibold)) .foregroundStyle(.secondary) .frame(maxWidth: .infinity, alignment: .center) content() .frame(maxWidth: .infinity, alignment: .center) } .frame(maxWidth: .infinity, alignment: .center) } @ViewBuilder private func markdownPreviewActionRow(@ViewBuilder content: () -> Content) -> some View { HStack(spacing: 14) { content() } .frame(maxWidth: .infinity, alignment: .center) } private var markdownPreviewPrimaryActionRow: some View { markdownPreviewActionRow { markdownPreviewExportButton } } @ViewBuilder private var markdownPreviewSecondaryActionRow: some View { #if os(iOS) if UIDevice.current.userInterfaceIdiom == .phone { EmptyView() } else { markdownPreviewActionRow { markdownPreviewSecondaryButtons } } #else markdownPreviewActionRow { markdownPreviewSecondaryButtons } #endif } #if os(macOS) @ViewBuilder private var markdownPreviewSecondaryButtons: some View { HStack(spacing: 20) { markdownPreviewShareButton .frame(maxWidth: .infinity, alignment: .trailing) markdownPreviewCopyHTMLButton .frame(maxWidth: .infinity, alignment: .center) markdownPreviewCopyMarkdownButton .frame(maxWidth: .infinity, alignment: .leading) } .frame(minWidth: 520, idealWidth: 620, maxWidth: 680) } #else @ViewBuilder private var markdownPreviewSecondaryButtons: some View { if UIDevice.current.userInterfaceIdiom == .pad { ViewThatFits(in: .horizontal) { HStack(spacing: 10) { markdownPreviewShareButton markdownPreviewCopyHTMLButton } VStack(spacing: 10) { markdownPreviewShareButton markdownPreviewCopyHTMLButton } } } else { HStack(spacing: 10) { markdownPreviewShareButton } } } #endif #if os(iOS) @ViewBuilder private var iPhoneUnifiedTopChromeHost: some View { VStack(spacing: 0) { iPhoneUnifiedToolbarRow .padding(.horizontal, 8) .padding(.vertical, 6) tabBarView } .background( enableTranslucentWindow ? AnyShapeStyle(.ultraThinMaterial) : AnyShapeStyle(iOSNonTranslucentSurfaceColor) ) } private var floatingStatusPillText: String { let base = effectiveLargeFileModeEnabled ? "\(caretStatus) • Lines: \(statusLineCount)\(vimStatusSuffix)" : "\(caretStatus) • Lines: \(statusLineCount) • Words: \(statusWordCount)\(vimStatusSuffix)" let suffixes = [largeFileStatusBadgeText, remoteSessionStatusBadgeText].filter { !$0.isEmpty } if suffixes.isEmpty { return base } return "\(base) • \(suffixes.joined(separator: " • "))" } private var floatingStatusPill: some View { GlassSurface( enabled: shouldUseLiquidGlass, material: primaryGlassMaterial, fallbackColor: toolbarFallbackColor, shape: .capsule, chromeStyle: .single ) { Text(floatingStatusPillText) .font(.system(size: 12, weight: .medium, design: .monospaced)) .lineLimit(1) .minimumScaleFactor(0.85) .foregroundStyle(iOSToolbarForegroundColor) .padding(.horizontal, 12) .padding(.vertical, 8) } .accessibilityLabel("Editor status") .accessibilityValue(floatingStatusPillText) } private var iOSToolbarForegroundColor: Color { if toolbarIconsBlueIOS { return NeonUIStyle.accentBlue } return colorScheme == .dark ? Color.white.opacity(0.95) : Color.primary.opacity(0.92) } #endif // Status line: caret location + live word count from the view model. @ViewBuilder var wordCountView: some View { HStack(spacing: 10) { if droppedFileLoadInProgress { HStack(spacing: 8) { if droppedFileProgressDeterminate { ProgressView(value: droppedFileLoadProgress) .progressViewStyle(.linear) .frame(width: 130) } else { ProgressView() .frame(width: 18) } Text(droppedFileProgressDeterminate ? "\(droppedFileLoadLabel) \(importProgressPercentText)" : "\(droppedFileLoadLabel) Loading…") .font(.system(size: 11)) .foregroundColor(.secondary) .lineLimit(1) } .padding(.leading, 12) } if effectiveLargeFileModeEnabled { largeFileStatusBadge Picker("Large file open mode", selection: $largeFileOpenModeRaw) { Text("Standard").tag("standard") Text("Deferred").tag("deferred") Text("Plain Text").tag("plainText") } .pickerStyle(.segmented) .labelsHidden() .frame(width: 280) .fixedSize(horizontal: false, vertical: true) .controlSize(.small) .accessibilityLabel("Large file open mode") .accessibilityHint("Choose how large files are opened and rendered") } if !remoteSessionStatusBadgeText.isEmpty { remoteSessionBadge } Spacer() Text(effectiveLargeFileModeEnabled ? "\(caretStatus) • Lines: \(statusLineCount)\(vimStatusSuffix)" : "\(caretStatus) • Lines: \(statusLineCount) • Words: \(statusWordCount)\(vimStatusSuffix)") .font(.system(size: 12)) .foregroundColor(.secondary) .padding(.bottom, 8) .padding(.trailing, 16) } .background(editorSurfaceBackgroundStyle) } private var largeFileStatusBadge: some View { Text(largeFileStatusBadgeText) .font(.system(size: 11, weight: .semibold)) .foregroundColor(.secondary) .padding(.horizontal, 8) .padding(.vertical, 3) .background( Capsule(style: .continuous) .fill(Color.secondary.opacity(0.16)) ) .accessibilityLabel("Large file mode") .accessibilityValue(currentLargeFileOpenModeLabel) } private var remoteSessionBadge: some View { Text(remoteSessionStatusBadgeText) .font(.system(size: 11, weight: .semibold)) .foregroundColor(.secondary) .padding(.horizontal, 8) .padding(.vertical, 3) .background( Capsule(style: .continuous) .fill(Color.secondary.opacity(0.16)) ) .accessibilityLabel("Remote session status") .accessibilityValue( remoteSessionStore.isRemotePreviewConnecting ? "Local workspace with a remote session connection in progress" : ( remoteSessionStore.isRemotePreviewConnected ? "Local workspace with an active remote session connection" : ( remoteSessionStore.isRemotePreviewReady ? "Local workspace with a selected remote preview target" : ( remotePreparedTarget.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? "Local workspace with remote preview enabled" : "Local workspace with a prepared remote target" ) ) ) ) } private var largeFileSessionBadge: some View { Menu { largeFileOpenModeMenuContent } label: { HStack(spacing: 8) { Image(systemName: "bolt.horizontal.circle.fill") .font(.system(size: 12, weight: .semibold)) .foregroundStyle(NeonUIStyle.accentBlue) Text(largeFileStatusBadgeText) .font(.system(size: 11, weight: .semibold)) .foregroundStyle(.primary) .lineLimit(1) Image(systemName: "chevron.down") .font(.system(size: 10, weight: .semibold)) .foregroundStyle(.secondary) } .padding(.horizontal, 10) .padding(.vertical, 7) .background(.ultraThinMaterial, in: Capsule(style: .continuous)) } .menuStyle(.borderlessButton) .accessibilityLabel("Large file session") .accessibilityValue(currentLargeFileOpenModeLabel) .accessibilityHint("Open large file mode options") } @ViewBuilder private var largeFileOpenModeMenuContent: some View { Button { largeFileOpenModeRaw = "standard" } label: { largeFileOpenModeMenuLabel(title: "Standard", isSelected: largeFileOpenModeRaw == "standard") } Button { largeFileOpenModeRaw = "deferred" } label: { largeFileOpenModeMenuLabel(title: "Deferred", isSelected: largeFileOpenModeRaw == "deferred") } Button { largeFileOpenModeRaw = "plainText" } label: { largeFileOpenModeMenuLabel(title: "Plain Text", isSelected: largeFileOpenModeRaw == "plainText") } } private func largeFileOpenModeMenuLabel(title: String, isSelected: Bool) -> some View { HStack { Text(title) Spacer(minLength: 10) if isSelected { Image(systemName: "checkmark") } } } @ViewBuilder var tabBarView: some View { VStack(spacing: 0) { ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 6) { if viewModel.tabs.isEmpty { Button { viewModel.addNewTab() } label: { HStack(spacing: 6) { Text("Untitled 1") .lineLimit(1) .font(.system(size: 12, weight: .semibold)) Image(systemName: "plus") .font(.system(size: 10, weight: .bold)) .foregroundStyle(NeonUIStyle.accentBlue) } .padding(.horizontal, 10) .padding(.vertical, 6) .background( RoundedRectangle(cornerRadius: 8, style: .continuous) .fill(Color.accentColor.opacity(0.18)) ) } .buttonStyle(.plain) } else { ForEach(viewModel.tabs) { tab in HStack(spacing: 8) { Button { viewModel.selectTab(id: tab.id) } label: { HStack(spacing: 6) { Text(tab.name + (tab.isDirty ? " •" : "")) .lineLimit(1) .font(.system(size: 12, weight: viewModel.selectedTabID == tab.id ? .semibold : .regular)) if tab.isReadOnlyPreview { Image(systemName: "lock.fill") .font(.system(size: 9, weight: .semibold)) .foregroundStyle(.secondary) } } .padding(.leading, 10) .padding(.vertical, 6) } .buttonStyle(.plain) #if os(macOS) .simultaneousGesture( TapGesture(count: 2) .onEnded { requestCloseTab(tab) } ) #endif Button { requestCloseTab(tab) } label: { Image(systemName: "xmark") .font(.system(size: 10, weight: .bold)) .padding(.trailing, 10) } .buttonStyle(.plain) .contentShape(Rectangle()) .help("Close \(tab.name)") } .background( RoundedRectangle(cornerRadius: 8, style: .continuous) .fill(viewModel.selectedTabID == tab.id ? Color.accentColor.opacity(0.18) : Color.secondary.opacity(0.10)) ) } } } .padding(.leading, tabBarLeadingPadding) .padding(.trailing, 10) .padding(.vertical, 6) } #if os(iOS) iOSHorizontalSurfaceDivider.opacity(0.7) #else Divider().opacity(0.45) #endif } .frame(minHeight: 42, maxHeight: 42, alignment: .center) #if os(macOS) .background(editorSurfaceBackgroundStyle) #else .background( enableTranslucentWindow ? AnyShapeStyle(.ultraThinMaterial) : (useIOSUnifiedSolidSurfaces ? AnyShapeStyle(iOSNonTranslucentSurfaceColor) : AnyShapeStyle(Color(.systemBackground))) ) .contentShape(Rectangle()) .zIndex(10) #endif } private var vimStatusSuffix: String { #if os(macOS) guard vimModeEnabled else { return " • Vim: OFF" } return vimInsertMode ? " • Vim: INSERT" : " • Vim: NORMAL" #else guard UIDevice.current.userInterfaceIdiom == .pad else { return "" } guard vimModeEnabled else { return " • Vim: OFF" } return vimInsertMode ? " • Vim: INSERT" : " • Vim: NORMAL" #endif } private var importProgressPercentText: String { let clamped = min(max(droppedFileLoadProgress, 0), 1) if clamped > 0, clamped < 0.01 { return "1%" } return "\(Int(clamped * 100))%" } private var quickSwitcherItems: [QuickFileSwitcherPanel.Item] { _ = recentFilesRefreshToken var items: [QuickFileSwitcherPanel.Item] = [] let fileURLSet = Set(viewModel.tabs.compactMap { $0.fileURL?.standardizedFileURL.path }) let commandItems: [QuickFileSwitcherPanel.Item] = [ .init(id: "cmd:new_tab", title: "New Tab", subtitle: "Create a new empty tab", isPinned: false, canTogglePin: false), .init(id: "cmd:open_file", title: "Open File", subtitle: "Open files from disk", isPinned: false, canTogglePin: false), .init(id: "cmd:save_file", title: "Save", subtitle: "Save current tab", isPinned: false, canTogglePin: false), .init(id: "cmd:save_as", title: "Save As", subtitle: "Save current tab to a new file", isPinned: false, canTogglePin: false), .init(id: "cmd:find_replace", title: "Find and Replace", subtitle: "Search and replace in current document", isPinned: false, canTogglePin: false), .init(id: "cmd:find_in_files", title: "Find in Files", subtitle: "Search across project files", isPinned: false, canTogglePin: false), .init(id: "cmd:toggle_sidebar", title: "Toggle Sidebar", subtitle: "Show or hide the outline sidebar", isPinned: false, canTogglePin: false) ] items.append(contentsOf: commandItems) for tab in viewModel.tabs { let subtitle = tab.fileURL?.path ?? "Open tab" items.append( QuickFileSwitcherPanel.Item( id: "tab:\(tab.id.uuidString)", title: tab.name, subtitle: subtitle, isPinned: false, canTogglePin: false ) ) } for recent in RecentFilesStore.items(limit: 12) { let standardized = recent.url.standardizedFileURL.path if fileURLSet.contains(standardized) { continue } items.append( QuickFileSwitcherPanel.Item( id: "file:\(standardized)", title: recent.title, subtitle: recent.subtitle, isPinned: recent.isPinned, canTogglePin: true ) ) } if projectFileIndexSnapshot.entries.isEmpty { for url in quickSwitcherProjectFileURLs { let standardized = url.standardizedFileURL.path if fileURLSet.contains(standardized) { continue } if items.contains(where: { $0.id == "file:\(standardized)" }) { continue } items.append( QuickFileSwitcherPanel.Item( id: "file:\(standardized)", title: url.lastPathComponent, subtitle: standardized, isPinned: false, canTogglePin: true ) ) } } else { for entry in projectFileIndexSnapshot.entries { let standardized = entry.standardizedPath let subtitle = entry.relativePath == entry.displayName ? standardized : entry.relativePath if fileURLSet.contains(standardized) { continue } if items.contains(where: { $0.id == "file:\(standardized)" }) { continue } items.append( QuickFileSwitcherPanel.Item( id: "file:\(standardized)", title: entry.displayName, subtitle: subtitle, isPinned: false, canTogglePin: true ) ) } } let query = quickSwitcherQuery.trimmingCharacters(in: .whitespacesAndNewlines) if query.isEmpty { return Array( items .sorted { let leftPinned = $0.isPinned ? 1 : 0 let rightPinned = $1.isPinned ? 1 : 0 if leftPinned != rightPinned { return leftPinned > rightPinned } return quickSwitcherRecencyScore(for: $0.id) > quickSwitcherRecencyScore(for: $1.id) } .prefix(300) ) } let ranked = items.compactMap { item -> (QuickFileSwitcherPanel.Item, Int)? in guard let score = quickSwitcherMatchScore(for: item, query: query) else { return nil } let pinBoost = item.isPinned ? 400 : 0 return (item, score + quickSwitcherRecencyScore(for: item.id) + pinBoost) } .sorted { if $0.1 == $1.1 { return $0.0.title.localizedCaseInsensitiveCompare($1.0.title) == .orderedAscending } return $0.1 > $1.1 } return Array(ranked.prefix(300).map(\.0)) } private func selectQuickSwitcherItem(_ item: QuickFileSwitcherPanel.Item) { rememberQuickSwitcherSelection(item.id) if item.id.hasPrefix("cmd:") { performQuickSwitcherCommand(item.id) return } if item.id.hasPrefix("tab:") { let raw = String(item.id.dropFirst(4)) if let id = UUID(uuidString: raw) { viewModel.selectTab(id: id) } return } if item.id.hasPrefix("file:") { let path = String(item.id.dropFirst(5)) openProjectFile(url: URL(fileURLWithPath: path)) } } private func toggleQuickSwitcherPin(_ item: QuickFileSwitcherPanel.Item) { guard item.canTogglePin, item.id.hasPrefix("file:") else { return } let path = String(item.id.dropFirst(5)) RecentFilesStore.togglePinned(URL(fileURLWithPath: path)) recentFilesRefreshToken = UUID() } var canCreateCodeSnapshot: Bool { !normalizedCodeSnapshotSelection().isEmpty } func presentCodeSnapshotComposer() { let selection = normalizedCodeSnapshotSelection() guard !selection.isEmpty else { return } let title = viewModel.selectedTab?.name ?? "Code Snapshot" codeSnapshotPayload = CodeSnapshotPayload( title: title, language: currentLanguage, text: selection ) } private func normalizedCodeSnapshotSelection() -> String { currentSelectionSnapshotText.trimmingCharacters(in: .whitespacesAndNewlines) } private func performQuickSwitcherCommand(_ commandID: String) { switch commandID { case "cmd:new_tab": viewModel.addNewTab() case "cmd:open_file": openFileFromToolbar() case "cmd:save_file": saveCurrentTabFromToolbar() case "cmd:save_as": saveCurrentTabAsFromToolbar() case "cmd:find_replace": showFindReplace = true case "cmd:find_in_files": showFindInFiles = true case "cmd:toggle_sidebar": viewModel.showSidebar.toggle() default: break } } private func rememberQuickSwitcherSelection(_ itemID: String) { quickSwitcherRecentItemIDs.removeAll { $0 == itemID } quickSwitcherRecentItemIDs.insert(itemID, at: 0) if quickSwitcherRecentItemIDs.count > 30 { quickSwitcherRecentItemIDs = Array(quickSwitcherRecentItemIDs.prefix(30)) } UserDefaults.standard.set(quickSwitcherRecentItemIDs, forKey: quickSwitcherRecentsDefaultsKey) } private func quickSwitcherRecencyScore(for itemID: String) -> Int { guard let index = quickSwitcherRecentItemIDs.firstIndex(of: itemID) else { return 0 } return max(0, 120 - (index * 5)) } private func quickSwitcherMatchScore(for item: QuickFileSwitcherPanel.Item, query: String) -> Int? { let normalizedQuery = query.lowercased() let title = item.title.lowercased() let subtitle = item.subtitle.lowercased() if title.hasPrefix(normalizedQuery) { return 320 } if title.contains(normalizedQuery) { return 240 } if subtitle.contains(normalizedQuery) { return 180 } if isFuzzyMatch(needle: normalizedQuery, haystack: title) { return 120 } if isFuzzyMatch(needle: normalizedQuery, haystack: subtitle) { return 90 } return nil } private func isFuzzyMatch(needle: String, haystack: String) -> Bool { if needle.isEmpty { return true } var cursor = haystack.startIndex for ch in needle { var found = false while cursor < haystack.endIndex { if haystack[cursor] == ch { found = true cursor = haystack.index(after: cursor) break } cursor = haystack.index(after: cursor) } if !found { return false } } return true } private func startFindInFiles() { guard let root = projectRootFolderURL else { findInFilesResults = [] findInFilesStatusMessage = "Open a project folder first." return } let query = findInFilesQuery.trimmingCharacters(in: .whitespacesAndNewlines) guard !query.isEmpty else { findInFilesResults = [] findInFilesStatusMessage = "Enter a search query." return } findInFilesTask?.cancel() let indexedProjectFileURLs = projectFileIndexSnapshot.fileURLs let candidateFiles = indexedProjectFileURLs.isEmpty ? nil : indexedProjectFileURLs if candidateFiles == nil, isProjectFileIndexing { findInFilesStatusMessage = "Searching while project index updates…" } else { findInFilesStatusMessage = "Searching…" } let caseSensitive = findInFilesCaseSensitive findInFilesTask = Task { let results = await ContentView.findInFiles( root: root, candidateFiles: candidateFiles, query: query, caseSensitive: caseSensitive, maxResults: 500 ) guard !Task.isCancelled else { return } findInFilesResults = results if results.isEmpty { findInFilesStatusMessage = "No matches found." } else { findInFilesStatusMessage = "\(results.count) matches" } } } private func selectFindInFilesMatch(_ match: FindInFilesMatch) { openProjectFile(url: match.fileURL) var userInfo: [String: Any] = [ EditorCommandUserInfo.rangeLocation: match.rangeLocation, EditorCommandUserInfo.rangeLength: match.rangeLength ] #if os(macOS) if let hostWindowNumber { userInfo[EditorCommandUserInfo.windowNumber] = hostWindowNumber } #endif DispatchQueue.main.asyncAfter(deadline: .now() + 0.08) { NotificationCenter.default.post(name: .moveCursorToRange, object: nil, userInfo: userInfo) } } private func scheduleWordCountRefresh(for text: String) { let snapshot = text let shouldSkipWordCount = effectiveLargeFileModeEnabled || currentDocumentUTF16Length >= 300_000 wordCountTask?.cancel() wordCountTask = Task(priority: .utility) { try? await Task.sleep(nanoseconds: 80_000_000) guard !Task.isCancelled else { return } let lineCount = Self.lineCount(for: snapshot) let wordCount = shouldSkipWordCount ? 0 : viewModel.wordCount(for: snapshot) await MainActor.run { statusLineCount = lineCount statusWordCount = wordCount } } } }