2026-01-25 12:46:33 +00:00
// C o n t e n t V i e w . s w i f t
// M a i n S w i f t U I c o n t a i n e r f o r N e o n V i s i o n E d i t o r . H o s t s t h e s i n g l e - d o c u m e n t e d i t o r U I ,
// t o o l b a r a c t i o n s , A I i n t e g r a t i o n , s y n t a x h i g h l i g h t i n g , l i n e n u m b e r s , a n d s i d e b a r T O C .
2026-02-14 13:24:01 +00:00
// / MARK: - I m p o r t s
2025-08-25 07:39:12 +00:00
import SwiftUI
2026-01-25 12:46:33 +00:00
import Foundation
2026-02-25 13:07:05 +00:00
import Observation
2026-02-07 10:51:52 +00:00
import UniformTypeIdentifiers
2026-02-18 19:19:49 +00:00
import OSLog
2026-03-17 17:40:32 +00:00
import Dispatch
2026-02-07 10:51:52 +00:00
#if os ( macOS )
import AppKit
#elseif canImport ( UIKit )
import UIKit
#endif
2026-02-09 11:15:22 +00:00
#if USE_FOUNDATION_MODELS && canImport ( FoundationModels )
2026-01-17 12:04:11 +00:00
import FoundationModels
#endif
2025-08-25 07:39:12 +00:00
2026-02-22 13:04:27 +00:00
#if os ( macOS )
private final class WindowCloseConfirmationDelegate : NSObject , NSWindowDelegate {
2026-02-25 13:07:05 +00:00
nonisolated ( unsafe ) weak var forwardedDelegate : NSWindowDelegate ?
2026-02-22 13:04:27 +00:00
var shouldConfirm : ( ( ) -> Bool ) ?
var hasDirtyTabs : ( ( ) -> Bool ) ?
var saveAllDirtyTabs : ( ( ) -> Bool ) ?
var dialogTitle : ( ( ) -> String ) ?
var dialogMessage : ( ( ) -> String ) ?
private var isPromptInFlight = false
private var allowNextClose = false
2026-02-25 13:07:05 +00:00
nonisolated override func responds ( to selector : Selector ! ) -> Bool {
2026-02-22 13:04:27 +00:00
super . responds ( to : selector ) || ( forwardedDelegate ? . responds ( to : selector ) ? ? false )
}
2026-02-25 13:07:05 +00:00
nonisolated override func forwardingTarget ( for selector : Selector ! ) -> Any ? {
2026-02-22 13:04:27 +00:00
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
2026-01-17 11:11:26 +00:00
2026-01-25 12:46:33 +00:00
// U t i l i t y : q u i c k w i d t h c a l c u l a t i o n f o r s t r i n g s w i t h a g i v e n f o n t ( A p p K i t - b a s e d )
2025-09-25 09:01:45 +00:00
extension String {
2026-02-07 10:51:52 +00:00
#if os ( macOS )
2025-09-25 09:01:45 +00:00
func width ( usingFont font : NSFont ) -> CGFloat {
let attributes = [ NSAttributedString . Key . font : font ]
let size = ( self as NSString ) . size ( withAttributes : attributes )
return size . width
}
2026-02-07 10:51:52 +00:00
#endif
2025-09-25 09:01:45 +00:00
}
2025-08-27 11:34:59 +00:00
2026-02-14 13:24:01 +00:00
// / MARK: - R o o t V i e w
2026-01-25 13:29:46 +00:00
// M a n a g e s t h e e d i t o r a r e a , t o o l b a r , p o p o v e r s , a n d b r i d g e s t o t h e v i e w m o d e l f o r f i l e I / O a n d m e t r i c s .
2025-09-25 09:01:45 +00:00
struct ContentView : View {
2026-02-28 18:26:00 +00:00
enum StartupBehavior {
case standard
case forceBlankDocument
2026-03-17 17:40:32 +00:00
case safeMode
2026-02-28 18:26:00 +00:00
}
2026-03-09 12:54:26 +00:00
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 }
}
2026-03-09 13:10:42 +00:00
enum DelimitedViewMode : String , CaseIterable , Identifiable {
case table
case text
var id : String { rawValue }
}
2026-04-16 10:37:03 +00:00
enum ProjectSidebarCreationKind : String {
case file
case folder
var title : String {
switch self {
case . file :
return NSLocalizedString ( " New File " , comment : " Project sidebar creation title for files " )
case . folder :
return NSLocalizedString ( " New Folder " , comment : " Project sidebar creation title for folders " )
}
}
var namePlaceholder : String {
switch self {
case . file :
return NSLocalizedString ( " File name " , comment : " Project sidebar file name placeholder " )
case . folder :
return NSLocalizedString ( " Folder name " , comment : " Project sidebar folder name placeholder " )
}
}
}
2026-03-09 13:10:42 +00:00
struct DelimitedTableSnapshot : Sendable {
let header : [ String ]
let rows : [ [ String ] ]
let totalRows : Int
let displayedRows : Int
let truncated : Bool
}
2026-03-09 13:13:52 +00:00
struct DelimitedTableParseError : LocalizedError {
let message : String
var errorDescription : String ? { message }
}
2026-02-28 18:26:00 +00:00
let startupBehavior : StartupBehavior
2026-03-17 17:40:32 +00:00
let safeModeMessage : String ?
2026-02-28 18:26:00 +00:00
2026-03-17 17:40:32 +00:00
init ( startupBehavior : StartupBehavior = . standard , safeModeMessage : String ? = nil ) {
2026-02-28 18:26:00 +00:00
self . startupBehavior = startupBehavior
2026-03-17 17:40:32 +00:00
self . safeModeMessage = safeModeMessage
2026-02-28 18:26:00 +00:00
}
2026-02-20 17:04:36 +00:00
private enum EditorPerformanceThresholds {
static let largeFileBytes = 12_000_000
2026-02-20 19:32:03 +00:00
static let largeFileBytesHTMLCSV = 4_000_000
2026-03-09 12:54:26 +00:00
static let largeFileBytesMobile = 8_000_000
static let largeFileBytesHTMLCSVMobile = 3_000_000
2026-02-20 17:04:36 +00:00
static let heavyFeatureUTF16Length = 450_000
2026-02-20 19:32:03 +00:00
static let largeFileLineBreaks = 40_000
static let largeFileLineBreaksHTMLCSV = 15_000
2026-03-09 12:54:26 +00:00
static let largeFileLineBreaksMobile = 25_000
static let largeFileLineBreaksHTMLCSVMobile = 10_000
2026-02-20 17:04:36 +00:00
}
2026-02-18 19:19:49 +00:00
private static let completionSignposter = OSSignposter ( subsystem : " h3p.Neon-Vision-Editor " , category : " InlineCompletion " )
private struct CompletionCacheEntry {
let suggestion : String
let createdAt : Date
}
2026-03-03 09:47:45 +00:00
private struct SavedDraftTabSnapshot : Codable {
2026-02-22 13:31:05 +00:00
let name : String
let content : String
let language : String
let fileURLString : String ?
}
2026-03-03 09:47:45 +00:00
private struct SavedDraftSnapshot : Codable {
let tabs : [ SavedDraftTabSnapshot ]
2026-02-22 13:31:05 +00:00
let selectedIndex : Int ?
2026-03-03 09:47:45 +00:00
let createdAt : Date
2026-02-22 13:31:05 +00:00
}
2026-01-25 12:46:33 +00:00
// E n v i r o n m e n t - p r o v i d e d v i e w m o d e l a n d t h e m e / e r r o r b i n d i n g s
2026-02-25 13:07:05 +00:00
@ Environment ( EditorViewModel . self ) var viewModel
2026-02-11 10:20:17 +00:00
@ EnvironmentObject private var supportPurchaseManager : SupportPurchaseManager
2026-02-14 13:24:01 +00:00
@ EnvironmentObject var appUpdateManager : AppUpdateManager
2026-02-06 18:59:53 +00:00
@ Environment ( \ . colorScheme ) var colorScheme
2026-02-07 10:51:52 +00:00
#if os ( iOS )
@ Environment ( \ . horizontalSizeClass ) var horizontalSizeClass
#endif
#if os ( macOS )
2026-02-06 18:59:53 +00:00
@ Environment ( \ . openWindow ) var openWindow
2026-02-13 00:14:15 +00:00
@ Environment ( \ . openSettings ) var openSettingsAction
2026-02-07 10:51:52 +00:00
#endif
2026-02-06 18:59:53 +00:00
@ Environment ( \ . showGrokError ) var showGrokError
@ Environment ( \ . grokErrorMessage ) var grokErrorMessage
2026-01-17 11:11:26 +00:00
2026-01-25 12:46:33 +00:00
// S i n g l e - d o c u m e n t f a l l b a c k s t a t e ( u s e d w h e n n o t a b m o d e l i s s e l e c t e d )
2026-02-12 22:20:39 +00:00
@ AppStorage ( " SelectedAIModel " ) private var selectedModelRaw : String = AIModel . appleIntelligence . rawValue
2026-02-06 18:59:53 +00:00
@ State var singleContent : String = " "
2026-02-09 10:21:50 +00:00
@ State var singleLanguage : String = " plain "
2026-02-06 18:59:53 +00:00
@ State var caretStatus : String = " Ln 1, Col 1 "
2026-02-11 10:20:17 +00:00
@ 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
2026-02-12 22:20:39 +00:00
@ AppStorage ( " SettingsHighlightMatchingBrackets " ) var highlightMatchingBrackets : Bool = false
@ AppStorage ( " SettingsShowScopeGuides " ) var showScopeGuides : Bool = false
@ AppStorage ( " SettingsHighlightScopeBackground " ) var highlightScopeBackground : Bool = false
2026-02-11 10:20:17 +00:00
@ AppStorage ( " SettingsLineWrapEnabled " ) var settingsLineWrapEnabled : Bool = false
// R e m o v e d s h o w H o r i z o n t a l R u l e r a n d s h o w V e r t i c a l R u l e r A p p S t o r a g e p r o p e r t i e s
@ 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
2026-02-12 22:20:39 +00:00
@ AppStorage ( " SettingsReopenLastSession " ) var reopenLastSession : Bool = true
@ AppStorage ( " SettingsOpenWithBlankDocument " ) var openWithBlankDocument : Bool = true
@ AppStorage ( " SettingsConfirmCloseDirtyTab " ) var confirmCloseDirtyTab : Bool = true
@ AppStorage ( " SettingsConfirmClearEditor " ) var confirmClearEditor : Bool = true
2026-02-11 10:20:17 +00:00
@ AppStorage ( " SettingsActiveTab " ) var settingsActiveTab : String = " general "
2026-03-17 17:40:32 +00:00
@ AppStorage ( " SettingsAppearance " ) var appearance : String = " system "
2026-02-11 10:20:17 +00:00
@ AppStorage ( " SettingsTemplateLanguage " ) private var settingsTemplateLanguage : String = " swift "
@ AppStorage ( " SettingsThemeName " ) private var settingsThemeName : String = " Neon Glow "
2026-03-17 17:40:32 +00:00
@ 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
2026-02-06 18:59:53 +00:00
@ State var lastProviderUsed : String = " Apple "
2026-02-11 10:20:17 +00:00
@ State private var highlightRefreshToken : Int = 0
2026-02-28 18:26:00 +00:00
@ State var editorExternalMutationRevision : Int = 0
2026-01-17 11:11:26 +00:00
2026-01-25 12:46:33 +00:00
// P e r s i s t e d A P I t o k e n s f o r e x t e r n a l p r o v i d e r s
2026-02-14 13:24:01 +00:00
@ State var grokAPIToken : String = " "
@ State var openAIAPIToken : String = " "
@ State var geminiAPIToken : String = " "
@ State var anthropicAPIToken : String = " "
2026-01-25 12:46:33 +00:00
2026-02-19 08:09:35 +00:00
// D e b o u n c e / c a n c e l l a t i o n h a n d l e s f o r i n l i n e c o m p l e t i o n
@ State private var completionDebounceTask : Task < Void , Never > ?
2026-02-18 18:59:25 +00:00
@ State private var completionTask : Task < Void , Never > ?
2026-02-19 08:09:35 +00:00
@ State private var lastCompletionTriggerSignature : String = " "
2026-02-09 10:21:50 +00:00
@ State private var isApplyingCompletion : Bool = false
2026-02-18 19:19:49 +00:00
@ State private var completionCache : [ String : CompletionCacheEntry ] = [ : ]
@ State private var pendingHighlightRefresh : DispatchWorkItem ?
2026-02-20 00:31:14 +00:00
#if os ( iOS )
@ AppStorage ( " EnableTranslucentWindow " ) var enableTranslucentWindow : Bool = true
#else
2026-02-14 20:57:32 +00:00
@ AppStorage ( " EnableTranslucentWindow " ) var enableTranslucentWindow : Bool = false
2026-02-20 00:31:14 +00:00
#endif
2026-02-20 02:41:08 +00:00
#if os ( iOS )
@ State private var previousKeyboardAccessoryVisibility : Bool ? = nil
2026-03-26 18:19:45 +00:00
@ State var markdownPreviewSheetDetent : PresentationDetent = . medium
2026-02-20 02:41:08 +00:00
#endif
2026-02-19 14:29:53 +00:00
#if os ( macOS )
@ AppStorage ( " SettingsMacTranslucencyMode " ) private var macTranslucencyModeRaw : String = " balanced "
#endif
2026-01-20 23:46:04 +00:00
2026-02-06 18:59:53 +00:00
@ State var showFindReplace : Bool = false
2026-02-11 10:20:17 +00:00
@ State var showSettingsSheet : Bool = false
2026-02-14 13:24:01 +00:00
@ State var showUpdateDialog : Bool = false
2026-02-06 18:59:53 +00:00
@ State var findQuery : String = " "
@ State var replaceQuery : String = " "
@ State var findUsesRegex : Bool = false
@ State var findCaseSensitive : Bool = false
@ State var findStatusMessage : String = " "
2026-03-30 17:07:02 +00:00
@ State var findMatchCount : Int = 0
2026-02-12 22:20:39 +00:00
@ State var iOSFindCursorLocation : Int = 0
@ State var iOSLastFindFingerprint : String = " "
2026-02-06 18:59:53 +00:00
@ State var showProjectStructureSidebar : Bool = false
2026-02-07 10:51:52 +00:00
@ State var showCompactSidebarSheet : Bool = false
2026-02-20 01:16:58 +00:00
@ State var showCompactProjectSidebarSheet : Bool = false
2026-02-06 18:59:53 +00:00
@ State var projectRootFolderURL : URL ? = nil
@ State var projectTreeNodes : [ ProjectTreeNode ] = [ ]
2026-02-19 09:12:09 +00:00
@ State var projectTreeRefreshGeneration : Int = 0
2026-04-16 10:37:03 +00:00
@ State var projectTreeRevealURL : URL ? = nil
2026-03-08 14:31:01 +00:00
@ AppStorage ( " SettingsShowSupportedProjectFilesOnly " ) var showSupportedProjectFilesOnly : Bool = true
2026-04-16 10:37:03 +00:00
@ AppStorage ( " SettingsShowInvisibleCharacters " ) var showInvisibleCharacters : Bool = false
2026-03-03 09:47:45 +00:00
@ State var projectOverrideIndentWidth : Int ? = nil
@ State var projectOverrideLineWrapEnabled : Bool ? = nil
2026-02-09 19:49:55 +00:00
@ State var showProjectFolderPicker : Bool = false
@ State var projectFolderSecurityURL : URL ? = nil
2026-02-06 19:20:03 +00:00
@ State var pendingCloseTabID : UUID ? = nil
@ State var showUnsavedCloseDialog : Bool = false
2026-03-08 14:31:01 +00:00
@ State var showCloseAllTabsDialog : Bool = false
2026-03-08 12:49:48 +00:00
@ State private var showExternalConflictDialog : Bool = false
2026-03-30 17:07:02 +00:00
@ State private var showRemoteSaveIssueDialog : Bool = false
2026-03-08 12:49:48 +00:00
@ State private var showExternalConflictCompareSheet : Bool = false
@ State private var externalConflictCompareSnapshot : EditorViewModel . ExternalFileComparisonSnapshot ?
2026-03-30 17:07:02 +00:00
@ State private var showRemoteConflictCompareSheet : Bool = false
@ State private var remoteConflictCompareSnapshot : EditorViewModel . RemoteConflictComparisonSnapshot ?
2026-02-12 22:20:39 +00:00
@ State var showClearEditorConfirmDialog : Bool = false
2026-02-07 10:51:52 +00:00
@ State var showIOSFileImporter : Bool = false
@ State var showIOSFileExporter : Bool = false
2026-03-08 14:31:01 +00:00
@ State var showUnsupportedFileAlert : Bool = false
@ State var unsupportedFileName : String = " "
2026-04-16 10:37:03 +00:00
@ State var showProjectItemCreationPrompt : Bool = false
@ State var projectItemCreationNameDraft : String = " "
@ State var projectItemCreationKind : ProjectSidebarCreationKind = . file
@ State var projectItemCreationParentURL : URL ? = nil
@ State var showProjectItemRenamePrompt : Bool = false
@ State var projectItemRenameNameDraft : String = " "
@ State var projectItemRenameSourceURL : URL ? = nil
@ State var showProjectItemDeleteConfirmation : Bool = false
@ State var projectItemDeleteTargetURL : URL ? = nil
@ State var projectItemDeleteTargetName : String = " "
@ State var showProjectItemOperationErrorAlert : Bool = false
@ State var projectItemOperationErrorMessage : String = " "
2026-02-07 10:51:52 +00:00
@ State var iosExportDocument : PlainTextDocument = PlainTextDocument ( text : " " )
@ State var iosExportFilename : String = " Untitled.txt "
@ State var iosExportTabID : UUID ? = nil
2026-03-17 17:40:32 +00:00
@ State var showMarkdownPDFExporter : Bool = false
@ State var markdownPDFExportDocument : PDFExportDocument = PDFExportDocument ( )
@ State var markdownPDFExportFilename : String = " Markdown-Preview.pdf "
@ State var markdownPDFExportErrorMessage : String ?
2026-03-30 17:07:02 +00:00
@ State var markdownPreviewActionStatusMessage : String = " "
@ State var markdownPreviewActionStatusToken : UUID = UUID ( )
2026-02-08 00:06:06 +00:00
@ State var showQuickSwitcher : Bool = false
@ State var quickSwitcherQuery : String = " "
2026-02-19 14:29:53 +00:00
@ State var quickSwitcherProjectFileURLs : [ URL ] = [ ]
2026-03-26 18:19:45 +00:00
@ State var projectFileIndexSnapshot : ProjectFileIndex . Snapshot = . empty
2026-03-17 17:40:32 +00:00
@ State var isProjectFileIndexing : Bool = false
@ State var projectFileIndexRefreshGeneration : Int = 0
@ State var projectFileIndexTask : Task < Void , Never > ? = nil
@ State var projectFolderMonitorSource : DispatchSourceFileSystemObject ? = nil
@ State var pendingProjectFolderRefreshWorkItem : DispatchWorkItem ? = nil
2026-03-03 09:47:45 +00:00
@ State private var quickSwitcherRecentItemIDs : [ String ] = [ ]
2026-03-17 17:40:32 +00:00
@ State var recentFilesRefreshToken : UUID = UUID ( )
2026-03-15 16:37:20 +00:00
@ State private var currentSelectionSnapshotText : String = " "
@ State private var codeSnapshotPayload : CodeSnapshotPayload ?
2026-02-25 13:07:05 +00:00
@ State var showFindInFiles : Bool = false
@ State var findInFilesQuery : String = " "
@ State var findInFilesCaseSensitive : Bool = false
@ State var findInFilesResults : [ FindInFilesMatch ] = [ ]
@ State var findInFilesStatusMessage : String = " "
2026-03-30 17:07:02 +00:00
@ State var findInFilesSourceMessage : String = " "
2026-02-25 13:07:05 +00:00
@ State private var findInFilesTask : Task < Void , Never > ?
2026-02-19 14:29:53 +00:00
@ State private var statusWordCount : Int = 0
2026-03-13 14:13:44 +00:00
@ State private var statusLineCount : Int = 1
2026-02-19 14:29:53 +00:00
@ State private var wordCountTask : Task < Void , Never > ?
2026-03-26 18:19:45 +00:00
@ AppStorage ( " EditorVimModeEnabled " ) var vimModeEnabled : Bool = false
2026-02-08 00:06:06 +00:00
@ State var vimInsertMode : Bool = true
2026-03-17 17:40:32 +00:00
@ State var safeModeRecoveryPreparedForNextLaunch : Bool = false
2026-02-08 11:14:49 +00:00
@ State var droppedFileLoadInProgress : Bool = false
@ State var droppedFileProgressDeterminate : Bool = true
@ State var droppedFileLoadProgress : Double = 0
@ State var droppedFileLoadLabel : String = " "
@ State var largeFileModeEnabled : Bool = false
2026-04-16 10:37:03 +00:00
@ SceneStorage ( " ProjectSidebarWidth " ) private var projectSidebarWidth : Double = 320
2026-03-09 13:10:42 +00:00
@ 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 < Void , Never > ? = nil
2026-03-09 12:54:26 +00:00
@ AppStorage ( " SettingsProjectNavigatorPlacement " ) var projectNavigatorPlacementRaw : String = ProjectNavigatorPlacement . trailing . rawValue
@ AppStorage ( " SettingsPerformancePreset " ) var performancePresetRaw : String = PerformancePreset . balanced . rawValue
2026-03-13 14:13:44 +00:00
@ AppStorage ( " SettingsLargeFileOpenMode " ) private var largeFileOpenModeRaw : String = " deferred "
2026-03-28 19:13:32 +00:00
@ AppStorage ( " SettingsRemoteSessionsEnabled " ) private var remoteSessionsEnabled : Bool = false
@ AppStorage ( " SettingsRemotePreparedTarget " ) private var remotePreparedTarget : String = " "
2026-03-28 19:35:14 +00:00
@ State private var remoteSessionStore = RemoteSessionStore . shared
2026-02-19 09:33:17 +00:00
#if os ( iOS )
@ AppStorage ( " SettingsForceLargeFileMode " ) var forceLargeFileMode : Bool = false
2026-02-19 14:29:53 +00:00
@ AppStorage ( " SettingsShowKeyboardAccessoryBarIOS " ) var showKeyboardAccessoryBarIOS : Bool = false
2026-02-19 09:33:17 +00:00
@ AppStorage ( " SettingsShowBottomActionBarIOS " ) var showBottomActionBarIOS : Bool = true
@ AppStorage ( " SettingsUseLiquidGlassToolbarIOS " ) var shouldUseLiquidGlass : Bool = true
2026-02-20 00:31:14 +00:00
@ AppStorage ( " SettingsToolbarIconsBlueIOS " ) var toolbarIconsBlueIOS : Bool = false
2026-02-19 09:33:17 +00:00
#endif
2026-02-08 00:06:06 +00:00
@ AppStorage ( " HasSeenWelcomeTourV1 " ) var hasSeenWelcomeTourV1 : Bool = false
2026-02-09 10:21:50 +00:00
@ AppStorage ( " WelcomeTourSeenRelease " ) var welcomeTourSeenRelease : String = " "
2026-03-09 19:39:32 +00:00
@ AppStorage ( " AppLaunchCountV1 " ) var appLaunchCount : Int = 0
@ AppStorage ( " HasShownSupportPromptV1 " ) var hasShownSupportPromptV1 : Bool = false
2026-02-08 00:06:06 +00:00
@ State var showWelcomeTour : Bool = false
2026-03-09 19:39:32 +00:00
@ State var showEditorHelp : Bool = false
@ State var showSupportPromptSheet : Bool = false
2026-02-08 09:58:46 +00:00
#if os ( macOS )
@ State private var hostWindowNumber : Int ? = nil
2026-02-19 08:58:59 +00:00
@ AppStorage ( " ShowBracketHelperBarMac " ) var showBracketHelperBarMac : Bool = false
2026-03-10 15:14:21 +00:00
@ AppStorage ( " SettingsToolbarSymbolsColorMac " ) var toolbarSymbolsColorMacRaw : String = " blue "
2026-02-28 19:48:06 +00:00
@ State private var windowCloseConfirmationDelegate : WindowCloseConfirmationDelegate ? = nil
#endif
2026-02-24 14:44:43 +00:00
@ State var showMarkdownPreviewPane : Bool = false
2026-02-28 19:48:06 +00:00
#if os ( macOS )
2026-02-24 14:44:43 +00:00
@ AppStorage ( " MarkdownPreviewTemplateMac " ) var markdownPreviewTemplateRaw : String = " default "
2026-02-28 19:48:06 +00:00
#elseif os ( iOS )
@ AppStorage ( " MarkdownPreviewTemplateIOS " ) var markdownPreviewTemplateRaw : String = " default "
2026-02-08 09:58:46 +00:00
#endif
2026-03-17 18:48:21 +00:00
@ AppStorage ( " MarkdownPreviewBackgroundStyle " ) var markdownPreviewBackgroundStyleRaw : String = " automatic "
2026-03-17 17:40:32 +00:00
@ AppStorage ( " MarkdownPreviewPDFExportMode " ) var markdownPDFExportModeRaw : String = " paginated-fit "
2026-02-09 10:21:50 +00:00
@ State private var showLanguageSetupPrompt : Bool = false
@ State private var languagePromptSelection : String = " plain "
@ State private var languagePromptInsertTemplate : Bool = false
2026-03-04 19:40:17 +00:00
@ State private var showLanguageSearchSheet : Bool = false
@ State private var languageSearchQuery : String = " "
2026-02-11 10:20:17 +00:00
@ State private var whitespaceInspectorMessage : String ? = nil
2026-02-12 22:20:39 +00:00
@ State private var didApplyStartupBehavior : Bool = false
2026-02-19 14:29:53 +00:00
@ State private var didRunInitialWindowLayoutSetup : Bool = false
2026-03-03 09:47:45 +00:00
@ State private var pendingLargeFileModeReevaluation : DispatchWorkItem ? = nil
@ State private var recoverySnapshotIdentifier : String = UUID ( ) . uuidString
2026-03-08 12:49:48 +00:00
@ State private var lastCaretLocation : Int = 0
@ State private var sessionCaretByFileURL : [ String : Int ] = [ : ]
2026-03-09 16:47:50 +00:00
#if os ( macOS )
@ State private var isProjectSidebarResizeHandleHovered : Bool = false
#endif
2026-03-03 09:47:45 +00:00
private let quickSwitcherRecentsDefaultsKey = " QuickSwitcherRecentItemsV1 "
Add broad language support + Find/Replace panel; minor helpers
Languages:
- Extend picker, detection, TOC, and syntax highlighting for:
swift, python, javascript, typescript, java, kotlin, go, ruby, rust, sql,
html, css, c, cpp, objective-c, json, xml, yaml, toml, ini, markdown,
bash, zsh, powershell, plain
Editor:
- Add Find/Replace sheet with Find Next, Replace, Replace All
- New toolbar button (magnifying glass) to open Find/Replace
- Implement find/replace helpers operating on the active NSTextView
- Small NSRange helper for cleaner optional handling
Syntax highlighting:
- Add lightweight regex patterns for Java, Kotlin, Go, Ruby, Rust, TypeScript,
Objective‑C, SQL, XML, YAML, TOML, INI
- Keep performance-friendly patterns consistent with existing approach
TOC:
- Add TOC generation for Java, Kotlin, Go, Ruby, Rust, TypeScript, Objective‑C
Detection:
- Extend heuristics for XML, YAML, TOML/INI, SQL, Go, Java, Kotlin, TypeScript,
Ruby, Rust, Objective‑C, INI
Testing:
- Verify language picker shows all new entries and switching updates highlighting
- Paste snippets of each language; ensure heuristics pick a sensible default
- Open Find/Replace; test Find Next, Replace, Replace All; verify selection/scroll
- Check large files still perform acceptably with lightweight patterns
2026-02-04 15:21:56 +00:00
2026-02-09 11:15:22 +00:00
#if USE_FOUNDATION_MODELS && canImport ( FoundationModels )
2026-02-06 18:59:53 +00:00
var appleModelAvailable : Bool { true }
2026-02-05 21:30:21 +00:00
#else
2026-02-06 18:59:53 +00:00
var appleModelAvailable : Bool { false }
2026-02-05 21:30:21 +00:00
#endif
2026-02-20 16:08:18 +00:00
var activeProviderName : String {
let trimmed = lastProviderUsed . trimmingCharacters ( in : . whitespacesAndNewlines )
if trimmed . isEmpty || trimmed = = " Apple " {
return selectedModel . displayName
}
return trimmed
}
2026-03-09 12:54:26 +00:00
private var projectNavigatorPlacement : ProjectNavigatorPlacement {
ProjectNavigatorPlacement ( rawValue : projectNavigatorPlacementRaw ) ? ? . trailing
}
private var performancePreset : PerformancePreset {
PerformancePreset ( rawValue : performancePresetRaw ) ? ? . balanced
}
2026-03-09 13:10:42 +00:00
2026-04-16 10:37:03 +00:00
private var minimumProjectSidebarWidth : CGFloat { 320 }
private var maximumProjectSidebarWidth : CGFloat { 520 }
2026-03-09 13:10:42 +00:00
private var clampedProjectSidebarWidth : CGFloat {
2026-04-16 10:37:03 +00:00
let clamped = min ( max ( projectSidebarWidth , Double ( minimumProjectSidebarWidth ) ) , Double ( maximumProjectSidebarWidth ) )
2026-03-09 13:10:42 +00:00
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
}
2026-02-19 08:44:24 +00:00
#if os ( macOS )
2026-02-19 14:29:53 +00:00
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 {
2026-03-17 17:40:32 +00:00
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
2026-02-19 14:29:53 +00:00
}
}
}
private var macTranslucencyMode : MacTranslucencyMode {
MacTranslucencyMode ( rawValue : macTranslucencyModeRaw ) ? ? . balanced
}
2026-02-19 08:44:24 +00:00
private let bracketHelperTokens : [ String ] = [ " ( " , " ) " , " { " , " } " , " [ " , " ] " , " < " , " > " , " ' " , " \" " , " ` " , " () " , " {} " , " [] " , " \" \" " , " '' " ]
2026-02-19 14:29:53 +00:00
private var macUnifiedTranslucentMaterialStyle : AnyShapeStyle {
AnyShapeStyle ( macTranslucencyMode . material . opacity ( macTranslucencyMode . opacity ) )
}
2026-03-26 18:19:45 +00:00
private var macSolidSurfaceColor : Color {
currentEditorTheme ( colorScheme : colorScheme ) . background
}
2026-02-19 14:29:53 +00:00
private var macChromeBackgroundStyle : AnyShapeStyle {
if enableTranslucentWindow {
return macUnifiedTranslucentMaterialStyle
}
2026-03-26 18:19:45 +00:00
return AnyShapeStyle ( macSolidSurfaceColor )
2026-02-19 14:29:53 +00:00
}
2026-03-10 15:14:21 +00:00
private var macToolbarBackgroundStyle : AnyShapeStyle {
if enableTranslucentWindow {
2026-03-17 17:40:32 +00:00
return AnyShapeStyle ( macTranslucencyMode . material . opacity ( macTranslucencyMode . toolbarOpacity ) )
2026-03-10 15:14:21 +00:00
}
2026-03-26 18:19:45 +00:00
return AnyShapeStyle ( macSolidSurfaceColor )
2026-03-10 15:14:21 +00:00
}
2026-02-19 09:33:17 +00:00
#elseif os ( iOS )
2026-02-20 00:31:14 +00:00
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
}
2026-02-19 09:33:17 +00:00
var toolbarDensityScale : CGFloat { 1.0 }
var toolbarDensityOpacity : Double { 1.0 }
2026-02-28 19:48:06 +00:00
private var canShowMarkdownPreviewOnCurrentDevice : Bool {
horizontalSizeClass = = . regular
}
2026-02-19 08:44:24 +00:00
#endif
2026-02-05 21:30:21 +00:00
2026-02-19 14:29:53 +00:00
private var editorSurfaceBackgroundStyle : AnyShapeStyle {
#if os ( macOS )
if enableTranslucentWindow {
return macUnifiedTranslucentMaterialStyle
}
2026-03-26 18:19:45 +00:00
return AnyShapeStyle ( macSolidSurfaceColor )
2026-02-19 14:29:53 +00:00
#else
2026-02-20 00:31:14 +00:00
if useIOSUnifiedSolidSurfaces {
return AnyShapeStyle ( iOSNonTranslucentSurfaceColor )
}
2026-02-19 14:29:53 +00:00
return enableTranslucentWindow ? AnyShapeStyle ( . ultraThinMaterial ) : AnyShapeStyle ( Color . clear )
#endif
}
2026-02-28 19:48:06 +00:00
var canShowMarkdownPreviewPane : Bool {
2026-03-08 14:31:01 +00:00
#if os ( iOS )
true
#else
true
#endif
}
private var canShowMarkdownPreviewSplitPane : Bool {
2026-02-28 19:48:06 +00:00
#if os ( iOS )
canShowMarkdownPreviewOnCurrentDevice
#else
true
#endif
}
2026-03-08 14:31:01 +00:00
#if os ( iOS )
private var shouldPresentMarkdownPreviewSheetOnIPhone : Bool {
UIDevice . current . userInterfaceIdiom = = . phone &&
showMarkdownPreviewPane &&
currentLanguage = = " markdown " &&
! brainDumpLayoutEnabled
}
private var markdownPreviewSheetPresentationBinding : Binding < Bool > {
Binding (
get : { shouldPresentMarkdownPreviewSheetOnIPhone } ,
set : { isPresented in
if ! isPresented {
showMarkdownPreviewPane = false
}
}
)
}
#endif
2026-02-28 19:48:06 +00:00
private var settingsSheetDetents : Set < PresentationDetent > {
#if os ( iOS )
if UIDevice . current . userInterfaceIdiom = = . pad {
2026-03-17 18:48:21 +00:00
return [ . fraction ( 0.985 ) ]
2026-02-28 19:48:06 +00:00
}
return [ . large ]
#else
return [ . large ]
#endif
}
2026-02-19 14:29:53 +00:00
#if os ( macOS )
private var macTabBarStripHeight : CGFloat { 36 }
#endif
2026-02-20 10:27:55 +00:00
private var useIPhoneUnifiedTopHost : Bool {
#if os ( iOS )
UIDevice . current . userInterfaceIdiom = = . phone
#else
false
#endif
}
2026-02-21 19:12:47 +00:00
private var tabBarLeadingPadding : CGFloat {
#if os ( iOS )
if UIDevice . current . userInterfaceIdiom = = . pad {
// K e e p t a b s c l e a r o f i P a d w i n d o w c o n t r o l s i n n a r r o w / m u l t i t a s k i n g l a y o u t s .
return horizontalSizeClass = = . compact ? 112 : 96
}
#endif
return 10
}
2026-02-12 22:20:39 +00:00
var selectedModel : AIModel {
get { AIModel ( rawValue : selectedModelRaw ) ? ? . appleIntelligence }
set { selectedModelRaw = newValue . rawValue }
}
2026-01-20 23:46:04 +00:00
private func promptForGrokTokenIfNeeded ( ) -> Bool {
if ! grokAPIToken . trimmingCharacters ( in : . whitespacesAndNewlines ) . isEmpty { return true }
2026-02-07 10:51:52 +00:00
#if os ( macOS )
2026-01-20 23:46:04 +00:00
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
2026-02-07 22:56:52 +00:00
SecureTokenStore . setToken ( token , for : . grok )
2026-01-20 23:46:04 +00:00
return true
}
2026-02-07 10:51:52 +00:00
#endif
2026-01-20 23:46:04 +00:00
return false
}
2026-01-25 12:46:33 +00:00
private func promptForOpenAITokenIfNeeded ( ) -> Bool {
if ! openAIAPIToken . trimmingCharacters ( in : . whitespacesAndNewlines ) . isEmpty { return true }
2026-02-07 10:51:52 +00:00
#if os ( macOS )
2026-01-25 12:46:33 +00:00
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
2026-02-07 22:56:52 +00:00
SecureTokenStore . setToken ( token , for : . openAI )
2026-01-25 12:46:33 +00:00
return true
}
2026-02-07 10:51:52 +00:00
#endif
2026-01-25 12:46:33 +00:00
return false
}
private func promptForGeminiTokenIfNeeded ( ) -> Bool {
2026-02-11 10:20:17 +00:00
if ! geminiAPIToken . isEmpty { return true }
2026-02-07 10:51:52 +00:00
#if os ( macOS )
2026-01-25 12:46:33 +00:00
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
2026-02-07 22:56:52 +00:00
SecureTokenStore . setToken ( token , for : . gemini )
2026-01-25 12:46:33 +00:00
return true
}
2026-02-07 10:51:52 +00:00
#endif
2026-01-25 12:46:33 +00:00
return false
}
2026-02-04 13:11:28 +00:00
private func promptForAnthropicTokenIfNeeded ( ) -> Bool {
if ! anthropicAPIToken . trimmingCharacters ( in : . whitespacesAndNewlines ) . isEmpty { return true }
2026-02-07 10:51:52 +00:00
#if os ( macOS )
2026-02-04 13:11:28 +00:00
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
2026-02-07 22:56:52 +00:00
SecureTokenStore . setToken ( token , for : . anthropic )
2026-02-04 13:11:28 +00:00
return true
}
2026-02-07 10:51:52 +00:00
#endif
2026-02-04 13:11:28 +00:00
return false
}
2026-02-18 18:59:25 +00:00
#if os ( macOS )
@ MainActor
private func performInlineCompletion ( for textView : NSTextView ) {
completionTask ? . cancel ( )
2026-02-18 19:19:49 +00:00
completionTask = Task ( priority : . utility ) {
2026-02-18 18:59:25 +00:00
await performInlineCompletionAsync ( for : textView )
2026-01-25 19:02:59 +00:00
}
}
2026-02-18 18:59:25 +00:00
@ MainActor
private func performInlineCompletionAsync ( for textView : NSTextView ) async {
2026-02-18 19:19:49 +00:00
let completionInterval = Self . completionSignposter . beginInterval ( " inline_completion " )
defer { Self . completionSignposter . endInterval ( " inline_completion " , completionInterval ) }
2026-01-25 19:02:59 +00:00
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
2026-02-18 18:59:25 +00:00
if Task . isCancelled { return }
2026-02-18 19:19:49 +00:00
if shouldThrottleHeavyEditorFeatures ( in : nsText ) { return }
2026-01-25 19:02:59 +00:00
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 ) )
}
// A u t o - c l o s e b r a c e s / b r a c k e t s / p a r e n s i f n o t a l r e a d y c l o s e d
let pairs : [ String : String ] = [ " { " : " } " , " ( " : " ) " , " [ " : " ] " ]
if let closing = pairs [ prevChar ] {
if nextChar != closing {
// I n s e r t c l o s i n g a n d m o v e c a r e t b a c k b e t w e e n p a i r
let insertion = closing
textView . insertText ( insertion , replacementRange : sel )
textView . setSelectedRange ( NSRange ( location : loc , length : 0 ) )
return
}
}
// I f p r e v i o u s c h a r i s ' { ' a n d l a n g u a g e i s s w i f t , j a v a s c r i p t , c , o r c p p , i n s e r t c o d e b l o c k s c a f f o l d
if prevChar = = " { " && [ " swift " , " javascript " , " c " , " cpp " ] . contains ( currentLanguage ) {
// G e t c u r r e n t l i n e i n d e n t a t i o n
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 s p a c e s
// B u i l d s c a f f o l d s t r i n g
let scaffold = " \n \( indentString ) \( indentSpaces ) \n \( indentString ) } "
2026-01-25 12:46:33 +00:00
2026-01-25 19:02:59 +00:00
// I n s e r t s c a f f o l d a t c a r e t p o s i t i o n
textView . insertText ( scaffold , replacementRange : NSRange ( location : loc , length : 0 ) )
// M o v e c a r e t t o i n d e n t e d e m p t y l i n e
let newCaretLocation = loc + 1 + indentLevel + indentSpaces . count
textView . setSelectedRange ( NSRange ( location : newCaretLocation , length : 0 ) )
return
}
2026-03-15 15:20:23 +00:00
// P r e f e r c h e a p l o c a l m a t c h e s b e f o r e m o d e l - b a c k e d c o m p l e t i o n .
2026-01-25 19:02:59 +00:00
let doc = textView . string
let nsDoc = doc as NSString
2026-03-15 15:20:23 +00:00
if let localSuggestion = CompletionHeuristics . localSuggestion (
in : nsDoc ,
caretLocation : loc ,
language : currentLanguage ,
includeDocumentWords : completionFromDocument ,
includeSyntaxKeywords : completionFromSyntax
) {
applyInlineSuggestion ( localSuggestion , textView : textView , selection : sel )
return
}
// L i m i t c o m p l e t i o n c o n t e x t b y b o t h r e c e n t l i n e s a n d U T F - 1 6 l e n g t h f o r l o w e r l a t e n c y .
let tokenContext = CompletionHeuristics . tokenContext ( in : nsDoc , caretLocation : loc )
2026-02-18 18:59:25 +00:00
let contextPrefix = completionContextPrefix ( in : nsDoc , caretLocation : loc )
2026-02-18 19:19:49 +00:00
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
}
2026-01-25 19:02:59 +00:00
2026-02-18 19:19:49 +00:00
let modelInterval = Self . completionSignposter . beginInterval ( " model_completion " )
2026-01-25 19:02:59 +00:00
let suggestion = await generateModelCompletion ( prefix : contextPrefix , language : currentLanguage )
2026-02-18 19:19:49 +00:00
Self . completionSignposter . endInterval ( " model_completion " , modelInterval )
2026-02-18 18:59:25 +00:00
if Task . isCancelled { return }
2026-03-15 15:20:23 +00:00
let sanitizedSuggestion = CompletionHeuristics . sanitizeModelSuggestion (
suggestion ,
currentTokenPrefix : tokenContext . prefix ,
nextDocumentText : tokenContext . nextDocumentText
)
storeCompletionInCache ( sanitizedSuggestion , for : cacheKey )
applyInlineSuggestion ( sanitizedSuggestion , textView : textView , selection : sel )
2026-02-18 18:59:25 +00:00
}
2026-01-25 19:02:59 +00:00
2026-02-18 18:59:25 +00:00
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
2026-01-25 19:02:59 +00:00
}
2026-02-18 18:59:25 +00:00
cursor = found . location
seenLines += 1
2026-01-25 19:02:59 +00:00
}
2026-02-18 18:59:25 +00:00
let startByLines = cursor
let start = max ( startByChars , startByLines )
return nsDoc . substring ( with : NSRange ( location : start , length : caretLocation - start ) )
2026-01-25 19:02:59 +00:00
}
2026-02-18 19:19:49 +00:00
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 {
2026-03-13 14:13:44 +00:00
if effectiveLargeFileModeEnabled { return true }
2026-02-20 22:04:03 +00:00
let length = nsText ? . length ? ? currentDocumentUTF16Length
2026-02-20 17:04:36 +00:00
return length >= EditorPerformanceThresholds . heavyFeatureUTF16Length
2026-02-18 19:19:49 +00:00
}
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 }
2026-02-19 08:09:35 +00:00
if shouldThrottleHeavyEditorFeatures ( in : nsText ) { return false }
2026-02-18 19:19:49 +00:00
let prevChar = nsText . substring ( with : NSRange ( location : location - 1 , length : 1 ) )
2026-03-15 15:20:23 +00:00
let triggerChars : Set < String > = [ " . " , " ( " , " ) " , " { " , " } " , " [ " , " ] " , " : " , " , " , " \n " , " \t " ]
2026-02-18 19:19:49 +00:00
if triggerChars . contains ( prevChar ) { return true }
2026-03-15 15:20:23 +00:00
if prevChar = = " " {
return CompletionHeuristics . shouldTriggerAfterWhitespace (
in : nsText ,
caretLocation : location ,
language : currentLanguage
)
}
2026-02-18 19:19:49 +00:00
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
2026-02-19 08:09:35 +00:00
if docLength >= 80_000 { return 0.9 }
if docLength >= 25_000 { return 0.7 }
return 0.45
2026-02-18 22:56:46 +00:00
}
2026-02-19 08:09:35 +00:00
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 = " "
}
// K e e p s i g n a t u r e c h e a p w h i l e s p e c i f i c e n o u g h t o s k i p d u p l i c a t e n o t i f i c a t i o n s .
return " \( location ) | \( prevChar ) | \( nextChar ) | \( nsText . length ) "
2026-02-18 19:19:49 +00:00
}
2026-02-18 18:59:25 +00:00
#endif
2026-01-25 19:02:59 +00:00
2026-02-05 21:30:21 +00:00
private func externalModelCompletion ( prefix : String , language : String ) async -> String {
// T r y G r o k
if ! grokAPIToken . isEmpty {
2026-01-25 19:02:59 +00:00
do {
2026-02-12 17:31:51 +00:00
guard let url = URL ( string : " https://api.x.ai/v1/chat/completions " ) else { return " " }
2026-02-05 21:30:21 +00:00
var request = URLRequest ( url : url )
request . httpMethod = " POST "
request . setValue ( " Bearer \( grokAPIToken ) " , forHTTPHeaderField : " Authorization " )
request . setValue ( " application/json " , forHTTPHeaderField : " Content-Type " )
2026-01-25 19:02:59 +00:00
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 :
" " "
2026-02-05 21:30:21 +00:00
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 )
}
2026-02-07 22:56:52 +00:00
} catch {
debugLog ( " [Completion][Fallback][Grok] request failed " )
}
2026-02-05 21:30:21 +00:00
}
// T r y O p e n A I
if ! openAIAPIToken . isEmpty {
do {
2026-02-12 17:31:51 +00:00
guard let url = URL ( string : " https://api.openai.com/v1/chat/completions " ) else { return " " }
2026-02-05 21:30:21 +00:00
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 )
}
2026-02-07 22:56:52 +00:00
} catch {
debugLog ( " [Completion][Fallback][OpenAI] request failed " )
}
2026-02-05 21:30:21 +00:00
}
// T r y G e m i n i
if ! geminiAPIToken . isEmpty {
do {
let model = " gemini-1.5-flash-latest "
2026-02-07 22:56:52 +00:00
let endpoint = " https://generativelanguage.googleapis.com/v1beta/models/ \( model ) :generateContent "
2026-02-05 21:30:21 +00:00
guard let url = URL ( string : endpoint ) else { return " " }
var request = URLRequest ( url : url )
request . httpMethod = " POST "
2026-02-07 22:56:52 +00:00
request . setValue ( geminiAPIToken , forHTTPHeaderField : " x-goog-api-key " )
2026-02-05 21:30:21 +00:00
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 )
}
2026-02-07 22:56:52 +00:00
} catch {
debugLog ( " [Completion][Fallback][Gemini] request failed " )
}
2026-02-05 21:30:21 +00:00
}
// T r y A n t h r o p i c
if ! anthropicAPIToken . isEmpty {
do {
2026-02-12 17:31:51 +00:00
guard let url = URL ( string : " https://api.anthropic.com/v1/messages " ) else { return " " }
2026-02-05 21:30:21 +00:00
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 )
}
2026-02-07 22:56:52 +00:00
} catch {
debugLog ( " [Completion][Fallback][Anthropic] request failed " )
}
2026-02-05 21:30:21 +00:00
}
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 \n Completion: " ) {
if firstChunk = = nil , ! chunk . trimmingCharacters ( in : . whitespacesAndNewlines ) . isEmpty {
firstChunk = chunk
break
} else {
aggregated += chunk
2026-01-25 19:02:59 +00:00
}
2026-02-05 21:30:21 +00:00
}
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 )
2026-01-20 23:46:04 +00:00
case . grok :
2026-02-05 21:30:21 +00:00
if grokAPIToken . isEmpty {
let res = await appleModelCompletion ( prefix : prefix , language : language )
await MainActor . run { lastProviderUsed = " Grok (fallback to Apple) " }
return res
}
2026-02-04 13:11:28 +00:00
do {
2026-02-12 17:31:51 +00:00
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
}
2026-02-04 13:11:28 +00:00
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 " ,
2026-02-05 21:30:21 +00:00
" messages " : [ [ " role " : " user " , " content " : prompt ] ] ,
2026-02-04 13:11:28 +00:00
" 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 {
2026-02-05 21:30:21 +00:00
await MainActor . run { lastProviderUsed = " Grok " }
2026-02-04 13:11:28 +00:00
return sanitizeCompletion ( content )
}
2026-02-05 21:30:21 +00:00
// I f n o c o n t e n t , f a l l b a c k t o A p p l e
let res = await appleModelCompletion ( prefix : prefix , language : language )
await MainActor . run { lastProviderUsed = " Grok (fallback to Apple) " }
return res
2026-02-04 13:11:28 +00:00
} catch {
2026-02-07 22:56:52 +00:00
debugLog ( " [Completion][Grok] request failed " )
2026-02-05 21:30:21 +00:00
let res = await appleModelCompletion ( prefix : prefix , language : language )
await MainActor . run { lastProviderUsed = " Grok (fallback to Apple) " }
return res
2026-02-04 13:11:28 +00:00
}
2026-01-25 12:46:33 +00:00
case . openAI :
2026-02-05 21:30:21 +00:00
if openAIAPIToken . isEmpty {
let res = await appleModelCompletion ( prefix : prefix , language : language )
await MainActor . run { lastProviderUsed = " OpenAI (fallback to Apple) " }
return res
}
2026-01-25 19:02:59 +00:00
do {
2026-02-12 17:31:51 +00:00
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
}
2026-01-25 19:02:59 +00:00
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 " ,
2026-02-05 21:30:21 +00:00
" messages " : [ [ " role " : " user " , " content " : prompt ] ] ,
2026-01-25 19:02:59 +00:00
" 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 {
2026-02-05 21:30:21 +00:00
await MainActor . run { lastProviderUsed = " OpenAI " }
2026-01-25 19:02:59 +00:00
return sanitizeCompletion ( content )
}
2026-02-05 21:30:21 +00:00
let res = await appleModelCompletion ( prefix : prefix , language : language )
await MainActor . run { lastProviderUsed = " OpenAI (fallback to Apple) " }
return res
2026-01-25 19:02:59 +00:00
} catch {
2026-02-07 22:56:52 +00:00
debugLog ( " [Completion][OpenAI] request failed " )
2026-02-05 21:30:21 +00:00
let res = await appleModelCompletion ( prefix : prefix , language : language )
await MainActor . run { lastProviderUsed = " OpenAI (fallback to Apple) " }
return res
2026-01-25 19:02:59 +00:00
}
2026-01-25 12:46:33 +00:00
case . gemini :
2026-02-05 21:30:21 +00:00
if geminiAPIToken . isEmpty {
let res = await appleModelCompletion ( prefix : prefix , language : language )
await MainActor . run { lastProviderUsed = " Gemini (fallback to Apple) " }
return res
}
2026-02-04 13:11:28 +00:00
do {
let model = " gemini-1.5-flash-latest "
2026-02-07 22:56:52 +00:00
let endpoint = " https://generativelanguage.googleapis.com/v1beta/models/ \( model ) :generateContent "
2026-02-05 21:30:21 +00:00
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
}
2026-02-04 13:11:28 +00:00
var request = URLRequest ( url : url )
request . httpMethod = " POST "
2026-02-07 22:56:52 +00:00
request . setValue ( geminiAPIToken , forHTTPHeaderField : " x-goog-api-key " )
2026-02-04 13:11:28 +00:00
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 ] = [
2026-02-05 21:30:21 +00:00
" contents " : [ [ " parts " : [ [ " text " : prompt ] ] ] ] ,
" generationConfig " : [ " temperature " : 0.5 , " maxOutputTokens " : 64 ]
2026-02-04 13:11:28 +00:00
]
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 {
2026-02-05 21:30:21 +00:00
await MainActor . run { lastProviderUsed = " Gemini " }
2026-02-04 13:11:28 +00:00
return sanitizeCompletion ( text )
}
2026-02-05 21:30:21 +00:00
let res = await appleModelCompletion ( prefix : prefix , language : language )
await MainActor . run { lastProviderUsed = " Gemini (fallback to Apple) " }
return res
2026-02-04 13:11:28 +00:00
} catch {
2026-02-07 22:56:52 +00:00
debugLog ( " [Completion][Gemini] request failed " )
2026-02-05 21:30:21 +00:00
let res = await appleModelCompletion ( prefix : prefix , language : language )
await MainActor . run { lastProviderUsed = " Gemini (fallback to Apple) " }
return res
2026-02-04 13:11:28 +00:00
}
case . anthropic :
2026-02-05 21:30:21 +00:00
if anthropicAPIToken . isEmpty {
let res = await appleModelCompletion ( prefix : prefix , language : language )
await MainActor . run { lastProviderUsed = " Anthropic (fallback to Apple) " }
return res
}
2026-02-04 13:11:28 +00:00
do {
2026-02-12 17:31:51 +00:00
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
}
2026-02-04 13:11:28 +00:00
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 ,
2026-02-05 21:30:21 +00:00
" messages " : [ [ " role " : " user " , " content " : prompt ] ]
2026-02-04 13:11:28 +00:00
]
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 {
2026-02-05 21:30:21 +00:00
await MainActor . run { lastProviderUsed = " Anthropic " }
2026-02-04 13:11:28 +00:00
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 {
2026-02-05 21:30:21 +00:00
await MainActor . run { lastProviderUsed = " Anthropic " }
2026-02-04 13:11:28 +00:00
return sanitizeCompletion ( text )
}
2026-02-05 21:30:21 +00:00
let res = await appleModelCompletion ( prefix : prefix , language : language )
await MainActor . run { lastProviderUsed = " Anthropic (fallback to Apple) " }
return res
2026-02-04 13:11:28 +00:00
} catch {
2026-02-07 22:56:52 +00:00
debugLog ( " [Completion][Anthropic] request failed " )
2026-02-05 21:30:21 +00:00
let res = await appleModelCompletion ( prefix : prefix , language : language )
await MainActor . run { lastProviderUsed = " Anthropic (fallback to Apple) " }
return res
2026-02-04 13:11:28 +00:00
}
2026-01-20 23:46:04 +00:00
}
2026-01-25 19:02:59 +00:00
}
2026-01-25 12:46:33 +00:00
2026-01-25 19:02:59 +00:00
private func sanitizeCompletion ( _ raw : String ) -> String {
2026-03-15 15:20:23 +00:00
CompletionHeuristics . sanitizeModelSuggestion ( raw , currentTokenPrefix : " " , nextDocumentText : " " )
2026-01-20 23:46:04 +00:00
}
2026-02-07 22:56:52 +00:00
private func debugLog ( _ message : String ) {
2026-02-25 13:07:05 +00:00
if message . contains ( " [Completion] " ) || message . contains ( " AI " ) || message . contains ( " [AI] " ) {
AIActivityLog . record ( message , source : " Completion " )
}
2026-02-07 22:56:52 +00:00
#if DEBUG
print ( message )
#endif
}
2026-02-08 09:58:46 +00:00
#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
2026-02-22 13:04:27 +00:00
installWindowCloseConfirmationDelegate ( window )
2026-03-13 14:13:44 +00:00
updateWindowChrome ( window )
2026-02-08 09:58:46 +00:00
if let number {
WindowViewModelRegistry . shared . register ( viewModel , for : number )
}
}
2026-02-19 08:44:24 +00:00
2026-03-13 14:13:44 +00:00
private func updateWindowChrome ( _ window : NSWindow ? = nil ) {
guard let targetWindow = window ? ? hostWindowNumber . flatMap ( { NSApp . window ( withWindowNumber : $0 ) } ) else { return }
2026-03-28 19:13:32 +00:00
targetWindow . subtitle = windowSubtitleText
2026-03-13 14:13:44 +00:00
}
2026-02-22 13:04:27 +00:00
private func saveAllDirtyTabsForWindowClose ( ) -> Bool {
let dirtyTabIDs = viewModel . tabs . filter ( \ . isDirty ) . map ( \ . id )
guard ! dirtyTabIDs . isEmpty else { return true }
for tabID in dirtyTabIDs {
2026-02-25 13:07:05 +00:00
guard viewModel . tabs . contains ( where : { $0 . id = = tabID } ) else { continue }
viewModel . saveFile ( tabID : tabID )
2026-02-22 13:04:27 +00:00
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 ( ) }
}
2026-02-19 08:44:24 +00:00
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
)
}
2026-02-08 09:58:46 +00:00
#else
private func matchesCurrentWindow ( _ notif : Notification ) -> Bool { true }
#endif
2026-02-19 08:44:24 +00:00
#if os ( macOS )
private var bracketHelperBar : some View {
ScrollView ( . horizontal , showsIndicators : false ) {
HStack ( spacing : 8 ) {
ForEach ( bracketHelperTokens , id : \ . self ) { token in
2026-03-13 08:27:31 +00:00
Button ( action : {
2026-02-19 08:44:24 +00:00
requestBracketHelperInsert ( token )
2026-03-13 08:27:31 +00:00
} ) {
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 ) )
)
2026-02-19 08:44:24 +00:00
}
. buttonStyle ( . plain )
2026-03-13 08:27:31 +00:00
. accessibilityLabel ( Text ( " Insert \( token ) " ) )
2026-02-19 08:44:24 +00:00
}
}
. padding ( . horizontal , 10 )
. padding ( . vertical , 6 )
}
2026-02-19 14:29:53 +00:00
. background ( editorSurfaceBackgroundStyle )
2026-02-19 08:44:24 +00:00
}
#endif
2026-02-08 09:58:46 +00:00
private func withBaseEditorEvents < Content : View > ( _ view : Content ) -> some View {
2026-02-11 12:56:57 +00:00
let viewWithClipboardEvents = view
2026-02-08 09:58:46 +00:00
. onReceive ( NotificationCenter . default . publisher ( for : . caretPositionDidChange ) ) { notif in
2026-03-08 12:49:48 +00:00
if let location = notif . userInfo ? [ " location " ] as ? Int , location >= 0 {
lastCaretLocation = location
if let selectedURL = viewModel . selectedTab ? . fileURL ? . standardizedFileURL {
sessionCaretByFileURL [ selectedURL . absoluteString ] = location
}
}
2026-02-08 09:58:46 +00:00
if let line = notif . userInfo ? [ " line " ] as ? Int , let col = notif . userInfo ? [ " column " ] as ? Int {
2026-02-08 11:57:41 +00:00
if line <= 0 {
caretStatus = " Pos \( col ) "
} else {
caretStatus = " Ln \( line ) , Col \( col ) "
}
2026-02-08 09:58:46 +00:00
}
2026-03-04 19:54:59 +00:00
#if os ( iOS )
// K e e p f l o a t i n g s t a t u s p i l l w o r d c o u n t i n s y n c w i t h l i v e b u f f e r w h i l e t y p i n g .
let liveText = liveEditorBufferText ( ) ? ? currentContent
scheduleWordCountRefresh ( for : liveText )
#endif
2026-02-08 09:58:46 +00:00
}
2026-03-15 16:37:20 +00:00
. onReceive ( NotificationCenter . default . publisher ( for : . editorSelectionDidChange ) ) { notif in
let selection = ( notif . object as ? String ) ? ? " "
currentSelectionSnapshotText = selection
}
2026-03-15 17:51:17 +00:00
. onReceive ( NotificationCenter . default . publisher ( for : . editorRequestCodeSnapshotFromSelection ) ) { _ in
presentCodeSnapshotComposer ( )
}
2026-02-11 12:56:57 +00:00
. onReceive ( NotificationCenter . default . publisher ( for : . pastedText ) ) { notif in
handlePastedTextNotification ( notif )
2026-02-11 10:20:17 +00:00
}
2026-02-11 12:56:57 +00:00
. onReceive ( NotificationCenter . default . publisher ( for : . pastedFileURL ) ) { notif in
handlePastedFileNotification ( notif )
2026-02-11 10:20:17 +00:00
}
2026-02-11 12:56:57 +00:00
. 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 )
2026-02-11 10:20:17 +00:00
}
2026-02-11 12:56:57 +00:00
. onReceive ( NotificationCenter . default . publisher ( for : . droppedFileURL ) ) { notif in
handleDroppedFileNotification ( notif )
2026-02-11 10:20:17 +00:00
}
2026-02-11 12:56:57 +00:00
2026-03-30 17:42:37 +00:00
let viewWithDroppedFileLoadEvents = AnyView (
viewWithClipboardEvents
2026-02-11 12:56:57 +00:00
. 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
// R e c o v e r e v e n i f " s t a r t e d " w a s m i s s e d .
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
}
}
2026-03-30 17:42:37 +00:00
)
let viewWithSelectionObservers = AnyView (
viewWithDroppedFileLoadEvents
2026-02-11 12:56:57 +00:00
. onChange ( of : viewModel . selectedTab ? . id ) { _ , _ in
2026-03-13 08:27:31 +00:00
editorExternalMutationRevision &+= 1
2026-03-03 09:47:45 +00:00
updateLargeFileModeForCurrentContext ( )
scheduleLargeFileModeReevaluation ( after : 0.9 )
scheduleHighlightRefresh ( )
2026-03-08 12:49:48 +00:00
if let selectedID = viewModel . selectedTab ? . id {
viewModel . refreshExternalConflictForTab ( tabID : selectedID )
}
restoreCaretForSelectedSessionFileIfAvailable ( )
persistSessionIfReady ( )
2026-03-03 09:47:45 +00:00
}
. onChange ( of : viewModel . selectedTab ? . isLoadingContent ? ? false ) { _ , isLoading in
if isLoading {
2026-03-16 10:20:48 +00:00
let shouldPreEnableLargeMode =
droppedFileLoadInProgress ||
viewModel . selectedTab ? . isLargeFileCandidate = = true ||
currentDocumentUTF16Length >= 300_000
if shouldPreEnableLargeMode , ! largeFileModeEnabled {
2026-02-20 19:32:03 +00:00
largeFileModeEnabled = true
}
} else {
2026-03-03 09:47:45 +00:00
scheduleLargeFileModeReevaluation ( after : 0.8 )
2026-02-20 19:32:03 +00:00
}
2026-02-18 19:19:49 +00:00
scheduleHighlightRefresh ( )
2026-02-08 11:14:49 +00:00
}
2026-02-11 12:56:57 +00:00
. onChange ( of : currentLanguage ) { _ , newValue in
settingsTemplateLanguage = newValue
2026-02-08 11:14:49 +00:00
}
2026-03-08 12:49:48 +00:00
. onChange ( of : viewModel . pendingExternalFileConflict ? . tabID ) { _ , conflictTabID in
if conflictTabID != nil {
showExternalConflictDialog = true
}
}
2026-03-30 17:07:02 +00:00
. onChange ( of : viewModel . pendingRemoteSaveIssue ? . tabID ) { _ , issueTabID in
if issueTabID != nil {
showRemoteSaveIssueDialog = true
}
}
2026-03-30 17:42:37 +00:00
)
return viewWithSelectionObservers
2026-03-30 17:07:02 +00:00
. onChange ( of : showRemoteSaveIssueDialog ) { _ , isPresented in
if ! isPresented , viewModel . pendingRemoteSaveIssue != nil {
viewModel . dismissRemoteSaveIssue ( )
}
}
2026-02-11 12:56:57 +00:00
}
private func handlePastedTextNotification ( _ notif : Notification ) {
guard let pasted = notif . object as ? String else {
2026-02-11 10:20:17 +00:00
DispatchQueue . main . async {
2026-03-13 14:13:44 +00:00
updateLargeFileModeForCurrentContext ( )
2026-02-18 19:19:49 +00:00
scheduleHighlightRefresh ( )
2026-02-11 10:20:17 +00:00
}
2026-02-11 12:56:57 +00:00
return
2026-02-08 11:14:49 +00:00
}
2026-02-11 12:56:57 +00:00
let result = LanguageDetector . shared . detect ( text : pasted , name : nil , fileURL : nil )
2026-02-25 13:07:05 +00:00
if let tab = viewModel . selectedTab ,
! tab . languageLocked ,
tab . language = = " plain " ,
result . lang != " plain " {
viewModel . setTabLanguage ( tabID : tab . id , language : result . lang , lock : false )
2026-02-11 12:56:57 +00:00
} else if singleLanguage = = " plain " , result . lang != " plain " {
singleLanguage = result . lang
2026-02-08 11:14:49 +00:00
}
2026-02-11 12:56:57 +00:00
DispatchQueue . main . async {
2026-03-13 14:13:44 +00:00
updateLargeFileModeForCurrentContext ( )
2026-02-18 19:19:49 +00:00
scheduleHighlightRefresh ( )
2026-02-08 11:14:49 +00:00
}
2026-02-11 12:56:57 +00:00
}
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 {
2026-03-13 14:13:44 +00:00
updateLargeFileModeForCurrentContext ( )
2026-02-18 19:19:49 +00:00
scheduleHighlightRefresh ( )
2026-02-11 10:20:17 +00:00
}
2026-02-11 12:56:57 +00:00
}
private func handleDroppedFileNotification ( _ notif : Notification ) {
guard let fileURL = notif . object as ? URL else { return }
if let preferred = LanguageDetector . shared . preferredLanguage ( for : fileURL ) {
2026-02-25 13:07:05 +00:00
if let tab = viewModel . selectedTab ,
! tab . languageLocked ,
tab . language = = " plain " {
viewModel . setTabLanguage ( tabID : tab . id , language : preferred , lock : false )
2026-02-11 12:56:57 +00:00
} else if singleLanguage = = " plain " {
singleLanguage = preferred
}
}
DispatchQueue . main . async {
2026-03-13 14:13:44 +00:00
updateLargeFileModeForCurrentContext ( )
2026-02-18 19:19:49 +00:00
scheduleHighlightRefresh ( )
2026-02-11 10:20:17 +00:00
}
}
2026-02-19 09:33:17 +00:00
func updateLargeFileMode ( for text : String ) {
2026-03-16 10:20:48 +00:00
if droppedFileLoadInProgress {
2026-02-20 19:32:03 +00:00
if ! largeFileModeEnabled {
largeFileModeEnabled = true
scheduleHighlightRefresh ( )
}
return
}
2026-03-16 10:20:48 +00:00
if viewModel . selectedTab ? . isLoadingContent = = true {
if ( viewModel . selectedTab ? . isLargeFileCandidate = = true || currentDocumentUTF16Length >= 300_000 ) ,
! largeFileModeEnabled {
largeFileModeEnabled = true
scheduleHighlightRefresh ( )
}
return
}
2026-03-13 14:13:44 +00:00
if viewModel . selectedTab ? . isLargeFileCandidate = = true {
if ! largeFileModeEnabled {
largeFileModeEnabled = true
scheduleHighlightRefresh ( )
}
return
}
2026-02-20 19:32:03 +00:00
let lowerLanguage = currentLanguage . lowercased ( )
let isHTMLLike = [ " html " , " htm " , " xml " , " svg " , " xhtml " ] . contains ( lowerLanguage )
let isCSVLike = [ " csv " , " tsv " ] . contains ( lowerLanguage )
let useAggressiveThresholds = isHTMLLike || isCSVLike
2026-03-09 12:54:26 +00:00
#if os ( iOS )
var byteThreshold = useAggressiveThresholds
? EditorPerformanceThresholds . largeFileBytesHTMLCSVMobile
: EditorPerformanceThresholds . largeFileBytesMobile
var lineThreshold = useAggressiveThresholds
? EditorPerformanceThresholds . largeFileLineBreaksHTMLCSVMobile
: EditorPerformanceThresholds . largeFileLineBreaksMobile
#else
var byteThreshold = useAggressiveThresholds
2026-02-20 19:32:03 +00:00
? EditorPerformanceThresholds . largeFileBytesHTMLCSV
: EditorPerformanceThresholds . largeFileBytes
2026-03-09 12:54:26 +00:00
var lineThreshold = useAggressiveThresholds
2026-02-20 19:32:03 +00:00
? EditorPerformanceThresholds . largeFileLineBreaksHTMLCSV
: EditorPerformanceThresholds . largeFileLineBreaks
2026-03-09 12:54:26 +00:00
#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 ) )
}
2026-02-20 19:32:03 +00:00
let byteCount = text . utf8 . count
let exceedsByteThreshold = byteCount >= byteThreshold
let exceedsLineThreshold : Bool = {
if exceedsByteThreshold { return true }
var lineBreaks = 0
2026-03-06 18:29:12 +00:00
var currentLineLength = 0
let csvLongLineThreshold = 16_000
2026-02-20 19:32:03 +00:00
for codeUnit in text . utf16 {
if codeUnit = = 10 { // ' \ n '
lineBreaks += 1
2026-03-06 18:29:12 +00:00
currentLineLength = 0
2026-02-20 19:32:03 +00:00
if lineBreaks >= lineThreshold {
return true
}
2026-03-06 18:29:12 +00:00
continue
}
if isCSVLike {
currentLineLength += 1
if currentLineLength >= csvLongLineThreshold {
return true
}
2026-02-20 19:32:03 +00:00
}
}
return false
} ( )
2026-02-19 09:33:17 +00:00
#if os ( iOS )
2026-02-20 19:32:03 +00:00
let isLarge = forceLargeFileMode
|| exceedsByteThreshold
|| exceedsLineThreshold
2026-02-19 09:33:17 +00:00
#else
2026-02-20 19:32:03 +00:00
let isLarge = exceedsByteThreshold
|| exceedsLineThreshold
2026-02-19 09:33:17 +00:00
#endif
2026-02-19 08:09:35 +00:00
if largeFileModeEnabled != isLarge {
largeFileModeEnabled = isLarge
2026-02-18 19:19:49 +00:00
scheduleHighlightRefresh ( )
2026-02-11 10:20:17 +00:00
}
}
2026-03-03 09:47:45 +00:00
private func updateLargeFileModeForCurrentContext ( ) {
2026-03-16 10:20:48 +00:00
if droppedFileLoadInProgress {
2026-03-13 14:13:44 +00:00
if ! largeFileModeEnabled {
largeFileModeEnabled = true
scheduleHighlightRefresh ( )
}
return
}
2026-03-16 10:20:48 +00:00
if viewModel . selectedTab ? . isLoadingContent = = true {
if ( viewModel . selectedTab ? . isLargeFileCandidate = = true || currentDocumentUTF16Length >= 300_000 ) ,
! largeFileModeEnabled {
largeFileModeEnabled = true
scheduleHighlightRefresh ( )
}
return
}
2026-03-13 14:13:44 +00:00
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
2026-03-03 09:47:45 +00:00
}
private func scheduleLargeFileModeReevaluation ( after delay : TimeInterval ) {
pendingLargeFileModeReevaluation ? . cancel ( )
let work = DispatchWorkItem {
updateLargeFileModeForCurrentContext ( )
}
pendingLargeFileModeReevaluation = work
DispatchQueue . main . asyncAfter ( deadline : . now ( ) + delay , execute : work )
}
2026-02-19 09:33:17 +00:00
func recordDiagnostic ( _ message : String ) {
#if DEBUG
print ( " [NVE] \( message ) " )
#endif
}
2026-02-11 10:20:17 +00:00
func adjustEditorFontSize ( _ delta : Double ) {
let clamped = min ( 28 , max ( 10 , editorFontSize + delta ) )
if clamped != editorFontSize {
editorFontSize = clamped
2026-02-18 19:19:49 +00:00
scheduleHighlightRefresh ( )
2026-02-11 10:20:17 +00:00
}
}
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
2026-02-08 09:58:46 +00:00
}
private func withCommandEvents < Content : View > ( _ view : Content ) -> some View {
2026-02-11 10:20:17 +00:00
let viewWithEditorActions = view
2026-02-08 09:58:46 +00:00
. onReceive ( NotificationCenter . default . publisher ( for : . clearEditorRequested ) ) { notif in
guard matchesCurrentWindow ( notif ) else { return }
2026-02-12 22:20:39 +00:00
requestClearEditorContent ( )
2026-02-08 09:58:46 +00:00
}
2026-02-11 10:20:17 +00:00
. onChange ( of : isAutoCompletionEnabled ) { _ , enabled in
if enabled && viewModel . isBrainDumpMode {
viewModel . isBrainDumpMode = false
UserDefaults . standard . set ( false , forKey : " BrainDumpModeEnabled " )
}
2026-02-18 18:59:25 +00:00
syncAppleCompletionAvailability ( )
2026-02-11 10:20:17 +00:00
if enabled && currentLanguage = = " plain " && ! showLanguageSetupPrompt {
showLanguageSetupPrompt = true
}
2026-02-08 09:58:46 +00:00
}
. 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
2026-03-17 17:40:32 +00:00
NotificationCenter . default . post (
name : . vimModeStateDidChange ,
object : nil ,
userInfo : [ " insertMode " : vimInsertMode ]
)
2026-02-08 09:58:46 +00:00
}
. 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 }
2026-02-20 10:27:55 +00:00
#if os ( iOS )
viewModel . isBrainDumpMode = false
UserDefaults . standard . set ( false , forKey : " BrainDumpModeEnabled " )
#else
2026-02-08 09:58:46 +00:00
viewModel . isBrainDumpMode . toggle ( )
UserDefaults . standard . set ( viewModel . isBrainDumpMode , forKey : " BrainDumpModeEnabled " )
2026-02-20 10:27:55 +00:00
#endif
2026-02-08 09:58:46 +00:00
}
. 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
}
}
2026-02-11 10:20:17 +00:00
2026-03-15 14:56:58 +00:00
let viewWithPanelTriggers = viewWithEditorActions
2026-02-11 10:20:17 +00:00
. onReceive ( NotificationCenter . default . publisher ( for : . showFindReplaceRequested ) ) { notif in
guard matchesCurrentWindow ( notif ) else { return }
showFindReplace = true
}
2026-02-25 13:07:05 +00:00
. onReceive ( NotificationCenter . default . publisher ( for : . findNextRequested ) ) { notif in
guard matchesCurrentWindow ( notif ) else { return }
findNext ( )
}
2026-02-11 10:20:17 +00:00
. onReceive ( NotificationCenter . default . publisher ( for : . showQuickSwitcherRequested ) ) { notif in
guard matchesCurrentWindow ( notif ) else { return }
quickSwitcherQuery = " "
showQuickSwitcher = true
}
2026-03-15 14:56:58 +00:00
. 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 ( )
}
2026-03-03 09:47:45 +00:00
. onReceive ( NotificationCenter . default . publisher ( for : . addNextMatchRequested ) ) { notif in
guard matchesCurrentWindow ( notif ) else { return }
addNextMatchSelection ( )
}
2026-02-25 13:07:05 +00:00
. onReceive ( NotificationCenter . default . publisher ( for : . showFindInFilesRequested ) ) { notif in
guard matchesCurrentWindow ( notif ) else { return }
if findInFilesQuery . isEmpty {
findInFilesQuery = findQuery
}
showFindInFiles = true
}
2026-02-11 10:20:17 +00:00
. onReceive ( NotificationCenter . default . publisher ( for : . showWelcomeTourRequested ) ) { notif in
guard matchesCurrentWindow ( notif ) else { return }
showWelcomeTour = true
}
2026-03-09 19:39:32 +00:00
. 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
}
2026-03-15 14:56:58 +00:00
let viewWithPanels = viewWithPanelTriggers
. onReceive ( NotificationCenter . default . publisher ( for : . toggleProjectStructureSidebarRequested ) ) { notif in
guard matchesCurrentWindow ( notif ) else { return }
toggleProjectSidebarFromToolbar ( )
}
2026-02-25 13:07:05 +00:00
. onReceive ( NotificationCenter . default . publisher ( for : . openProjectFolderRequested ) ) { notif in
guard matchesCurrentWindow ( notif ) else { return }
openProjectFolder ( )
}
2026-02-08 09:58:46 +00:00
. onReceive ( NotificationCenter . default . publisher ( for : . showAPISettingsRequested ) ) { notif in
guard matchesCurrentWindow ( notif ) else { return }
2026-02-11 10:20:17 +00:00
openAPISettings ( )
2026-02-08 09:58:46 +00:00
}
2026-02-20 16:13:22 +00:00
. 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 ( )
}
}
2026-03-08 12:20:14 +00:00
. onReceive ( NotificationCenter . default . publisher ( for : . closeSelectedTabRequested ) ) { notif in
guard matchesCurrentWindow ( notif ) else { return }
guard let tab = viewModel . selectedTab else { return }
requestCloseTab ( tab )
}
2026-02-14 13:24:01 +00:00
. onReceive ( NotificationCenter . default . publisher ( for : . showUpdaterRequested ) ) { notif in
guard matchesCurrentWindow ( notif ) else { return }
let shouldCheckNow = ( notif . object as ? Bool ) ? ? true
showUpdaterDialog ( checkNow : shouldCheckNow )
}
2026-02-08 09:58:46 +00:00
. 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 }
2026-02-12 22:20:39 +00:00
selectedModelRaw = model . rawValue
2026-02-08 09:58:46 +00:00
}
2026-02-11 10:20:17 +00:00
return viewWithPanels
2026-02-08 09:58:46 +00:00
}
private func withTypingEvents < Content : View > ( _ view : Content ) -> some View {
#if os ( macOS )
view
2026-02-18 18:59:25 +00:00
. onReceive ( NotificationCenter . default . publisher ( for : NSText . didChangeNotification ) ) { notif in
2026-02-09 10:21:50 +00:00
guard isAutoCompletionEnabled && ! viewModel . isBrainDumpMode && ! isApplyingCompletion else { return }
2026-02-18 18:59:25 +00:00
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
}
2026-02-18 19:19:49 +00:00
guard shouldScheduleCompletion ( for : changedTextView ) else { return }
2026-02-19 08:09:35 +00:00
let signature = completionTriggerSignature ( for : changedTextView )
guard ! signature . isEmpty else { return }
if signature = = lastCompletionTriggerSignature {
return
}
lastCompletionTriggerSignature = signature
completionDebounceTask ? . cancel ( )
2026-02-18 18:59:25 +00:00
completionTask ? . cancel ( )
2026-02-18 19:19:49 +00:00
let debounce = completionDebounceInterval ( for : changedTextView )
2026-02-19 08:09:35 +00:00
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 )
2026-02-08 09:58:46 +00:00
}
}
#else
view
#endif
}
2026-02-07 10:51:52 +00:00
@ ViewBuilder
private var platformLayout : some View {
#if os ( macOS )
2026-01-25 19:02:59 +00:00
Group {
2026-02-07 10:51:52 +00:00
if shouldUseSplitView {
2026-01-25 19:02:59 +00:00
NavigationSplitView {
sidebarView
} detail : {
editorView
}
. navigationSplitViewColumnWidth ( min : 200 , ideal : 250 , max : 600 )
2026-02-19 14:29:53 +00:00
. background ( editorSurfaceBackgroundStyle )
2026-01-25 19:02:59 +00:00
} else {
editorView
}
2025-09-25 09:01:45 +00:00
}
. frame ( minWidth : 600 , minHeight : 400 )
2026-02-07 10:51:52 +00:00
#else
NavigationStack {
Group {
if shouldUseSplitView {
NavigationSplitView {
sidebarView
} detail : {
editorView
}
2026-03-26 18:19:45 +00:00
. navigationSplitViewColumnWidth ( min : 200 , ideal : 250 , max : 600 )
. background ( editorSurfaceBackgroundStyle )
2026-02-07 10:51:52 +00:00
} else {
editorView
}
}
}
. frame ( maxWidth : . infinity , maxHeight : . infinity , alignment : . topLeading )
#endif
}
2026-02-19 08:09:35 +00:00
// L a y o u t : N a v i g a t i o n S p l i t V i e w w i t h o p t i o n a l s i d e b a r a n d t h e p r i m a r y c o d e e d i t o r .
var body : some View {
2026-02-25 13:07:05 +00:00
lifecycleConfiguredRootView
#if os ( macOS )
. background (
WindowAccessor { window in
updateWindowRegistration ( window )
}
. frame ( width : 0 , height : 0 )
)
2026-03-30 17:07:02 +00:00
. background (
FindReplaceWindowPresenter (
isPresented : $ showFindReplace ,
findQuery : $ findQuery ,
replaceQuery : $ replaceQuery ,
useRegex : $ findUsesRegex ,
caseSensitive : $ findCaseSensitive ,
matchCount : $ findMatchCount ,
statusMessage : $ findStatusMessage ,
onPreviewChanged : { refreshFindPreview ( ) } ,
onFindNext : {
findNext ( )
refreshFindMatchCount ( )
} ,
onJumpToMatch : { jumpToCurrentFindMatch ( ) } ,
onReplace : {
replaceSelection ( )
refreshFindPreview ( )
} ,
onReplaceAll : {
replaceAll ( )
refreshFindPreview ( )
} ,
onClose : { showFindReplace = false }
)
. frame ( width : 0 , height : 0 )
)
2026-02-25 13:07:05 +00:00
. onDisappear {
handleWindowDisappear ( )
2026-02-19 08:09:35 +00:00
}
2026-03-13 14:13:44 +00:00
. onChange ( of : viewModel . tabsObservationToken ) { _ , _ in
updateWindowChrome ( )
}
. onChange ( of : largeFileOpenModeRaw ) { _ , _ in
updateWindowChrome ( )
}
2026-03-28 19:13:32 +00:00
. onChange ( of : remoteSessionsEnabled ) { _ , _ in
updateWindowChrome ( )
}
. onChange ( of : remotePreparedTarget ) { _ , _ in
updateWindowChrome ( )
}
2026-02-20 02:41:08 +00:00
#endif
2026-02-25 13:07:05 +00:00
}
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 ? ? " " )
2026-02-18 22:56:46 +00:00
}
2026-03-17 17:40:32 +00:00
. alert (
" PDF Export Failed " ,
isPresented : Binding (
get : { markdownPDFExportErrorMessage != nil } ,
set : { if ! $0 { markdownPDFExportErrorMessage = nil } }
)
) {
Button ( " OK " , role : . cancel ) { }
} message : {
Text ( markdownPDFExportErrorMessage ? ? " " )
}
2026-02-25 13:07:05 +00:00
. navigationTitle ( " Neon Vision Editor " )
2026-02-19 14:29:53 +00:00
#if os ( iOS )
2026-02-25 13:07:05 +00:00
. navigationBarTitleDisplayMode ( . inline )
2026-03-03 09:47:45 +00:00
. background (
IPadKeyboardShortcutBridge (
onNewTab : { viewModel . addNewTab ( ) } ,
onOpenFile : { openFileFromToolbar ( ) } ,
onSave : { saveCurrentTabFromToolbar ( ) } ,
onFind : { showFindReplace = true } ,
onFindInFiles : { showFindInFiles = true } ,
onQuickOpen : {
quickSwitcherQuery = " "
showQuickSwitcher = true
2026-03-30 17:07:02 +00:00
} ,
onToggleSidebar : { toggleSidebarFromToolbar ( ) } ,
onToggleProjectSidebar : { toggleProjectSidebarFromToolbar ( ) }
2026-03-03 09:47:45 +00:00
)
. frame ( width : 0 , height : 0 )
)
2026-02-19 14:29:53 +00:00
#endif
2026-02-25 13:07:05 +00:00
}
2026-03-17 17:40:32 +00:00
private var rootViewWithStateObservers : some View {
2026-03-30 18:38:51 +00:00
applyUpdateVisibilityObservers ( to : basePlatformRootView )
2026-02-25 13:07:05 +00:00
. onAppear {
handleSettingsAndEditorDefaultsOnAppear ( )
2026-02-19 14:29:53 +00:00
}
2026-02-25 13:07:05 +00:00
. onChange ( of : settingsLineWrapEnabled ) { _ , enabled in
2026-03-03 09:47:45 +00:00
let target = projectOverrideLineWrapEnabled ? ? enabled
if viewModel . isLineWrapEnabled != target {
viewModel . isLineWrapEnabled = target
2026-02-25 13:07:05 +00:00
}
2026-02-18 22:56:46 +00:00
}
2026-02-25 13:07:05 +00:00
. onChange ( of : viewModel . isLineWrapEnabled ) { _ , enabled in
2026-03-03 09:47:45 +00:00
guard projectOverrideLineWrapEnabled = = nil else { return }
2026-02-25 13:07:05 +00:00
if settingsLineWrapEnabled != enabled {
settingsLineWrapEnabled = enabled
}
2026-02-18 22:56:46 +00:00
}
2026-02-25 13:07:05 +00:00
. onChange ( of : settingsThemeName ) { _ , _ in
scheduleHighlightRefresh ( )
}
2026-03-17 17:40:32 +00:00
. onChange ( of : themeFormattingRefreshSignature ) { _ , _ in
scheduleHighlightRefresh ( )
}
2026-02-25 13:07:05 +00:00
. 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 ( )
}
2026-03-08 12:49:48 +00:00
. onChange ( of : viewModel . showSidebar ) { _ , _ in
persistSessionIfReady ( )
}
. onChange ( of : showProjectStructureSidebar ) { _ , _ in
persistSessionIfReady ( )
}
2026-03-08 14:31:01 +00:00
. onChange ( of : showSupportedProjectFilesOnly ) { _ , _ in
2026-03-17 17:40:32 +00:00
refreshProjectBrowserState ( )
2026-03-08 14:31:01 +00:00
}
2026-03-08 12:49:48 +00:00
. onChange ( of : showMarkdownPreviewPane ) { _ , _ in
persistSessionIfReady ( )
}
2026-03-30 18:38:51 +00:00
}
private func applyUpdateVisibilityObservers < Content : View > ( to view : Content ) -> some View {
view
. 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 : appUpdateManager . automaticPromptToken ) { _ , _ in
if appUpdateManager . consumeAutomaticPromptIfNeeded ( ) {
showUpdaterDialog ( checkNow : false )
}
}
. onChange ( of : appUpdateManager . isInstalling ) { _ , isInstalling in
if isInstalling && ! showUpdateDialog {
showUpdaterDialog ( checkNow : false )
}
}
. onChange ( of : appUpdateManager . awaitingInstallCompletionAction ) { _ , awaitingAction in
if awaitingAction && ! showUpdateDialog {
showUpdaterDialog ( checkNow : false )
}
}
2026-03-17 17:40:32 +00:00
}
private var rootViewWithPlatformLifecycleObservers : some View {
rootViewWithStateObservers
2026-03-08 12:49:48 +00:00
#if os ( iOS )
. onReceive ( NotificationCenter . default . publisher ( for : UIApplication . didBecomeActiveNotification ) ) { _ in
2026-03-17 17:40:32 +00:00
handleAppDidBecomeActive ( )
2026-03-08 12:49:48 +00:00
}
#elseif os ( macOS )
. onReceive ( NotificationCenter . default . publisher ( for : NSApplication . didBecomeActiveNotification ) ) { _ in
2026-03-17 17:40:32 +00:00
handleAppDidBecomeActive ( )
2026-03-08 12:49:48 +00:00
}
2026-03-16 10:20:48 +00:00
. onReceive ( NotificationCenter . default . publisher ( for : NSApplication . willResignActiveNotification ) ) { _ in
2026-03-17 17:40:32 +00:00
handleAppWillResignActive ( )
2026-03-16 10:20:48 +00:00
}
. onReceive ( NotificationCenter . default . publisher ( for : NSApplication . willTerminateNotification ) ) { _ in
2026-03-17 17:40:32 +00:00
handleAppWillResignActive ( )
2026-03-16 10:20:48 +00:00
}
2026-03-08 12:49:48 +00:00
#endif
2026-03-17 17:40:32 +00:00
}
private var lifecycleConfiguredRootView : some View {
rootViewWithPlatformLifecycleObservers
2026-02-25 13:07:05 +00:00
. 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 ( ) {
2026-02-25 13:14:38 +00:00
let defaults = UserDefaults . standard
2026-03-03 09:47:45 +00:00
if let saved = defaults . stringArray ( forKey : quickSwitcherRecentsDefaultsKey ) {
quickSwitcherRecentItemIDs = saved
}
2026-02-25 13:07:05 +00:00
if UserDefaults . standard . object ( forKey : " SettingsAutoIndent " ) = = nil {
autoIndentEnabled = true
2026-02-23 08:02:15 +00:00
}
2026-02-22 13:31:05 +00:00
#if os ( iOS )
2026-02-25 13:14:38 +00:00
if defaults . object ( forKey : " SettingsShowKeyboardAccessoryBarIOS " ) = = nil {
2026-02-25 13:07:05 +00:00
showKeyboardAccessoryBarIOS = false
2026-02-19 08:09:35 +00:00
}
2026-02-22 13:31:05 +00:00
#endif
2026-02-25 13:07:05 +00:00
#if os ( macOS )
2026-02-25 13:14:38 +00:00
if defaults . object ( forKey : " ShowBracketHelperBarMac " ) = = nil {
2026-02-25 13:07:05 +00:00
showBracketHelperBarMac = false
}
#endif
2026-02-25 13:14:38 +00:00
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 " )
}
2026-03-03 09:47:45 +00:00
viewModel . isLineWrapEnabled = effectiveLineWrapEnabled
2026-02-25 13:07:05 +00:00
syncAppleCompletionAvailability ( )
}
2026-02-12 22:20:39 +00:00
2026-02-25 13:07:05 +00:00
private func handleStartupOnAppear ( ) {
2026-03-03 09:47:45 +00:00
EditorPerformanceMonitor . shared . markFirstPaint ( )
2026-02-25 13:07:05 +00:00
if ! didRunInitialWindowLayoutSetup {
// S t a r t w i t h s i d e b a r s c o l l a p s e d o n l y o n c e ; o t h e r w i s e t o g g l e s c a n g e t r e s e t o n l a y o u t t r a n s i t i o n s .
viewModel . showSidebar = false
showProjectStructureSidebar = false
2026-03-30 17:07:02 +00:00
#if os ( iOS )
2026-04-16 10:37:03 +00:00
if UIDevice . current . userInterfaceIdiom = = . pad && projectSidebarWidth < Double ( minimumProjectSidebarWidth ) {
projectSidebarWidth = Double ( minimumProjectSidebarWidth )
2026-03-30 17:07:02 +00:00
}
#endif
2026-02-25 13:07:05 +00:00
didRunInitialWindowLayoutSetup = true
}
applyStartupBehaviorIfNeeded ( )
2026-02-18 22:56:46 +00:00
2026-02-25 13:07:05 +00:00
// K e e p i O S t a b / e d i t o r l a y o u t s t a b l e b y f o r c i n g B r a i n D u m p o f f o n m o b i l e .
2026-02-20 10:27:55 +00:00
#if os ( iOS )
2026-02-25 13:07:05 +00:00
viewModel . isBrainDumpMode = false
UserDefaults . standard . set ( false , forKey : " BrainDumpModeEnabled " )
2026-02-20 10:27:55 +00:00
#else
2026-02-25 13:07:05 +00:00
if UserDefaults . standard . object ( forKey : " BrainDumpModeEnabled " ) != nil {
viewModel . isBrainDumpMode = UserDefaults . standard . bool ( forKey : " BrainDumpModeEnabled " )
}
2026-02-20 10:27:55 +00:00
#endif
2026-02-06 13:29:34 +00:00
2026-02-25 13:07:05 +00:00
applyWindowTranslucency ( enableTranslucentWindow )
if ! hasSeenWelcomeTourV1 || welcomeTourSeenRelease != WelcomeTourView . releaseID {
DispatchQueue . main . asyncAfter ( deadline : . now ( ) + 0.2 ) {
showWelcomeTour = true
2026-02-08 00:06:06 +00:00
}
2026-02-19 08:09:35 +00:00
}
2026-03-09 19:39:32 +00:00
if appLaunchCount >= 5 && ! hasShownSupportPromptV1 {
DispatchQueue . main . asyncAfter ( deadline : . now ( ) + 0.8 ) {
guard ! showWelcomeTour , ! hasShownSupportPromptV1 else { return }
hasShownSupportPromptV1 = true
showSupportPromptSheet = true
}
}
2026-02-25 13:07:05 +00:00
}
2026-02-18 22:56:46 +00:00
#if os ( macOS )
2026-02-25 13:07:05 +00:00
private func handleWindowDisappear ( ) {
completionDebounceTask ? . cancel ( )
completionTask ? . cancel ( )
lastCompletionTriggerSignature = " "
pendingHighlightRefresh ? . cancel ( )
2026-03-03 09:47:45 +00:00
pendingLargeFileModeReevaluation ? . cancel ( )
2026-02-25 13:07:05 +00:00
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 )
2026-02-19 08:09:35 +00:00
}
2025-09-25 09:01:45 +00:00
}
2026-02-25 13:07:05 +00:00
#endif
2026-01-17 11:11:26 +00:00
2026-02-18 19:19:49 +00:00
private func scheduleHighlightRefresh ( delay : TimeInterval = 0.05 ) {
pendingHighlightRefresh ? . cancel ( )
let work = DispatchWorkItem {
highlightRefreshToken &+= 1
}
pendingHighlightRefresh = work
2026-02-19 08:09:35 +00:00
DispatchQueue . main . asyncAfter ( deadline : . now ( ) + delay , execute : work )
2026-02-18 19:19:49 +00:00
}
#if ! os ( macOS )
private func shouldThrottleHeavyEditorFeatures ( in nsText : NSString ? = nil ) -> Bool {
2026-03-13 14:13:44 +00:00
if effectiveLargeFileModeEnabled { return true }
2026-02-20 22:04:03 +00:00
let length = nsText ? . length ? ? currentDocumentUTF16Length
2026-02-20 17:04:36 +00:00
return length >= EditorPerformanceThresholds . heavyFeatureUTF16Length
2026-02-18 19:19:49 +00:00
}
#endif
2026-02-14 13:24:01 +00:00
private struct ModalPresentationModifier : ViewModifier {
let contentView : ContentView
2026-03-30 17:19:09 +00:00
#if os ( iOS )
private var isiPhone : Bool {
UIDevice . current . userInterfaceIdiom = = . phone
}
private var findReplaceSheetMaxWidth : CGFloat ? {
isiPhone ? nil : 460
}
private var findReplaceSheetDetents : Set < PresentationDetent > {
isiPhone ? [ . height ( 448 ) , . medium ] : [ . height ( 560 ) ]
}
private var findInFilesSheetDetents : Set < PresentationDetent > {
isiPhone ? [ . height ( 540 ) , . medium ] : [ . height ( 700 ) , . large ]
}
@ ViewBuilder
private var findReplaceSheetContent : some View {
FindReplacePanel (
findQuery : contentView . $ findQuery ,
replaceQuery : contentView . $ replaceQuery ,
useRegex : contentView . $ findUsesRegex ,
caseSensitive : contentView . $ findCaseSensitive ,
matchCount : contentView . $ findMatchCount ,
statusMessage : contentView . $ findStatusMessage ,
onPreviewChanged : { contentView . refreshFindPreview ( ) } ,
onFindNext : {
contentView . findNext ( )
contentView . refreshFindMatchCount ( )
} ,
onJumpToMatch : { contentView . jumpToCurrentFindMatch ( ) } ,
onReplace : {
contentView . replaceSelection ( )
contentView . refreshFindPreview ( )
} ,
onReplaceAll : {
contentView . replaceAll ( )
contentView . refreshFindPreview ( )
} ,
onClose : { contentView . showFindReplace = false }
)
. frame ( maxWidth : findReplaceSheetMaxWidth )
. presentationDetents ( findReplaceSheetDetents )
. presentationDragIndicator ( . visible )
. presentationContentInteraction ( . scrolls )
}
@ ViewBuilder
private var findInFilesSheetContent : some View {
FindInFilesPanel (
query : contentView . $ findInFilesQuery ,
caseSensitive : contentView . $ findInFilesCaseSensitive ,
results : contentView . findInFilesResults ,
statusMessage : contentView . findInFilesStatusMessage ,
sourceMessage : contentView . findInFilesSourceMessage ,
onSearch : { contentView . startFindInFiles ( ) } ,
onClear : { contentView . clearFindInFiles ( ) } ,
onSelect : { contentView . selectFindInFilesMatch ( $0 ) } ,
onClose : { contentView . showFindInFiles = false }
)
. presentationDetents ( findInFilesSheetDetents )
. presentationDragIndicator ( . visible )
. presentationContentInteraction ( . scrolls )
}
#endif
2026-03-30 17:07:02 +00:00
#if ! os ( macOS )
2026-03-30 17:35:58 +00:00
private func applyingFindReplaceSheet ( to view : AnyView ) -> AnyView {
AnyView ( view . sheet ( isPresented : contentView . $ showFindReplace ) {
2026-03-30 17:19:09 +00:00
#if canImport ( UIKit )
2026-03-30 17:26:31 +00:00
findReplaceSheetContent
2026-03-30 17:19:09 +00:00
#else
2026-03-30 17:26:31 +00:00
FindReplacePanel (
findQuery : contentView . $ findQuery ,
replaceQuery : contentView . $ replaceQuery ,
useRegex : contentView . $ findUsesRegex ,
caseSensitive : contentView . $ findCaseSensitive ,
matchCount : contentView . $ findMatchCount ,
statusMessage : contentView . $ findStatusMessage ,
onPreviewChanged : { contentView . refreshFindPreview ( ) } ,
onFindNext : {
contentView . findNext ( )
contentView . refreshFindMatchCount ( )
} ,
onJumpToMatch : { contentView . jumpToCurrentFindMatch ( ) } ,
onReplace : {
contentView . replaceSelection ( )
contentView . refreshFindPreview ( )
} ,
onReplaceAll : {
contentView . replaceAll ( )
contentView . refreshFindPreview ( )
} ,
onClose : { contentView . showFindReplace = false }
)
. frame ( width : 420 )
2026-02-14 13:24:01 +00:00
#endif
2026-03-30 17:35:58 +00:00
} )
2026-03-30 17:26:31 +00:00
}
2026-03-30 17:07:02 +00:00
#endif
2026-03-30 17:26:31 +00:00
2026-02-14 13:24:01 +00:00
#if canImport ( UIKit )
2026-03-30 17:35:58 +00:00
private func applyingSettingsSheet ( to view : AnyView ) -> AnyView {
AnyView ( view . sheet ( isPresented : contentView . $ showSettingsSheet ) {
2026-03-30 17:26:31 +00:00
ConfiguredSettingsView (
supportsOpenInTabs : false ,
supportsTranslucency : false ,
editorViewModel : contentView . viewModel ,
supportPurchaseManager : contentView . supportPurchaseManager ,
appUpdateManager : contentView . appUpdateManager
)
2026-02-14 13:24:01 +00:00
#if os ( iOS )
2026-03-30 17:26:31 +00:00
. presentationDetents ( contentView . settingsSheetDetents )
. presentationDragIndicator ( . visible )
. presentationContentInteraction ( . scrolls )
#endif
2026-03-30 17:35:58 +00:00
} )
2026-03-30 17:26:31 +00:00
}
2026-03-30 17:35:58 +00:00
private func applyingProjectFolderPickerSheet ( to view : AnyView ) -> AnyView {
AnyView ( view . sheet ( isPresented : contentView . $ showProjectFolderPicker ) {
2026-03-30 17:26:31 +00:00
ProjectFolderPicker (
onPick : { url in
contentView . setProjectFolder ( url )
contentView . $ showProjectFolderPicker . wrappedValue = false
} ,
onCancel : { contentView . $ showProjectFolderPicker . wrappedValue = false }
)
2026-03-30 17:35:58 +00:00
} )
2026-03-30 17:26:31 +00:00
}
#endif
2026-03-30 17:35:58 +00:00
private func applyingQuickSwitcherSheet ( to view : AnyView ) -> AnyView {
AnyView ( view . sheet ( isPresented : contentView . $ showQuickSwitcher ) {
2026-03-30 17:26:31 +00:00
QuickFileSwitcherPanel (
query : contentView . $ quickSwitcherQuery ,
items : contentView . quickSwitcherItems ,
statusMessage : contentView . quickSwitcherStatusMessage ,
onSelect : { contentView . selectQuickSwitcherItem ( $0 ) } ,
onTogglePin : { contentView . toggleQuickSwitcherPin ( $0 ) }
)
2026-03-30 17:35:58 +00:00
} )
2026-03-30 17:26:31 +00:00
}
2026-03-30 17:35:58 +00:00
private func applyingCodeSnapshotSheet ( to view : AnyView ) -> AnyView {
AnyView ( view . sheet ( item : contentView . $ codeSnapshotPayload ) { payload in
2026-03-30 17:26:31 +00:00
CodeSnapshotComposerView ( payload : payload )
2026-03-30 17:35:58 +00:00
} )
2026-03-30 17:26:31 +00:00
}
2026-03-30 17:35:58 +00:00
private func applyingFindInFilesSheet ( to view : AnyView ) -> AnyView {
AnyView ( view . sheet ( isPresented : contentView . $ showFindInFiles ) {
2026-03-30 17:26:31 +00:00
#if os ( iOS )
findInFilesSheetContent
#else
FindInFilesPanel (
query : contentView . $ findInFilesQuery ,
caseSensitive : contentView . $ findInFilesCaseSensitive ,
results : contentView . findInFilesResults ,
statusMessage : contentView . findInFilesStatusMessage ,
sourceMessage : contentView . findInFilesSourceMessage ,
onSearch : { contentView . startFindInFiles ( ) } ,
onClear : { contentView . clearFindInFiles ( ) } ,
onSelect : { contentView . selectFindInFilesMatch ( $0 ) } ,
onClose : { contentView . showFindInFiles = false }
)
2026-02-14 13:24:01 +00:00
#endif
2026-03-30 17:35:58 +00:00
} )
2026-03-30 17:26:31 +00:00
}
2026-03-30 17:35:58 +00:00
private func applyingLanguageSheets ( to view : AnyView ) -> AnyView {
AnyView (
view
2026-03-30 17:26:31 +00:00
. sheet ( isPresented : contentView . $ showLanguageSetupPrompt ) {
contentView . languageSetupSheet
2026-02-14 13:24:01 +00:00
}
2026-03-30 17:26:31 +00:00
. sheet ( isPresented : contentView . $ showLanguageSearchSheet ) {
contentView . languageSearchSheet
}
2026-03-30 17:35:58 +00:00
)
2026-03-30 17:26:31 +00:00
}
2026-02-14 13:24:01 +00:00
#if os ( iOS )
2026-03-30 17:35:58 +00:00
private func applyingCompactIOSSheets ( to view : AnyView ) -> AnyView {
AnyView (
view
2026-02-14 13:24:01 +00:00
. sheet ( isPresented : contentView . $ showCompactSidebarSheet ) {
NavigationStack {
2026-02-19 14:29:53 +00:00
SidebarView (
content : contentView . currentContent ,
language : contentView . currentLanguage ,
translucentBackgroundEnabled : false
)
2026-03-30 17:07:02 +00:00
. navigationTitle ( Text ( NSLocalizedString ( " Sidebar " , comment : " " ) ) )
2026-02-14 13:24:01 +00:00
. toolbar {
ToolbarItem ( placement : . topBarTrailing ) {
2026-03-30 17:07:02 +00:00
Button ( NSLocalizedString ( " Done " , comment : " " ) ) {
2026-02-14 13:24:01 +00:00
contentView . $ showCompactSidebarSheet . wrappedValue = false
}
}
}
}
. presentationDetents ( [ . medium , . large ] )
}
2026-02-20 01:16:58 +00:00
. sheet ( isPresented : contentView . $ showCompactProjectSidebarSheet ) {
NavigationStack {
ProjectStructureSidebarView (
rootFolderURL : contentView . projectRootFolderURL ,
nodes : contentView . projectTreeNodes ,
selectedFileURL : contentView . viewModel . selectedTab ? . fileURL ,
2026-03-08 14:31:01 +00:00
showSupportedFilesOnly : contentView . showSupportedProjectFilesOnly ,
2026-02-20 01:16:58 +00:00
translucentBackgroundEnabled : false ,
2026-03-29 13:57:01 +00:00
boundaryEdge : nil ,
2026-02-20 01:16:58 +00:00
onOpenFile : { contentView . openFileFromToolbar ( ) } ,
onOpenFolder : { contentView . openProjectFolder ( ) } ,
2026-03-08 14:31:01 +00:00
onToggleSupportedFilesOnly : { contentView . showSupportedProjectFilesOnly = $0 } ,
2026-02-20 01:16:58 +00:00
onOpenProjectFile : { contentView . openProjectFile ( url : $0 ) } ,
2026-04-16 10:37:03 +00:00
onRefreshTree : { contentView . refreshProjectBrowserState ( ) } ,
onCreateProjectFile : { contentView . startProjectItemCreation ( kind : . file , in : $0 ) } ,
onCreateProjectFolder : { contentView . startProjectItemCreation ( kind : . folder , in : $0 ) } ,
onRenameProjectItem : { contentView . startProjectItemRename ( $0 ) } ,
onDuplicateProjectItem : { contentView . duplicateProjectItem ( $0 ) } ,
onDeleteProjectItem : { contentView . requestDeleteProjectItem ( $0 ) } ,
revealURL : contentView . projectTreeRevealURL
2026-02-20 01:16:58 +00:00
)
2026-03-30 17:07:02 +00:00
. navigationTitle ( Text ( NSLocalizedString ( " Project Structure " , comment : " " ) ) )
2026-02-20 01:16:58 +00:00
. toolbar {
ToolbarItem ( placement : . topBarTrailing ) {
2026-03-30 17:07:02 +00:00
Button ( NSLocalizedString ( " Done " , comment : " " ) ) {
2026-02-20 01:16:58 +00:00
contentView . $ showCompactProjectSidebarSheet . wrappedValue = false
}
}
}
}
. presentationDetents ( [ . medium , . large ] )
}
2026-03-08 14:31:01 +00:00
. sheet ( isPresented : contentView . markdownPreviewSheetPresentationBinding ) {
NavigationStack {
contentView . markdownPreviewPane
2026-03-30 17:07:02 +00:00
. navigationTitle ( Text ( NSLocalizedString ( " Markdown Preview " , comment : " " ) ) )
2026-03-08 14:31:01 +00:00
. navigationBarTitleDisplayMode ( . inline )
. toolbar {
ToolbarItem ( placement : . topBarTrailing ) {
2026-03-30 17:07:02 +00:00
Button ( NSLocalizedString ( " Done " , comment : " " ) ) {
2026-03-08 14:31:01 +00:00
contentView . showMarkdownPreviewPane = false
}
}
}
}
. presentationDetents ( [ . fraction ( 0.35 ) , . medium , . large ] , selection : contentView . $ markdownPreviewSheetDetent )
. presentationDragIndicator ( . visible )
. presentationContentInteraction ( . scrolls )
}
2026-03-30 17:26:31 +00:00
)
2026-03-30 17:35:58 +00:00
}
#endif
func body ( content : Content ) -> some View {
let baseContent = AnyView ( content )
#if ! os ( macOS )
let withFindReplace = applyingFindReplaceSheet ( to : baseContent )
#else
let withFindReplace = baseContent
#endif
#if canImport ( UIKit )
let withSettings = applyingSettingsSheet ( to : withFindReplace )
#else
let withSettings = withFindReplace
#endif
#if os ( iOS )
let withCompactSheets = applyingCompactIOSSheets ( to : withSettings )
2026-03-30 17:26:31 +00:00
#else
let withCompactSheets = withSettings
2026-02-14 13:24:01 +00:00
#endif
#if canImport ( UIKit )
2026-03-30 17:35:58 +00:00
let withProjectPicker = applyingProjectFolderPickerSheet ( to : withCompactSheets )
2026-03-30 17:19:09 +00:00
#else
2026-03-30 17:26:31 +00:00
let withProjectPicker = AnyView ( withCompactSheets )
2026-03-30 17:07:02 +00:00
#endif
2026-03-30 17:35:58 +00:00
let withQuickSwitcher = applyingQuickSwitcherSheet ( to : withProjectPicker )
let withCodeSnapshot = applyingCodeSnapshotSheet ( to : withQuickSwitcher )
let withFindInFiles = applyingFindInFilesSheet ( to : withCodeSnapshot )
let modalRoot = applyingLanguageSheets ( to : withFindInFiles )
2026-03-30 17:26:31 +00:00
modalRoot
2026-02-21 19:12:47 +00:00
#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
2026-02-14 13:24:01 +00:00
. sheet ( isPresented : contentView . $ showWelcomeTour ) {
WelcomeTourView {
contentView . $ hasSeenWelcomeTourV1 . wrappedValue = true
contentView . $ welcomeTourSeenRelease . wrappedValue = WelcomeTourView . releaseID
contentView . $ showWelcomeTour . wrappedValue = false
}
}
2026-02-21 19:12:47 +00:00
#endif
2026-03-09 19:39:32 +00:00
. sheet ( isPresented : contentView . $ showEditorHelp ) {
EditorHelpView {
contentView . $ showEditorHelp . wrappedValue = false
}
}
. sheet ( isPresented : contentView . $ showSupportPromptSheet ) {
SupportPromptSheetView {
contentView . $ showSupportPromptSheet . wrappedValue = false
}
. environmentObject ( contentView . supportPurchaseManager )
}
2026-02-14 13:24:01 +00:00
. 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. " )
}
}
2026-03-08 14:31:01 +00:00
. 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 ) { }
}
2026-03-08 12:49:48 +00:00
. 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. " )
}
}
2026-03-30 17:07:02 +00:00
. confirmationDialog (
contentView . viewModel . pendingRemoteSaveIssue ? . isConflict = = true
? " Remote file changed "
: ( contentView . viewModel . pendingRemoteSaveIssue ? . requiresReconnect = = true
? " Remote session unavailable "
: " Remote save failed " ) ,
isPresented : contentView . $ showRemoteSaveIssueDialog ,
titleVisibility : . visible
) {
if let issue = contentView . viewModel . pendingRemoteSaveIssue {
if issue . isConflict {
Button ( " Compare " ) {
Task {
if let snapshot = await contentView . viewModel . remoteConflictComparisonSnapshot ( tabID : issue . tabID ) {
await MainActor . run {
contentView . remoteConflictCompareSnapshot = snapshot
contentView . showRemoteConflictCompareSheet = true
}
}
}
}
Button ( " Reload from Remote " , role : . destructive ) {
contentView . viewModel . reloadRemoteDocumentAfterConflict ( tabID : issue . tabID )
}
} else if issue . requiresReconnect {
Button ( " Detach Broker " , role : . destructive ) {
contentView . viewModel . detachRemoteBrokerAfterSaveIssue ( )
}
} else {
Button ( " Try Save Again " ) {
contentView . viewModel . retryRemoteSave ( tabID : issue . tabID )
}
}
}
Button ( " Dismiss " , role : . cancel ) {
contentView . viewModel . dismissRemoteSaveIssue ( )
}
} message : {
if let issue = contentView . viewModel . pendingRemoteSaveIssue {
Text ( issue . requiresReconnect ? issue . recoveryGuidance : issue . detail )
} else {
Text ( " The remote document could not be saved. " )
}
}
2026-03-08 12:49:48 +00:00
. 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 " )
}
}
}
2026-03-30 17:07:02 +00:00
. sheet ( isPresented : contentView . $ showRemoteConflictCompareSheet , onDismiss : {
contentView . remoteConflictCompareSnapshot = nil
} ) {
if let snapshot = contentView . remoteConflictCompareSnapshot {
NavigationStack {
VStack ( spacing : 12 ) {
Text ( " Compare Local vs Remote: \( 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 ( " Remote " )
. font ( . subheadline . weight ( . semibold ) )
TextEditor ( text : . constant ( snapshot . remoteContent ) )
. font ( . system ( . footnote , design : . monospaced ) )
. disabled ( true )
}
}
. frame ( maxHeight : . infinity )
HStack {
Button ( " Reload from Remote " , role : . destructive ) {
contentView . viewModel . reloadRemoteDocumentAfterConflict ( tabID : snapshot . tabID )
contentView . showRemoteConflictCompareSheet = false
}
Spacer ( )
Button ( " Close " ) {
contentView . showRemoteConflictCompareSheet = false
}
}
}
. padding ( 16 )
. navigationTitle ( " Remote Conflict " )
}
}
}
2026-02-14 13:24:01 +00:00
. 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. " )
}
2026-03-08 14:31:01 +00:00
. 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
) )
}
2026-04-16 10:37:03 +00:00
. alert ( contentView . projectItemCreationKind . title , isPresented : contentView . $ showProjectItemCreationPrompt ) {
TextField (
contentView . projectItemCreationKind . namePlaceholder ,
text : contentView . $ projectItemCreationNameDraft
)
Button ( " Create " ) { contentView . confirmProjectItemCreation ( ) }
Button ( " Cancel " , role : . cancel ) { contentView . cancelProjectItemCreation ( ) }
} message : {
Text ( NSLocalizedString ( " Choose a name for the new item. " , comment : " Project item creation prompt message " ) )
}
. alert ( NSLocalizedString ( " Rename Item " , comment : " Project item rename alert title " ) , isPresented : contentView . $ showProjectItemRenamePrompt ) {
TextField (
NSLocalizedString ( " Name " , comment : " Project item rename name field placeholder " ) ,
text : contentView . $ projectItemRenameNameDraft
)
Button ( " Rename " ) { contentView . confirmProjectItemRename ( ) }
Button ( " Cancel " , role : . cancel ) { contentView . cancelProjectItemRename ( ) }
} message : {
Text ( NSLocalizedString ( " Enter a new name. " , comment : " Project item rename prompt message " ) )
}
. confirmationDialog (
NSLocalizedString ( " Delete Item? " , comment : " Project item delete confirmation title " ) ,
isPresented : contentView . $ showProjectItemDeleteConfirmation ,
titleVisibility : . visible
) {
Button ( " Delete " , role : . destructive ) { contentView . confirmDeleteProjectItem ( ) }
Button ( " Cancel " , role : . cancel ) { contentView . cancelDeleteProjectItem ( ) }
} message : {
if ! contentView . projectItemDeleteTargetName . isEmpty {
Text (
String (
format : NSLocalizedString (
" This will permanently delete \" %@ \" . " ,
comment : " Project item delete confirmation message "
) ,
contentView . projectItemDeleteTargetName
)
)
}
}
. alert ( NSLocalizedString ( " Can’ t Complete Action " , comment : " Project item operation error alert title " ) , isPresented : contentView . $ showProjectItemOperationErrorAlert ) {
Button ( " OK " , role : . cancel ) { }
} message : {
Text ( contentView . projectItemOperationErrorMessage )
}
2026-02-14 13:24:01 +00:00
#if canImport ( UIKit )
. fileImporter (
isPresented : contentView . $ showIOSFileImporter ,
2026-02-20 23:05:45 +00:00
allowedContentTypes : [ . item ] ,
2026-02-20 16:24:27 +00:00
allowsMultipleSelection : true
2026-02-14 13:24:01 +00:00
) { result in
contentView . handleIOSImportResult ( result )
}
. fileExporter (
isPresented : contentView . $ showIOSFileExporter ,
document : contentView . iosExportDocument ,
contentType : . plainText ,
defaultFilename : contentView . iosExportFilename
) { result in
contentView . handleIOSExportResult ( result )
}
#endif
}
}
2026-02-07 10:51:52 +00:00
private var shouldUseSplitView : Bool {
#if os ( macOS )
2026-02-20 10:27:55 +00:00
return viewModel . showSidebar && ! brainDumpLayoutEnabled
2026-02-07 10:51:52 +00:00
#else
// K e e p i P h o n e l a y o u t s i n g l e - c o l u m n t o a v o i d h o r i z o n t a l c l i p p i n g .
2026-02-20 10:27:55 +00:00
return viewModel . showSidebar && ! brainDumpLayoutEnabled && horizontalSizeClass = = . regular
2026-02-07 10:51:52 +00:00
#endif
}
2026-03-17 17:40:32 +00:00
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
}
2026-03-03 09:47:45 +00:00
private var effectiveIndentWidth : Int {
projectOverrideIndentWidth ? ? indentWidth
}
private var effectiveLineWrapEnabled : Bool {
projectOverrideLineWrapEnabled ? ? settingsLineWrapEnabled
}
2026-02-12 22:20:39 +00:00
private func applyStartupBehaviorIfNeeded ( ) {
guard ! didApplyStartupBehavior else { return }
2026-03-17 17:40:32 +00:00
if startupBehavior = = . forceBlankDocument || startupBehavior = = . safeMode {
2026-02-28 18:26:00 +00:00
viewModel . resetTabsForSessionRestore ( )
viewModel . addNewTab ( )
projectRootFolderURL = nil
2026-03-03 09:47:45 +00:00
clearProjectEditorOverrides ( )
2026-02-28 18:26:00 +00:00
projectTreeNodes = [ ]
quickSwitcherProjectFileURLs = [ ]
2026-03-17 17:40:32 +00:00
stopProjectFolderObservation ( )
2026-03-26 18:19:45 +00:00
projectFileIndexSnapshot = . empty
2026-03-17 17:40:32 +00:00
isProjectFileIndexing = false
projectFileIndexTask ? . cancel ( )
projectFileIndexTask = nil
2026-02-28 18:26:00 +00:00
didApplyStartupBehavior = true
2026-03-17 17:40:32 +00:00
if startupBehavior != . safeMode {
persistSessionIfReady ( )
}
2026-02-28 18:26:00 +00:00
return
}
2026-02-27 17:13:12 +00:00
if viewModel . tabs . contains ( where : { $0 . fileURL != nil } ) {
2026-02-22 13:31:05 +00:00
didApplyStartupBehavior = true
persistSessionIfReady ( )
return
}
2026-03-16 10:20:48 +00:00
// I f b o t h s t a r t u p t o g g l e s a r e e n a b l e d ( l e g a c y / d e f a u l t m i s m a t c h ) , p r e f e r s e s s i o n r e s t o r e .
let shouldOpenBlankOnStartup = openWithBlankDocument && ! reopenLastSession
if shouldOpenBlankOnStartup {
2026-02-27 17:13:12 +00:00
viewModel . resetTabsForSessionRestore ( )
viewModel . addNewTab ( )
projectRootFolderURL = nil
2026-03-03 09:47:45 +00:00
clearProjectEditorOverrides ( )
2026-02-27 17:13:12 +00:00
projectTreeNodes = [ ]
quickSwitcherProjectFileURLs = [ ]
2026-03-17 17:40:32 +00:00
stopProjectFolderObservation ( )
2026-03-26 18:19:45 +00:00
projectFileIndexSnapshot = . empty
2026-03-17 17:40:32 +00:00
isProjectFileIndexing = false
projectFileIndexTask ? . cancel ( )
projectFileIndexTask = nil
2026-02-27 17:13:12 +00:00
didApplyStartupBehavior = true
persistSessionIfReady ( )
return
}
2026-03-17 17:40:32 +00:00
var restoredSessionTabs = false
2026-02-16 19:02:43 +00:00
// R e s t o r e l a s t s e s s i o n f i r s t w h e n e n a b l e d .
2026-02-12 22:20:39 +00:00
if reopenLastSession {
2026-02-25 15:19:58 +00:00
if projectRootFolderURL = = nil , let restoredProjectFolderURL = restoredLastSessionProjectFolderURL ( ) {
setProjectFolder ( restoredProjectFolderURL )
}
2026-02-22 12:38:31 +00:00
let urls = restoredLastSessionFileURLs ( )
let selectedURL = restoredLastSessionSelectedFileURL ( )
2026-02-12 22:20:39 +00:00
if ! urls . isEmpty {
2026-02-25 13:07:05 +00:00
viewModel . resetTabsForSessionRestore ( )
2026-02-12 22:20:39 +00:00
for url in urls {
viewModel . openFile ( url : url )
}
2026-02-22 12:38:31 +00:00
if let selectedURL {
2026-02-12 22:20:39 +00:00
_ = viewModel . focusTabIfOpen ( for : selectedURL )
}
2026-03-17 17:40:32 +00:00
restoredSessionTabs = ! viewModel . tabs . isEmpty
2026-02-12 22:20:39 +00:00
if viewModel . tabs . isEmpty {
viewModel . addNewTab ( )
}
}
}
2026-03-17 17:40:32 +00:00
// R e s t o r e u n s a v e d d r a f t s o n l y a s f a l l b a c k w h e n n o f i l e s e s s i o n t a b s w e r e r e s t o r e d .
if ! restoredSessionTabs , restoreUnsavedDraftSnapshotIfAvailable ( ) {
didApplyStartupBehavior = true
persistSessionIfReady ( )
return
}
2026-02-20 00:31:14 +00:00
#if os ( iOS )
// K e e p m o b i l e l a y o u t i n a v a l i d t a b s t a t e s o t h e f i l e t a b b a r a l w a y s h a s c o n t e n t .
if viewModel . tabs . isEmpty {
viewModel . addNewTab ( )
}
#endif
2026-03-08 12:49:48 +00:00
restoreLastSessionViewContextIfAvailable ( )
restoreCaretForSelectedSessionFileIfAvailable ( )
2026-02-12 22:20:39 +00:00
didApplyStartupBehavior = true
persistSessionIfReady ( )
}
2026-03-17 17:40:32 +00:00
func persistSessionIfReady ( ) {
2026-02-12 22:20:39 +00:00
guard didApplyStartupBehavior else { return }
2026-03-17 17:40:32 +00:00
guard startupBehavior != . safeMode else { return }
2026-02-22 12:38:31 +00:00
let fileURLs = viewModel . tabs . compactMap { $0 . fileURL }
UserDefaults . standard . set ( fileURLs . map ( \ . absoluteString ) , forKey : " LastSessionFileURLs " )
2026-02-12 22:20:39 +00:00
UserDefaults . standard . set ( viewModel . selectedTab ? . fileURL ? . absoluteString , forKey : " LastSessionSelectedFileURL " )
2026-03-08 12:49:48 +00:00
persistLastSessionViewContext ( )
2026-02-25 15:19:58 +00:00
persistLastSessionProjectFolderURL ( projectRootFolderURL )
2026-02-22 12:38:31 +00:00
#if os ( iOS )
persistLastSessionSecurityScopedBookmarks ( fileURLs : fileURLs , selectedURL : viewModel . selectedTab ? . fileURL )
2026-02-23 08:02:15 +00:00
#elseif os ( macOS )
persistLastSessionSecurityScopedBookmarksMac ( fileURLs : fileURLs , selectedURL : viewModel . selectedTab ? . fileURL )
2026-02-22 12:38:31 +00:00
#endif
}
private func restoredLastSessionFileURLs ( ) -> [ URL ] {
2026-02-23 08:02:15 +00:00
#if os ( macOS )
let bookmarked = restoreSessionURLsFromSecurityScopedBookmarksMac ( )
if ! bookmarked . isEmpty {
return bookmarked
}
#elseif os ( iOS )
2026-02-22 12:38:31 +00:00
let bookmarked = restoreSessionURLsFromSecurityScopedBookmarks ( )
if ! bookmarked . isEmpty {
return bookmarked
}
#endif
2026-02-23 08:02:15 +00:00
let stored = UserDefaults . standard . stringArray ( forKey : " LastSessionFileURLs " ) ? ? [ ]
var urls : [ URL ] = [ ]
var seen : Set < String > = [ ]
for raw in stored {
guard let parsed = restoredSessionURL ( from : raw ) else { continue }
let standardized = parsed . standardizedFileURL
// O n l y r e s t o r e f i l e s t h a t s t i l l e x i s t ; a v o i d s e m p t y p l a c e h o l d e r t a b s o n l a u n c h .
guard FileManager . default . fileExists ( atPath : standardized . path ) else { continue }
let key = standardized . absoluteString
if seen . insert ( key ) . inserted {
urls . append ( standardized )
}
}
return urls
2026-02-22 12:38:31 +00:00
}
private func restoredLastSessionSelectedFileURL ( ) -> URL ? {
2026-02-23 08:02:15 +00:00
#if os ( macOS )
if let bookmarked = restoreSelectedURLFromSecurityScopedBookmarkMac ( ) {
return bookmarked
}
#elseif os ( iOS )
2026-02-22 12:38:31 +00:00
if let bookmarked = restoreSelectedURLFromSecurityScopedBookmark ( ) {
return bookmarked
}
#endif
2026-02-23 08:02:15 +00:00
guard let selectedPath = UserDefaults . standard . string ( forKey : " LastSessionSelectedFileURL " ) ,
let selectedURL = restoredSessionURL ( from : selectedPath ) else {
2026-02-22 12:38:31 +00:00
return nil
}
2026-02-23 08:02:15 +00:00
let standardized = selectedURL . standardizedFileURL
return FileManager . default . fileExists ( atPath : standardized . path ) ? standardized : nil
}
private func restoredSessionURL ( from raw : String ) -> URL ? {
// S u p p o r t b o t h a b s o l u t e U R L s t r i n g s ( " f i l e : / / / . . . " ) a n d l e g a c y p l a i n p a t h s .
if let url = URL ( string : raw ) , url . isFileURL {
return url
}
if raw . hasPrefix ( " / " ) {
return URL ( fileURLWithPath : raw )
}
return nil
2026-02-22 12:38:31 +00:00
}
2026-03-08 12:49:48 +00:00
private var lastSessionShowSidebarKey : String { " LastSessionShowSidebarV1 " }
private var lastSessionShowProjectSidebarKey : String { " LastSessionShowProjectSidebarV1 " }
private var lastSessionShowMarkdownPreviewKey : String { " LastSessionShowMarkdownPreviewV1 " }
private var lastSessionCaretByFileURLKey : String { " LastSessionCaretByFileURLV1 " }
2026-02-25 15:19:58 +00:00
private var lastSessionProjectFolderURLKey : String { " LastSessionProjectFolderURL " }
2026-03-08 12:49:48 +00:00
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 ,
2026-03-30 17:07:02 +00:00
EditorCommandUserInfo . rangeLength : 0 ,
EditorCommandUserInfo . focusEditor : true
2026-03-08 12:49:48 +00:00
]
#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 )
}
}
2026-02-25 15:19:58 +00:00
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
}
2026-02-23 08:02:15 +00:00
#if os ( macOS )
private var macLastSessionBookmarksKey : String { " MacLastSessionFileBookmarks " }
private var macLastSessionSelectedBookmarkKey : String { " MacLastSessionSelectedFileBookmark " }
2026-02-25 15:19:58 +00:00
private var macLastSessionProjectFolderBookmarkKey : String { " MacLastSessionProjectFolderBookmark " }
2026-02-23 08:02:15 +00:00
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 < String > = [ ]
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
}
2026-02-25 15:19:58 +00:00
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
}
2026-02-23 08:02:15 +00:00
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
2026-03-03 09:47:45 +00:00
private var unsavedDraftSnapshotRegistryKey : String { " UnsavedDraftSnapshotRegistryV1 " }
private var unsavedDraftSnapshotKey : String { " UnsavedDraftSnapshotV2. \( recoverySnapshotIdentifier ) " }
2026-02-22 13:31:05 +00:00
private var maxPersistedDraftTabs : Int { 20 }
private var maxPersistedDraftUTF16Length : Int { 2_000_000 }
private func persistUnsavedDraftSnapshotIfNeeded ( ) {
2026-03-03 09:47:45 +00:00
let defaults = UserDefaults . standard
2026-02-22 13:31:05 +00:00
let dirtyTabs = viewModel . tabs . filter ( \ . isDirty )
2026-03-03 09:47:45 +00:00
var registry = defaults . stringArray ( forKey : unsavedDraftSnapshotRegistryKey ) ? ? [ ]
2026-02-22 13:31:05 +00:00
guard ! dirtyTabs . isEmpty else {
2026-03-03 09:47:45 +00:00
defaults . removeObject ( forKey : unsavedDraftSnapshotKey )
registry . removeAll { $0 = = unsavedDraftSnapshotKey }
defaults . set ( registry , forKey : unsavedDraftSnapshotRegistryKey )
2026-02-22 13:31:05 +00:00
return
}
2026-03-03 09:47:45 +00:00
var savedTabs : [ SavedDraftTabSnapshot ] = [ ]
2026-02-22 13:31:05 +00:00
savedTabs . reserveCapacity ( min ( dirtyTabs . count , maxPersistedDraftTabs ) )
for tab in dirtyTabs . prefix ( maxPersistedDraftTabs ) {
let content = tab . content
let nsContent = content as NSString
2026-03-03 09:47:45 +00:00
let clampedContent = nsContent . length > maxPersistedDraftUTF16Length
? nsContent . substring ( to : maxPersistedDraftUTF16Length )
: content
2026-02-22 13:31:05 +00:00
savedTabs . append (
2026-03-03 09:47:45 +00:00
SavedDraftTabSnapshot (
2026-02-22 13:31:05 +00:00
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 } )
} ( )
2026-03-03 09:47:45 +00:00
let snapshot = SavedDraftSnapshot ( tabs : savedTabs , selectedIndex : selectedIndex , createdAt : Date ( ) )
2026-02-22 13:31:05 +00:00
guard let encoded = try ? JSONEncoder ( ) . encode ( snapshot ) else { return }
2026-03-03 09:47:45 +00:00
defaults . set ( encoded , forKey : unsavedDraftSnapshotKey )
if ! registry . contains ( unsavedDraftSnapshotKey ) {
registry . append ( unsavedDraftSnapshotKey )
defaults . set ( registry , forKey : unsavedDraftSnapshotRegistryKey )
}
2026-02-22 13:31:05 +00:00
}
private func restoreUnsavedDraftSnapshotIfAvailable ( ) -> Bool {
2026-03-03 09:47:45 +00:00
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 )
2026-02-22 13:31:05 +00:00
}
2026-03-03 09:47:45 +00:00
guard ! snapshots . isEmpty else { return false }
2026-02-22 13:31:05 +00:00
2026-03-03 09:47:45 +00:00
snapshots . sort { $0 . createdAt < $1 . createdAt }
let mergedTabs = snapshots . flatMap ( \ . tabs )
guard ! mergedTabs . isEmpty else { return false }
let restoredTabs = mergedTabs . map { saved in
2026-02-25 13:07:05 +00:00
EditorViewModel . RestoredTabSnapshot (
2026-02-22 13:31:05 +00:00
name : saved . name ,
content : saved . content ,
language : saved . language ,
fileURL : saved . fileURLString . flatMap ( URL . init ( string : ) ) ,
languageLocked : true ,
isDirty : true ,
2026-03-08 12:49:48 +00:00
lastSavedFingerprint : nil ,
lastKnownFileModificationDate : nil
2026-02-22 13:31:05 +00:00
)
}
2026-03-03 09:47:45 +00:00
viewModel . restoreTabsFromSnapshot ( restoredTabs , selectedIndex : nil )
for key in keys {
defaults . removeObject ( forKey : key )
}
defaults . removeObject ( forKey : unsavedDraftSnapshotRegistryKey )
2026-02-22 13:31:05 +00:00
return true
}
2026-02-22 12:38:31 +00:00
2026-03-03 09:47:45 +00:00
#if os ( iOS )
private var lastSessionBookmarksKey : String { " LastSessionFileBookmarks " }
private var lastSessionSelectedBookmarkKey : String { " LastSessionSelectedFileBookmark " }
private var lastSessionProjectFolderBookmarkKey : String { " LastSessionProjectFolderBookmark " }
2026-02-22 12:38:31 +00:00
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 )
}
2026-02-12 22:20:39 +00:00
}
2026-02-22 12:38:31 +00:00
private func restoreSessionURLsFromSecurityScopedBookmarks ( ) -> [ URL ] {
guard let saved = UserDefaults . standard . array ( forKey : lastSessionBookmarksKey ) as ? [ Data ] , ! saved . isEmpty else {
return [ ]
}
var urls : [ URL ] = [ ]
var seen : Set < String > = [ ]
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 )
}
2026-02-25 15:19:58 +00:00
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
}
2026-02-22 12:38:31 +00:00
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
2026-01-25 12:46:33 +00:00
// S i d e b a r s h o w s a l i g h t w e i g h t t a b l e o f c o n t e n t s ( T O C ) d e r i v e d f r o m t h e c u r r e n t d o c u m e n t .
2025-09-25 09:01:45 +00:00
@ ViewBuilder
2026-02-06 18:59:53 +00:00
var sidebarView : some View {
2026-02-20 10:27:55 +00:00
if viewModel . showSidebar && ! brainDumpLayoutEnabled {
2026-02-19 14:29:53 +00:00
SidebarView (
2026-02-20 22:04:03 +00:00
content : sidebarTOCContent ,
2026-02-19 14:29:53 +00:00
language : currentLanguage ,
translucentBackgroundEnabled : enableTranslucentWindow
)
2026-01-17 11:11:26 +00:00
. frame ( minWidth : 200 , idealWidth : 250 , maxWidth : 600 )
2025-09-25 09:01:45 +00:00
. safeAreaInset ( edge : . bottom ) {
2026-03-26 18:19:45 +00:00
#if os ( iOS )
iOSHorizontalSurfaceDivider
#else
2025-09-25 09:01:45 +00:00
Divider ( )
2026-03-26 18:19:45 +00:00
#endif
2025-09-25 09:01:45 +00:00
}
2026-02-19 14:29:53 +00:00
. background ( editorSurfaceBackgroundStyle )
2026-01-25 19:02:59 +00:00
} else {
EmptyView ( )
2025-09-25 09:01:45 +00:00
}
}
2026-01-17 11:11:26 +00:00
2026-01-25 12:46:33 +00:00
// B i n d i n g s t h a t r e s o l v e t o t h e a c t i v e t a b ( i f p r e s e n t ) o r f a l l b a c k s i n g l e - d o c u m e n t s t a t e .
2026-02-06 18:59:53 +00:00
var currentContentBinding : Binding < String > {
2026-02-20 15:43:14 +00:00
if let selectedID = viewModel . selectedTabID ,
2026-02-25 13:07:05 +00:00
viewModel . selectedTab != nil {
2026-01-17 11:11:26 +00:00
return Binding (
2026-02-20 17:09:55 +00:00
get : {
2026-02-25 13:07:05 +00:00
viewModel . selectedTab ? . content ? ? singleContent
2026-02-20 17:09:55 +00:00
} ,
2026-02-20 15:43:14 +00:00
set : { newValue in
2026-03-29 10:39:47 +00:00
guard viewModel . selectedTab ? . isReadOnlyPreview != true else { return }
2026-02-25 13:07:05 +00:00
viewModel . updateTabContent ( tabID : selectedID , content : newValue )
2026-02-20 15:43:14 +00:00
}
2026-01-17 11:11:26 +00:00
)
} else {
return $ singleContent
}
}
2026-02-06 18:59:53 +00:00
var currentLanguageBinding : Binding < String > {
2026-02-25 13:07:05 +00:00
if let selectedID = viewModel . selectedTabID ,
viewModel . selectedTab != nil {
2026-01-17 11:11:26 +00:00
return Binding (
2026-02-20 17:09:55 +00:00
get : {
2026-02-25 13:07:05 +00:00
viewModel . selectedTab ? . language ? ? singleLanguage
2026-02-20 17:09:55 +00:00
} ,
set : { newValue in
2026-02-25 13:07:05 +00:00
viewModel . updateTabLanguage ( tabID : selectedID , language : newValue )
2026-02-20 17:09:55 +00:00
}
2026-01-17 11:11:26 +00:00
)
} else {
return $ singleLanguage
}
}
2026-02-09 10:21:50 +00:00
var currentLanguagePickerBinding : Binding < String > {
Binding (
get : { currentLanguageBinding . wrappedValue } ,
set : { newValue in
if let tab = viewModel . selectedTab {
2026-02-25 13:07:05 +00:00
viewModel . updateTabLanguage ( tabID : tab . id , language : newValue )
2026-02-09 10:21:50 +00:00
} else {
singleLanguage = newValue
}
}
)
}
2026-02-06 18:59:53 +00:00
var currentContent : String { currentContentBinding . wrappedValue }
var currentLanguage : String { currentLanguageBinding . wrappedValue }
2026-01-17 11:11:26 +00:00
2026-02-20 22:04:03 +00:00
private var currentDocumentUTF16Length : Int {
2026-02-25 13:07:05 +00:00
if let tab = viewModel . selectedTab {
2026-02-20 22:04:03 +00:00
return tab . contentUTF16Length
}
return ( singleContent as NSString ) . length
}
2026-03-13 14:13:44 +00:00
private var effectiveLargeFileModeEnabled : Bool {
if largeFileModeEnabled { return true }
if droppedFileLoadInProgress { return true }
if viewModel . selectedTab ? . isLargeFileCandidate = = true { return true }
return currentDocumentUTF16Length >= 300_000
}
2026-03-29 10:39:47 +00:00
private var isSelectedTabReadOnlyPreview : Bool {
viewModel . selectedTab ? . isReadOnlyPreview = = true
}
2026-03-13 14:13:44 +00:00
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 ) "
}
2026-03-28 19:13:32 +00:00
private var remoteSessionStatusBadgeText : String {
guard remoteSessionsEnabled else { return " " }
2026-03-30 17:07:02 +00:00
if remoteSessionStore . runtimeState = = . failed , remoteSessionStore . isBrokerClientAttached {
return " Local Workspace • Remote Broker Lost "
}
if remoteSessionStore . runtimeState = = . failed , remoteSessionStore . hasBrokerSession {
return " Local Workspace • Remote Broker Failed "
}
if remoteSessionStore . runtimeState = = . failed {
return " Local Workspace • Remote Failed "
}
2026-03-29 13:57:01 +00:00
if remoteSessionStore . isBrokerClientAttached {
return " Local Workspace • Remote Broker Attached "
}
if remoteSessionStore . hasBrokerSession {
return " Local Workspace • Remote Broker Active "
}
2026-03-28 19:35:14 +00:00
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 "
}
2026-03-28 19:13:32 +00:00
return remotePreparedTarget . trimmingCharacters ( in : . whitespacesAndNewlines ) . isEmpty
? " Local Workspace • Remote Enabled "
: " Local Workspace • Remote Ready "
}
2026-03-30 17:07:02 +00:00
private var remoteSessionBadgeForegroundColor : Color {
remoteSessionStore . runtimeState = = . failed ? . red : . secondary
}
private var remoteSessionBadgeBackgroundColor : Color {
remoteSessionStore . runtimeState = = . failed
? Color . red . opacity ( 0.16 )
: Color . secondary . opacity ( 0.16 )
}
private var remoteSessionBadgeAccessibilityValue : String {
if remoteSessionStore . runtimeState = = . failed , remoteSessionStore . isBrokerClientAttached {
return " Local workspace lost its attached remote broker session. Reattach from Settings using a fresh code. "
}
if remoteSessionStore . runtimeState = = . failed , remoteSessionStore . hasBrokerSession {
return " Local workspace lost the active macOS remote broker session. Restart the Mac session before attaching again. "
}
if remoteSessionStore . runtimeState = = . failed {
return " Local workspace remote session failed. "
}
return remoteSessionStore . isBrokerClientAttached
? " Local workspace attached to a remote broker for read-only browsing "
: (
remoteSessionStore . hasBrokerSession
? " Local workspace with an active remote broker session on macOS "
: (
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 "
)
)
)
)
)
}
2026-03-28 19:13:32 +00:00
private var windowSubtitleText : String {
[ largeFileStatusBadgeText , remoteSessionStatusBadgeText ]
. filter { ! $0 . isEmpty }
. joined ( separator : " • " )
}
2026-02-20 22:04:03 +00:00
private var sidebarTOCContent : String {
2026-03-13 14:13:44 +00:00
if effectiveLargeFileModeEnabled || currentDocumentUTF16Length >= 400_000 {
2026-02-20 22:04:03 +00:00
return " "
}
return currentContent
}
2026-03-17 17:40:32 +00:00
var brainDumpLayoutEnabled : Bool {
2026-02-20 10:27:55 +00:00
#if os ( macOS )
return viewModel . isBrainDumpMode
#else
return false
#endif
}
2026-02-11 10:20:17 +00:00
2026-02-09 10:21:50 +00:00
func toggleAutoCompletion ( ) {
let willEnable = ! isAutoCompletionEnabled
if willEnable && viewModel . isBrainDumpMode {
viewModel . isBrainDumpMode = false
UserDefaults . standard . set ( false , forKey : " BrainDumpModeEnabled " )
}
isAutoCompletionEnabled . toggle ( )
2026-02-18 18:59:25 +00:00
syncAppleCompletionAvailability ( )
2026-02-09 10:21:50 +00:00
if willEnable {
maybePromptForLanguageSetup ( )
}
}
private func maybePromptForLanguageSetup ( ) {
guard currentLanguage = = " plain " else { return }
languagePromptSelection = currentLanguage = = " plain " ? " plain " : currentLanguage
languagePromptInsertTemplate = false
showLanguageSetupPrompt = true
}
2026-02-18 18:59:25 +00:00
private func syncAppleCompletionAvailability ( ) {
#if USE_FOUNDATION_MODELS && canImport ( FoundationModels )
// K e e p A p p l e F o u n d a t i o n M o d e l s i n s y n c w i t h t h e c o m p l e t i o n m a s t e r t o g g l e .
AppleFM . isEnabled = isAutoCompletionEnabled
#endif
}
2026-02-09 10:21:50 +00:00
private func applyLanguageSelection ( language : String , insertTemplate : Bool ) {
let contentIsEmpty = currentContent . trimmingCharacters ( in : . whitespacesAndNewlines ) . isEmpty
if let tab = viewModel . selectedTab {
2026-02-25 13:07:05 +00:00
viewModel . updateTabLanguage ( tabID : tab . id , language : language )
2026-02-09 10:21:50 +00:00
if insertTemplate , contentIsEmpty , let template = starterTemplate ( for : language ) {
2026-02-25 13:07:05 +00:00
viewModel . updateTabContent ( tabID : tab . id , content : template )
2026-02-09 10:21:50 +00:00
}
} 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 )
}
2026-03-04 19:40:17 +00:00
var languageOptions : [ String ] {
2026-03-15 17:51:17 +00:00
[ " 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 " ]
2026-02-09 10:21:50 +00:00
}
2026-03-04 19:40:17 +00:00
func languageLabel ( for lang : String ) -> String {
2026-02-09 10:21:50 +00:00
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 "
2026-03-15 17:51:17 +00:00
case " tex " : return " TeX "
2026-02-09 10:21:50 +00:00
case " html " : return " HTML "
2026-02-13 11:02:39 +00:00
case " expressionengine " : return " ExpressionEngine "
2026-02-09 10:21:50 +00:00
case " css " : return " CSS "
case " standard " : return " Standard "
default : return lang . capitalized
}
}
2026-03-04 19:40:17 +00:00
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
}
2026-02-09 10:21:50 +00:00
private func starterTemplate ( for language : String ) -> String ? {
2026-02-11 10:20:17 +00:00
if let override = UserDefaults . standard . string ( forKey : templateOverrideKey ( for : language ) ) ,
! override . isEmpty {
return override
}
2026-02-09 10:21:50 +00:00
switch language {
case " swift " :
return " import Foundation \n \n // TODO: Add code here \n "
case " python " :
return " def main(): \n pass \n \n \n if __name__ == \" __main__ \" : \n main() \n "
case " javascript " :
return " \" use strict \" ; \n \n function main() { \n // TODO: Add code here \n } \n \n main(); \n "
case " typescript " :
return " function main(): void { \n // TODO: Add code here \n } \n \n main(); \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 \n import \" fmt \" \n \n func main() { \n fmt.Println( \" Hello \" ) \n } \n "
case " ruby " :
return " def main \n # TODO: Add code here \n end \n \n main \n "
case " rust " :
return " fn main() { \n // TODO: Add code here \n } \n "
case " php " :
return " <?php \n \n // TODO: Add code here \n "
case " cobol " :
return " IDENTIFICATION DIVISION. \n PROGRAM-ID. MAIN. \n \n PROCEDURE DIVISION. \n DISPLAY \" TODO \" . \n STOP RUN. \n "
case " dotenv " :
return " # TODO=VALUE \n "
case " proto " :
return " syntax = \" proto3 \" ; \n \n package example; \n \n message Example { \n string id = 1; \n } \n "
case " graphql " :
return " type Query { \n hello: String \n } \n "
case " rst " :
return " Title \n ===== \n \n Write here. \n "
case " nginx " :
return " server { \n listen 80; \n server_name example.com; \n \n location / { \n return 200 \" TODO \" ; \n } \n } \n "
case " c " :
return " #include <stdio.h> \n \n int main(void) { \n // TODO: Add code here \n return 0; \n } \n "
case " cpp " :
return " #include <iostream> \n \n int main() { \n // TODO: Add code here \n return 0; \n } \n "
case " csharp " :
return " using System; \n \n public class Program { \n public static void Main(string[] args) { \n // TODO: Add code here \n } \n } \n "
case " objective-c " :
return " #import <Foundation/Foundation.h> \n \n int main(int argc, const char * argv[]) { \n @autoreleasepool { \n // TODO: Add code here \n } \n return 0; \n } \n "
case " html " :
return " <!doctype html> \n <html lang= \" en \" > \n <head> \n <meta charset= \" utf-8 \" /> \n <meta name= \" viewport \" content= \" width=device-width, initial-scale=1 \" /> \n <title>Document</title> \n </head> \n <body> \n \n </body> \n </html> \n "
2026-02-13 11:02:39 +00:00
case " expressionengine " :
return " {exp:channel:entries channel= \" news \" limit= \" 10 \" } \n <article> \n <h2>{title}</h2> \n <p>{summary}</p> \n </article> \n {/exp:channel:entries} \n "
2026-02-09 10:21:50 +00:00
case " css " :
return " /* TODO: Add styles here */ \n \n body { \n margin: 0; \n } \n "
case " sql " :
return " -- TODO: Add queries here \n "
case " markdown " :
return " # Title \n \n Write here. \n "
2026-03-15 17:51:17 +00:00
case " tex " :
return " \\ documentclass{article} \n \\ usepackage[utf8]{inputenc} \n \n \\ begin{document} \n \\ section{Title} \n \n TODO \n \n \\ end{document} \n "
2026-02-09 10:21:50 +00:00
case " yaml " :
return " # TODO: Add config here \n "
case " json " :
return " { \n \" todo \" : true \n } \n "
case " xml " :
return " <?xml version= \" 1.0 \" encoding= \" UTF-8 \" ?> \n <root> \n <todo>true</todo> \n </root> \n "
case " toml " :
return " # TODO = \" value \" \n "
case " csv " :
return " col1,col2 \n value1,value2 \n "
case " ini " :
return " [section] \n key=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 \n set -euo pipefail \n \n # TODO: Add script here \n "
case " zsh " :
return " #!/usr/bin/env zsh \n \n set -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 "
}
}
2026-02-11 10:20:17 +00:00
private func templateOverrideKey ( for language : String ) -> String {
" TemplateOverride_ \( language ) "
}
2026-02-09 10:21:50 +00:00
func insertTemplateForCurrentLanguage ( ) {
let language = currentLanguage
guard let template = starterTemplate ( for : language ) else { return }
2026-02-28 18:26:00 +00:00
editorExternalMutationRevision &+= 1
let sourceContent = liveEditorBufferText ( ) ? ? currentContentBinding . wrappedValue
let updated : String
if sourceContent . trimmingCharacters ( in : . whitespacesAndNewlines ) . isEmpty {
updated = template
2026-02-09 10:21:50 +00:00
} else {
2026-02-28 18:26:00 +00:00
updated = sourceContent + ( sourceContent . hasSuffix ( " \n " ) ? " \n " : " \n \n " ) + template
2026-02-09 10:21:50 +00:00
}
2026-02-28 18:26:00 +00:00
currentContentBinding . wrappedValue = updated
2026-02-09 10:21:50 +00:00
}
2026-01-17 12:04:11 +00:00
private func detectLanguageWithAppleIntelligence ( _ text : String ) async -> String {
// S u p p o r t e d l a n g u a g e s i n o u r p i c k e r
2026-03-15 17:51:17 +00:00
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 " ]
2026-01-17 12:04:11 +00:00
2026-02-09 11:15:22 +00:00
#if USE_FOUNDATION_MODELS && canImport ( FoundationModels )
2026-02-05 21:30:21 +00:00
// A t t e m p t a l i g h t w e i g h t m o d e l - b a s e d d e t e c t i o n v i a A p p l e I n t e l l i g e n c e A I C l i e n t i f a v a i l a b l e
2026-01-17 12:04:11 +00:00
do {
2026-02-05 21:30:21 +00:00
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 \n Snippet: \n \n \( text ) \n \n Answer: " ) {
response += chunk
}
2026-01-17 12:04:11 +00:00
let detectedRaw = response . trimmingCharacters ( in : CharacterSet . whitespacesAndNewlines ) . lowercased ( )
if let match = supported . first ( where : { detectedRaw . contains ( $0 ) } ) {
return match
}
}
#endif
// H e u r i s t i c f a l l b a c k
let lower = text . lowercased ( )
2026-02-05 21:30:21 +00:00
// N o r m a l i z e c o m m o n C # i n d i c a t o r s t o " c s h a r p " t o e n s u r e t h e p i c k e r h a s a m a t c h i n g t a g
if lower . contains ( " c# " ) || lower . contains ( " c sharp " ) || lower . range ( of : # " \ bcs \ b " # , options : . regularExpression ) != nil || lower . contains ( " .cs " ) {
return " csharp "
}
2026-02-07 23:20:47 +00:00
if lower . contains ( " <?php " ) || lower . contains ( " <?= " ) || lower . contains ( " $this-> " ) || lower . contains ( " $_get " ) || lower . contains ( " $_post " ) || lower . contains ( " $_server " ) {
return " php "
}
2026-02-13 11:02:39 +00:00
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 "
}
2026-02-08 22:41:39 +00:00
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 "
}
2026-03-15 17:51:17 +00:00
if lower . contains ( " \\ documentclass " )
|| lower . contains ( " \\ usepackage " )
|| lower . contains ( " \\ begin{document} " )
|| lower . contains ( " \\ end{document} " ) {
return " tex "
}
2026-02-08 22:41:39 +00:00
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 "
}
2026-02-07 23:20:47 +00:00
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 "
}
}
}
2026-02-05 21:30:21 +00:00
// C # s t r o n g h e u r i s t i c
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 "
}
2026-01-17 12:04:11 +00:00
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 "
}
Add broad language support + Find/Replace panel; minor helpers
Languages:
- Extend picker, detection, TOC, and syntax highlighting for:
swift, python, javascript, typescript, java, kotlin, go, ruby, rust, sql,
html, css, c, cpp, objective-c, json, xml, yaml, toml, ini, markdown,
bash, zsh, powershell, plain
Editor:
- Add Find/Replace sheet with Find Next, Replace, Replace All
- New toolbar button (magnifying glass) to open Find/Replace
- Implement find/replace helpers operating on the active NSTextView
- Small NSRange helper for cleaner optional handling
Syntax highlighting:
- Add lightweight regex patterns for Java, Kotlin, Go, Ruby, Rust, TypeScript,
Objective‑C, SQL, XML, YAML, TOML, INI
- Keep performance-friendly patterns consistent with existing approach
TOC:
- Add TOC generation for Java, Kotlin, Go, Ruby, Rust, TypeScript, Objective‑C
Detection:
- Extend heuristics for XML, YAML, TOML/INI, SQL, Go, Java, Kotlin, TypeScript,
Ruby, Rust, Objective‑C, INI
Testing:
- Verify language picker shows all new entries and switching updates highlighting
- Paste snippets of each language; ensure heuristics pick a sensible default
- Open Find/Replace; test Find Next, Replace, Replace All; verify selection/scroll
- Check large files still perform acceptably with lightweight patterns
2026-02-04 15:21:56 +00:00
// X M L
if lower . contains ( " <?xml " ) || ( lower . contains ( " </ " ) && lower . contains ( " > " ) ) {
return " xml "
}
// Y A M L
if lower . contains ( " : " ) && ( lower . contains ( " - " ) || lower . contains ( " \n " ) ) && ! lower . contains ( " ; " ) {
return " yaml "
}
// T O M L / I N I
if lower . range ( of : # " ^ \ [[^ \ ]]+ \ ] " # , options : [ . regularExpression , . anchored ] ) != nil || ( lower . contains ( " = " ) && lower . contains ( " \n [ " ) ) {
return lower . contains ( " toml " ) ? " toml " : " ini "
}
// S Q L
if lower . range ( of : # " \ b(select|insert|update|delete|create \ s+table|from|where|join) \ b " # , options : . regularExpression ) != nil {
return " sql "
}
// G o
if lower . contains ( " package " ) && lower . contains ( " func " ) {
return " go "
}
// J a v a
if lower . contains ( " public class " ) || lower . contains ( " public static void main " ) {
return " java "
}
// K o t l i n
if ( lower . contains ( " fun " ) || lower . contains ( " val " ) ) || ( lower . contains ( " var " ) && lower . contains ( " : " ) ) {
return " kotlin "
}
// T y p e S c r i p t
if lower . contains ( " interface " ) || ( lower . contains ( " type " ) && lower . contains ( " : " ) ) || lower . contains ( " : string " ) {
return " typescript "
}
// R u b y
if lower . contains ( " def " ) || ( lower . contains ( " end " ) && lower . contains ( " class " ) ) {
return " ruby "
}
// R u s t
if lower . contains ( " fn " ) || lower . contains ( " let mut " ) || lower . contains ( " pub struct " ) {
return " rust "
}
// O b j e c t i v e - C
if lower . contains ( " @interface " ) || lower . contains ( " @implementation " ) || lower . contains ( " #import " ) {
return " objective-c "
}
// I N I
if lower . range ( of : # " ^;.*$ " # , options : . regularExpression ) != nil || lower . range ( of : # " ^ \ w+ \ s*= \ s*.*$ " # , options : . regularExpression ) != nil {
return " ini "
}
2026-01-17 12:04:11 +00:00
if lower . contains ( " <html " ) || lower . contains ( " <div " ) || lower . contains ( " </ " ) {
return " html "
}
2026-02-05 21:30:21 +00:00
// S t r i c t e r C - f a m i l y d e t e c t i o n t o a v o i d m i s c l a s s i f y i n g C #
if lower . contains ( " #include " ) || lower . range ( of : # " ^ \ s*(int|void) \ s+main \ s* \( " #, options: .regularExpression) != nil {
return " cpp "
2026-01-17 12:04:11 +00:00
}
if lower . contains ( " class " ) && ( lower . contains ( " :: " ) || lower . contains ( " template< " ) ) {
return " cpp "
}
if lower . contains ( " ; " ) && lower . contains ( " : " ) && lower . contains ( " { " ) && lower . contains ( " } " ) && lower . contains ( " color: " ) {
return " css "
}
2026-01-23 11:49:52 +00:00
// S h e l l d e t e c t i o n ( b a s h / z s h )
2026-02-05 21:30:21 +00:00
if lower . contains ( " #!/bin/bash " ) || lower . contains ( " #!/usr/bin/env bash " ) || lower . contains ( " declare -a " ) || lower . contains ( " [[ " ) || lower . contains ( " ]] " ) || lower . contains ( " $( " ) {
2026-01-23 11:49:52 +00:00
return " bash "
}
if lower . contains ( " #!/bin/zsh " ) || lower . contains ( " #!/usr/bin/env zsh " ) || lower . contains ( " typeset " ) || lower . contains ( " autoload -Uz " ) || lower . contains ( " setopt " ) {
return " zsh "
}
// G e n e r i c P O S I X s h f a l l b a c k
if lower . contains ( " #!/bin/sh " ) || lower . contains ( " #!/usr/bin/env sh " ) || lower . contains ( " fi " ) || lower . contains ( " do " ) || lower . contains ( " done " ) || lower . contains ( " esac " ) {
return " bash "
}
2026-02-04 13:11:28 +00:00
// P o w e r S h e l l d e t e c t i o n
if lower . contains ( " write-host " ) || lower . contains ( " param( " ) || lower . contains ( " $psversiontable " ) || lower . range ( of : # " \ b(Get|Set|New|Remove|Add|Clear|Write)-[A-Za-z]+ \ b " # , options : . regularExpression ) != nil {
return " powershell "
}
2026-02-05 21:30:21 +00:00
return " standard "
2026-01-17 12:04:11 +00:00
}
2026-02-14 13:24:01 +00:00
// / MARK: - M a i n E d i t o r S t a c k
2026-03-09 12:54:26 +00:00
@ ViewBuilder
private var projectStructureSidebarPanel : some View {
#if os ( macOS )
2026-03-29 13:57:01 +00:00
projectStructureSidebarBody
. frame (
2026-03-09 13:10:42 +00:00
minWidth : clampedProjectSidebarWidth ,
idealWidth : clampedProjectSidebarWidth ,
maxWidth : clampedProjectSidebarWidth
)
2026-03-09 12:54:26 +00:00
#else
projectStructureSidebarBody
2026-03-09 13:10:42 +00:00
. frame (
minWidth : clampedProjectSidebarWidth ,
idealWidth : clampedProjectSidebarWidth ,
maxWidth : clampedProjectSidebarWidth
)
2026-03-26 18:19:45 +00:00
. background ( editorSurfaceBackgroundStyle )
2026-03-09 12:54:26 +00:00
#endif
}
2026-03-09 13:10:42 +00:00
private var projectSidebarResizeHandle : some View {
let drag = DragGesture ( minimumDistance : 0 )
. onChanged { value in
let startWidth = projectSidebarResizeStartWidth ? ? clampedProjectSidebarWidth
if projectSidebarResizeStartWidth = = nil {
projectSidebarResizeStartWidth = startWidth
}
let delta = value . translation . width
let proposed : CGFloat
switch projectNavigatorPlacement {
case . leading :
proposed = startWidth + delta
case . trailing :
proposed = startWidth - delta
}
2026-04-16 10:37:03 +00:00
let clamped = min ( max ( proposed , minimumProjectSidebarWidth ) , maximumProjectSidebarWidth )
2026-03-09 13:10:42 +00:00
projectSidebarWidth = Double ( clamped )
}
. onEnded { _ in
projectSidebarResizeStartWidth = nil
}
return ZStack {
2026-03-09 16:47:50 +00:00
// M a t c h t h e s a m e s u r f a c e a s t h e e d i t o r a r e a s o t h e s p l i t t e r d o e s n ' t l o o k l i k e a f o r e i g n s t r i p .
Rectangle ( )
. fill ( projectSidebarHandleSurfaceStyle )
2026-03-09 13:10:42 +00:00
Rectangle ( )
2026-03-09 16:47:50 +00:00
. fill ( Color . secondary . opacity ( 0.22 ) )
2026-03-09 13:10:42 +00:00
. frame ( width : 1 )
2026-03-29 13:57:01 +00:00
. frame ( maxWidth : . infinity , alignment : projectNavigatorPlacement = = . leading ? . leading : . trailing )
2026-03-09 13:10:42 +00:00
}
. frame ( width : 10 )
. contentShape ( Rectangle ( ) )
. gesture ( drag )
2026-03-09 16:47:50 +00:00
#if os ( macOS )
. onHover { hovering in
guard hovering != isProjectSidebarResizeHandleHovered else { return }
isProjectSidebarResizeHandleHovered = hovering
if hovering {
NSCursor . resizeLeftRight . push ( )
} else {
NSCursor . pop ( )
}
}
. onDisappear {
if isProjectSidebarResizeHandleHovered {
isProjectSidebarResizeHandleHovered = false
NSCursor . pop ( )
}
}
#endif
2026-03-09 13:10:42 +00:00
. accessibilityElement ( )
. accessibilityLabel ( " Resize Project Sidebar " )
. accessibilityHint ( " Drag left or right to adjust project sidebar width " )
}
2026-03-09 16:47:50 +00:00
private var projectSidebarHandleSurfaceStyle : AnyShapeStyle {
if enableTranslucentWindow {
return editorSurfaceBackgroundStyle
}
#if os ( iOS )
return useIOSUnifiedSolidSurfaces
? AnyShapeStyle ( iOSNonTranslucentSurfaceColor )
: AnyShapeStyle ( Color . clear )
#else
return AnyShapeStyle ( Color . clear )
#endif
}
2026-03-26 18:19:45 +00:00
#if os ( iOS )
var iOSSurfaceSeparatorFill : Color {
iOSNonTranslucentSurfaceColor
}
var iOSSurfaceSeparatorLine : Color {
colorScheme = = . dark ? Color . white . opacity ( 0.14 ) : Color . black . opacity ( 0.10 )
}
var iOSPaneDivider : some View {
ZStack {
Rectangle ( )
. fill ( iOSSurfaceSeparatorFill )
Rectangle ( )
. fill ( iOSSurfaceSeparatorLine )
. frame ( width : 1 )
}
. frame ( width : 10 )
}
var iOSHorizontalSurfaceDivider : some View {
ZStack {
Rectangle ( )
. fill ( iOSSurfaceSeparatorFill )
Rectangle ( )
. fill ( iOSSurfaceSeparatorLine )
. frame ( height : 1 )
}
. frame ( height : 10 )
}
var iOSVerticalSurfaceDivider : some View {
ZStack {
Rectangle ( )
. fill ( iOSSurfaceSeparatorFill )
Rectangle ( )
. fill ( iOSSurfaceSeparatorLine )
. frame ( width : 1 )
}
. frame ( width : 10 , height : 18 )
}
#endif
2026-03-09 12:54:26 +00:00
private var projectStructureSidebarBody : some View {
ProjectStructureSidebarView (
rootFolderURL : projectRootFolderURL ,
nodes : projectTreeNodes ,
selectedFileURL : viewModel . selectedTab ? . fileURL ,
showSupportedFilesOnly : showSupportedProjectFilesOnly ,
translucentBackgroundEnabled : enableTranslucentWindow ,
2026-03-29 13:57:01 +00:00
boundaryEdge : projectNavigatorPlacement = = . leading ? . trailing : . leading ,
2026-03-09 12:54:26 +00:00
onOpenFile : { openFileFromToolbar ( ) } ,
onOpenFolder : { openProjectFolder ( ) } ,
onToggleSupportedFilesOnly : { showSupportedProjectFilesOnly = $0 } ,
onOpenProjectFile : { openProjectFile ( url : $0 ) } ,
2026-04-16 10:37:03 +00:00
onRefreshTree : { refreshProjectBrowserState ( ) } ,
onCreateProjectFile : { startProjectItemCreation ( kind : . file , in : $0 ) } ,
onCreateProjectFolder : { startProjectItemCreation ( kind : . folder , in : $0 ) } ,
onRenameProjectItem : { startProjectItemRename ( $0 ) } ,
onDuplicateProjectItem : { duplicateProjectItem ( $0 ) } ,
onDeleteProjectItem : { requestDeleteProjectItem ( $0 ) } ,
revealURL : projectTreeRevealURL
2026-03-09 12:54:26 +00:00
)
}
2026-03-17 17:40:32 +00:00
private func handleAppDidBecomeActive ( ) {
if let selectedID = viewModel . selectedTab ? . id {
viewModel . refreshExternalConflictForTab ( tabID : selectedID )
}
if projectRootFolderURL != nil {
refreshProjectBrowserState ( )
}
}
private func handleAppWillResignActive ( ) {
persistSessionIfReady ( )
persistUnsavedDraftSnapshotIfNeeded ( )
}
2026-03-09 13:10:42 +00:00
private var delimitedModeControl : some View {
HStack ( spacing : 10 ) {
Picker ( " CSV/TSV View Mode " , selection : $ delimitedViewMode ) {
Text ( " Table " ) . tag ( DelimitedViewMode . table )
Text ( " Text " ) . tag ( DelimitedViewMode . text )
}
. pickerStyle ( . segmented )
. frame ( maxWidth : 210 )
. accessibilityLabel ( " CSV or TSV view mode " )
. accessibilityHint ( " Switch between table mode and raw text mode " )
if shouldShowDelimitedTable {
if isBuildingDelimitedTable {
ProgressView ( )
. scaleEffect ( 0.85 )
} else if let snapshot = delimitedTableSnapshot {
Text (
snapshot . truncated
? " Showing \( snapshot . displayedRows ) / \( snapshot . totalRows ) rows "
: " \( snapshot . totalRows ) rows "
)
. font ( . caption )
. foregroundStyle ( . secondary )
} else if ! delimitedTableStatus . isEmpty {
Text ( delimitedTableStatus )
. font ( . caption )
. foregroundStyle ( . secondary )
}
}
Spacer ( minLength : 0 )
}
. padding ( . horizontal , 10 )
. padding ( . vertical , 8 )
2026-03-09 16:47:50 +00:00
. background ( delimitedHeaderBackgroundColor )
}
private var delimitedHeaderBackgroundColor : Color {
#if os ( macOS )
2026-03-26 18:19:45 +00:00
currentEditorTheme ( colorScheme : colorScheme ) . background
2026-03-09 16:47:50 +00:00
#else
Color ( . systemBackground )
#endif
2026-03-09 13:10:42 +00:00
}
private var delimitedTableView : some View {
Group {
if isBuildingDelimitedTable {
VStack ( spacing : 12 ) {
ProgressView ( )
Text ( " Building table view… " )
. font ( . footnote )
. foregroundStyle ( . secondary )
}
. frame ( maxWidth : . infinity , maxHeight : . infinity )
} else if let snapshot = delimitedTableSnapshot {
ScrollView ( [ . horizontal , . vertical ] ) {
LazyVStack ( alignment : . leading , spacing : 0 , pinnedViews : [ . sectionHeaders ] ) {
Section {
ForEach ( Array ( snapshot . rows . enumerated ( ) ) , id : \ . offset ) { index , row in
delimitedRowView ( cells : row , isHeader : false , rowIndex : index )
}
} header : {
delimitedRowView ( cells : snapshot . header , isHeader : true , rowIndex : nil )
}
}
. padding ( . horizontal , 8 )
. padding ( . vertical , 6 )
}
. frame ( maxWidth : . infinity , maxHeight : . infinity , alignment : . topLeading )
} else {
Text ( delimitedTableStatus . isEmpty ? " No rows found. " : delimitedTableStatus )
. font ( . footnote )
. foregroundStyle ( . secondary )
. frame ( maxWidth : . infinity , maxHeight : . infinity )
}
}
. background (
Group {
if enableTranslucentWindow {
Color . clear . background ( editorSurfaceBackgroundStyle )
} else {
#if os ( iOS )
iOSNonTranslucentSurfaceColor
#else
Color . clear
#endif
}
}
)
. accessibilityElement ( children : . contain )
. accessibilityLabel ( " CSV or TSV table " )
}
private func delimitedRowView ( cells : [ String ] , isHeader : Bool , rowIndex : Int ? ) -> 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 = " "
2026-03-09 13:13:52 +00:00
case . failure ( let error ) :
2026-03-09 13:10:42 +00:00
delimitedTableSnapshot = nil
2026-03-09 13:13:52 +00:00
delimitedTableStatus = error . localizedDescription
2026-03-09 13:10:42 +00:00
}
}
}
private nonisolated static func buildDelimitedTableSnapshot (
from text : String ,
separator : Character ,
maxRows : Int ,
maxColumns : Int
2026-03-09 13:13:52 +00:00
) -> Result < DelimitedTableSnapshot , DelimitedTableParseError > {
guard ! text . isEmpty else { return . failure ( DelimitedTableParseError ( message : " No data in file. " ) ) }
2026-03-09 13:10:42 +00:00
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 ) )
}
}
2026-03-09 13:13:52 +00:00
guard ! rows . isEmpty else { return . failure ( DelimitedTableParseError ( message : " No rows found. " ) ) }
2026-03-09 13:10:42 +00:00
let rawHeader = rows . removeFirst ( )
let visibleColumns = max ( rawHeader . count , rows . first ? . count ? ? 0 )
let header : [ String ] = {
if rawHeader . isEmpty {
return ( 0. . < visibleColumns ) . map { " Column \( $0 + 1 ) " }
}
return rawHeader . enumerated ( ) . map { idx , value in
let trimmed = value . trimmingCharacters ( in : . whitespacesAndNewlines )
return trimmed . isEmpty ? " Column \( idx + 1 ) " : trimmed
}
} ( )
let normalizedRows = rows . map { row in
if row . count >= 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
}
2026-02-06 18:59:53 +00:00
var editorView : some View {
2026-02-25 13:07:05 +00:00
@ Bindable var bindableViewModel = viewModel
2026-02-18 19:19:49 +00:00
let shouldThrottleFeatures = shouldThrottleHeavyEditorFeatures ( )
2026-03-13 14:13:44 +00:00
let effectiveHighlightCurrentLine = highlightCurrentLine && ! shouldThrottleFeatures
2026-02-18 19:19:49 +00:00
let effectiveBracketHighlight = highlightMatchingBrackets && ! shouldThrottleFeatures
let effectiveScopeGuides = showScopeGuides && ! shouldThrottleFeatures
let effectiveScopeBackground = highlightScopeBackground && ! shouldThrottleFeatures
2026-02-08 09:58:46 +00:00
let content = HStack ( spacing : 0 ) {
2026-03-09 12:54:26 +00:00
if showProjectStructureSidebar && projectNavigatorPlacement = = . leading && ! brainDumpLayoutEnabled {
projectStructureSidebarPanel
2026-03-09 13:10:42 +00:00
projectSidebarResizeHandle
2026-03-09 12:54:26 +00:00
}
2026-02-06 18:59:53 +00:00
VStack ( spacing : 0 ) {
2026-02-20 10:27:55 +00:00
if ! useIPhoneUnifiedTopHost && ! brainDumpLayoutEnabled {
2026-02-06 19:20:03 +00:00
tabBarView
}
2026-02-19 08:44:24 +00:00
#if os ( macOS )
2026-02-19 08:58:59 +00:00
if showBracketHelperBarMac {
bracketHelperBar
}
2026-02-19 08:44:24 +00:00
#endif
2026-02-06 19:20:03 +00:00
2026-03-09 13:10:42 +00:00
if isDelimitedFileLanguage && ! brainDumpLayoutEnabled {
delimitedModeControl
}
Group {
if shouldShowDelimitedTable && ! brainDumpLayoutEnabled {
delimitedTableView
2026-03-13 14:13:44 +00:00
} else if shouldUseDeferredLargeFileOpenMode ,
viewModel . selectedTab ? . isLoadingContent = = true ,
2026-03-16 10:20:48 +00:00
( viewModel . selectedTab ? . isLargeFileCandidate = = true ||
currentDocumentUTF16Length >= 300_000 ||
largeFileModeEnabled ) {
2026-03-13 14:13:44 +00:00
largeFileLoadingPlaceholder
2026-03-09 13:10:42 +00:00
} else {
// S i n g l e e d i t o r ( n o T a b V i e w )
CustomTextEditor (
text : currentContentBinding ,
documentID : viewModel . selectedTabID ,
externalEditRevision : editorExternalMutationRevision ,
language : currentLanguage ,
colorScheme : colorScheme ,
fontSize : editorFontSize ,
isLineWrapEnabled : $ bindableViewModel . isLineWrapEnabled ,
2026-03-13 14:13:44 +00:00
isLargeFileMode : effectiveLargeFileModeEnabled ,
2026-03-09 13:10:42 +00:00
translucentBackgroundEnabled : enableTranslucentWindow ,
showKeyboardAccessoryBar : {
2026-02-19 14:29:53 +00:00
#if os ( iOS )
2026-03-09 13:10:42 +00:00
showKeyboardAccessoryBarIOS
2026-02-19 14:29:53 +00:00
#else
2026-03-09 13:10:42 +00:00
true
#endif
} ( ) ,
showLineNumbers : showLineNumbers ,
2026-04-16 10:37:03 +00:00
showInvisibleCharacters : showInvisibleCharacters ,
2026-03-13 14:13:44 +00:00
highlightCurrentLine : effectiveHighlightCurrentLine ,
2026-03-09 13:10:42 +00:00
highlightMatchingBrackets : effectiveBracketHighlight ,
showScopeGuides : effectiveScopeGuides ,
highlightScopeBackground : effectiveScopeBackground ,
indentStyle : indentStyle ,
indentWidth : effectiveIndentWidth ,
autoIndentEnabled : autoIndentEnabled ,
autoCloseBracketsEnabled : autoCloseBracketsEnabled ,
highlightRefreshToken : highlightRefreshToken ,
isTabLoadingContent : viewModel . selectedTab ? . isLoadingContent ? ? false ,
2026-03-29 10:39:47 +00:00
isReadOnly : isSelectedTabReadOnlyPreview ,
2026-03-09 13:10:42 +00:00
onTextMutation : { mutation in
viewModel . applyTabContentEdit (
tabID : mutation . documentID ,
range : mutation . range ,
replacement : mutation . replacement
)
}
2026-02-25 13:07:05 +00:00
)
2026-03-09 13:10:42 +00:00
. id ( currentLanguage )
2026-03-15 14:56:58 +00:00
. overlay {
2026-03-17 17:40:32 +00:00
if shouldShowStartupOverlay {
startupOverlay
2026-03-15 14:56:58 +00:00
}
}
2026-02-25 13:07:05 +00:00
}
2026-03-09 13:10:42 +00:00
}
2026-02-20 10:27:55 +00:00
. frame ( maxWidth : brainDumpLayoutEnabled ? 920 : . infinity )
2026-02-06 18:59:53 +00:00
. frame ( maxHeight : . infinity )
2026-02-20 10:27:55 +00:00
. padding ( . horizontal , brainDumpLayoutEnabled ? 24 : 0 )
. padding ( . vertical , brainDumpLayoutEnabled ? 40 : 0 )
2026-02-06 18:59:53 +00:00
. background (
Group {
if enableTranslucentWindow {
2026-02-19 14:29:53 +00:00
Color . clear . background ( editorSurfaceBackgroundStyle )
2026-02-18 22:56:46 +00:00
} else {
2026-02-20 00:31:14 +00:00
#if os ( iOS )
iOSNonTranslucentSurfaceColor
#else
2026-02-18 22:56:46 +00:00
Color . clear
2026-02-20 00:31:14 +00:00
#endif
2026-02-18 22:56:46 +00:00
}
2026-02-06 13:29:34 +00:00
}
2026-02-06 18:59:53 +00:00
)
2026-03-13 14:13:44 +00:00
. overlay ( alignment : . topTrailing ) {
if effectiveLargeFileModeEnabled && ! brainDumpLayoutEnabled {
largeFileSessionBadge
. padding ( . top , 10 )
. padding ( . trailing , 12 )
. zIndex ( 5 )
}
}
2026-02-06 18:59:53 +00:00
2026-02-20 10:27:55 +00:00
if ! brainDumpLayoutEnabled {
2026-03-04 19:47:31 +00:00
#if os ( macOS )
2026-02-06 18:59:53 +00:00
wordCountView
2026-03-04 19:47:31 +00:00
#endif
2026-02-06 13:29:34 +00:00
}
2026-02-06 18:59:53 +00:00
}
2026-02-14 22:15:22 +00:00
. frame (
maxWidth : . infinity ,
maxHeight : . infinity ,
2026-02-20 10:27:55 +00:00
alignment : brainDumpLayoutEnabled ? . top : . topLeading
2026-02-14 22:15:22 +00:00
)
2026-01-17 11:11:26 +00:00
2026-03-08 14:31:01 +00:00
if canShowMarkdownPreviewSplitPane && showMarkdownPreviewPane && currentLanguage = = " markdown " && ! brainDumpLayoutEnabled {
2026-03-26 18:19:45 +00:00
#if os ( iOS )
iOSPaneDivider
#else
2026-02-24 14:44:43 +00:00
Divider ( )
2026-03-26 18:19:45 +00:00
#endif
2026-02-24 14:44:43 +00:00
markdownPreviewPane
. frame ( minWidth : 280 , idealWidth : 420 , maxWidth : 680 , maxHeight : . infinity )
}
2026-03-09 12:54:26 +00:00
if showProjectStructureSidebar && projectNavigatorPlacement = = . trailing && ! brainDumpLayoutEnabled {
2026-03-09 13:10:42 +00:00
projectSidebarResizeHandle
2026-03-09 12:54:26 +00:00
projectStructureSidebarPanel
2025-09-25 09:01:45 +00:00
}
}
2026-02-14 22:15:22 +00:00
. background (
Group {
2026-02-20 10:27:55 +00:00
if brainDumpLayoutEnabled && enableTranslucentWindow {
2026-02-19 14:29:53 +00:00
Color . clear . background ( editorSurfaceBackgroundStyle )
2026-02-18 22:56:46 +00:00
} else {
2026-02-20 00:31:14 +00:00
#if os ( iOS )
2026-03-26 18:19:45 +00:00
Color . clear . background ( editorSurfaceBackgroundStyle )
2026-02-20 00:31:14 +00:00
#else
2026-02-18 22:56:46 +00:00
Color . clear
2026-02-20 00:31:14 +00:00
#endif
2026-02-18 22:56:46 +00:00
}
2026-02-14 22:15:22 +00:00
}
)
2026-02-07 10:51:52 +00:00
. frame ( maxWidth : . infinity , maxHeight : . infinity , alignment : . topLeading )
2026-02-20 10:27:55 +00:00
2026-02-20 02:41:08 +00:00
#if os ( iOS )
2026-02-20 10:27:55 +00:00
let contentWithTopChrome = useIPhoneUnifiedTopHost
? AnyView (
content . safeAreaInset ( edge : . top , spacing : 0 ) {
iPhoneUnifiedTopChromeHost
}
)
: AnyView ( content )
#else
let contentWithTopChrome = AnyView ( content )
2026-02-20 02:41:08 +00:00
#endif
2026-02-08 09:58:46 +00:00
let withEvents = withTypingEvents (
withCommandEvents (
2026-02-20 10:27:55 +00:00
withBaseEditorEvents ( contentWithTopChrome )
2026-02-08 09:58:46 +00:00
)
)
return withEvents
2026-02-19 14:29:53 +00:00
. onAppear {
2026-03-09 13:10:42 +00:00
if isDelimitedFileLanguage {
delimitedViewMode = . table
} else {
delimitedViewMode = . text
}
2026-03-13 14:13:44 +00:00
refreshSecondaryContentViewsIfNeeded ( )
2026-02-19 14:29:53 +00:00
}
2026-03-13 14:13:44 +00:00
. onChange ( of : viewModel . tabsObservationToken ) { _ , _ in
refreshSecondaryContentViewsIfNeeded ( )
2026-03-09 13:10:42 +00:00
}
. onChange ( of : delimitedViewMode ) { _ , newValue in
if newValue = = . table {
2026-03-13 14:13:44 +00:00
refreshSecondaryContentViewsIfNeeded ( )
2026-03-09 13:10:42 +00:00
} else {
delimitedParseTask ? . cancel ( )
isBuildingDelimitedTable = false
}
}
. onChange ( of : currentLanguage ) { _ , _ in
if isDelimitedFileLanguage {
if delimitedViewMode = = . text {
// K e e p e x p l i c i t u s e r c h o i c e w h e n a l r e a d y i n t e x t m o d e .
} else {
delimitedViewMode = . table
}
if shouldShowDelimitedTable {
2026-03-13 14:13:44 +00:00
refreshSecondaryContentViewsIfNeeded ( )
2026-03-09 13:10:42 +00:00
}
} else {
delimitedViewMode = . text
delimitedParseTask ? . cancel ( )
isBuildingDelimitedTable = false
delimitedTableSnapshot = nil
delimitedTableStatus = " "
}
2026-02-19 14:29:53 +00:00
}
. onDisappear {
wordCountTask ? . cancel ( )
2026-03-09 13:10:42 +00:00
delimitedParseTask ? . cancel ( )
2026-02-19 14:29:53 +00:00
}
2026-02-06 13:29:34 +00:00
. onChange ( of : enableTranslucentWindow ) { _ , newValue in
2026-02-06 18:59:53 +00:00
applyWindowTranslucency ( newValue )
2026-02-14 23:38:13 +00:00
// F o r c e i m m e d i a t e r e c o l o r w h e n t r a n s l u c e n c y c h a n g e s s o s y n t a x h i g h l i g h t i n g s t a y s v i s i b l e .
highlightRefreshToken &+= 1
2026-02-06 13:29:34 +00:00
}
2026-02-19 14:29:53 +00:00
#if os ( iOS )
. onChange ( of : showKeyboardAccessoryBarIOS ) { _ , isVisible in
NotificationCenter . default . post (
name : . keyboardAccessoryBarVisibilityChanged ,
object : isVisible
)
}
2026-02-28 19:48:06 +00:00
. onChange ( of : horizontalSizeClass ) { _ , newClass in
2026-03-08 14:31:01 +00:00
if UIDevice . current . userInterfaceIdiom = = . pad && newClass != . regular && showMarkdownPreviewPane {
2026-02-28 19:48:06 +00:00
showMarkdownPreviewPane = false
}
}
2026-02-20 02:41:08 +00:00
. onChange ( of : showSettingsSheet ) { _ , isPresented in
if isPresented {
if previousKeyboardAccessoryVisibility = = nil {
previousKeyboardAccessoryVisibility = showKeyboardAccessoryBarIOS
}
showKeyboardAccessoryBarIOS = false
} else if let previousKeyboardAccessoryVisibility {
showKeyboardAccessoryBarIOS = previousKeyboardAccessoryVisibility
self . previousKeyboardAccessoryVisibility = nil
}
}
2026-02-19 14:29:53 +00:00
#endif
#if os ( macOS )
. onChange ( of : macTranslucencyModeRaw ) { _ , _ in
// K e e p a l l c h r o m e / b a c k g r o u n d s u r f a c e s i n l o c k s t e p w h e n m o d e c h a n g e s .
2026-03-17 17:40:32 +00:00
applyWindowTranslucency ( enableTranslucentWindow )
2026-02-19 14:29:53 +00:00
highlightRefreshToken &+= 1
}
2026-03-26 18:19:45 +00:00
. onChange ( of : colorScheme ) { _ , _ in
applyWindowTranslucency ( enableTranslucentWindow )
highlightRefreshToken &+= 1
}
2026-02-19 14:29:53 +00:00
#endif
2025-09-25 09:01:45 +00:00
. toolbar {
2026-02-06 18:59:53 +00:00
editorToolbarContent
2025-08-27 11:33:45 +00:00
}
2026-02-11 10:20:17 +00:00
. overlay ( alignment : Alignment . topTrailing ) {
2026-02-08 11:14:49 +00:00
if droppedFileLoadInProgress {
2026-02-19 08:09:35 +00:00
HStack ( spacing : 8 ) {
if droppedFileProgressDeterminate {
ProgressView ( value : droppedFileLoadProgress )
. progressViewStyle ( . linear )
. frame ( width : 120 )
} else {
ProgressView ( )
. frame ( width : 16 )
2026-02-08 11:14:49 +00:00
}
2026-02-19 08:09:35 +00:00
Text ( droppedFileProgressDeterminate ? " \( droppedFileLoadLabel ) \( importProgressPercentText ) " : " \( droppedFileLoadLabel ) Loading… " )
. font ( . system ( size : 11 , weight : . medium ) )
. lineLimit ( 1 )
2026-02-08 11:14:49 +00:00
}
2026-02-19 08:09:35 +00:00
. padding ( . horizontal , 10 )
. padding ( . vertical , 7 )
. background ( . ultraThinMaterial , in : Capsule ( style : . continuous ) )
2026-02-20 10:27:55 +00:00
. padding ( . top , brainDumpLayoutEnabled ? 12 : 50 )
2026-02-08 11:14:49 +00:00
. padding ( . trailing , 12 )
}
}
2026-03-04 19:47:31 +00:00
#if os ( iOS )
. overlay ( alignment : . bottomTrailing ) {
if ! brainDumpLayoutEnabled {
floatingStatusPill
. padding ( . trailing , 12 )
. padding ( . bottom , 12 )
}
}
#endif
2026-02-24 14:44:43 +00:00
. onChange ( of : currentLanguage ) { _ , newLanguage in
if newLanguage != " markdown " , showMarkdownPreviewPane {
showMarkdownPreviewPane = false
}
}
2026-02-07 10:51:52 +00:00
#if os ( macOS )
2026-02-19 14:29:53 +00:00
. toolbarBackground (
2026-03-10 15:14:21 +00:00
macToolbarBackgroundStyle ,
2026-02-19 14:29:53 +00:00
for : ToolbarPlacement . windowToolbar
)
2026-02-20 22:04:03 +00:00
. toolbarBackgroundVisibility ( Visibility . visible , for : ToolbarPlacement . windowToolbar )
2026-02-19 14:29:53 +00:00
. tint ( NeonUIStyle . accentBlue )
2026-02-07 10:51:52 +00:00
#else
2026-02-20 10:27:55 +00:00
. toolbarBackground (
enableTranslucentWindow
? AnyShapeStyle ( . ultraThinMaterial )
: AnyShapeStyle ( Color ( . systemBackground ) ) ,
for : ToolbarPlacement . navigationBar
)
2026-02-07 10:51:52 +00:00
#endif
2025-08-27 11:34:59 +00:00
}
2026-01-25 12:46:33 +00:00
2026-03-13 14:13:44 +00:00
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 " )
}
2026-02-28 19:48:06 +00:00
#if os ( macOS ) || os ( iOS )
2026-02-24 14:44:43 +00:00
@ ViewBuilder
private var markdownPreviewPane : some View {
VStack ( alignment : . leading , spacing : 0 ) {
2026-03-17 17:40:32 +00:00
MarkdownPreviewWebView (
html : markdownPreviewHTML (
from : currentContent ,
preferDarkMode : markdownPreviewPreferDarkMode
)
)
2026-03-28 19:13:32 +00:00
. frame ( maxWidth : . infinity , maxHeight : . infinity )
2026-02-24 14:44:43 +00:00
. accessibilityLabel ( " Markdown Preview Content " )
}
2026-03-28 19:13:32 +00:00
. frame ( maxWidth : . infinity , maxHeight : . infinity )
2026-02-24 14:44:43 +00:00
. background ( editorSurfaceBackgroundStyle )
2026-03-26 18:19:45 +00:00
#if canImport ( UIKit )
. fileExporter (
isPresented : $ showMarkdownPDFExporter ,
document : markdownPDFExportDocument ,
contentType : . pdf ,
defaultFilename : markdownPDFExportFilename
) { result in
if case . failure ( let error ) = result {
markdownPDFExportErrorMessage = error . localizedDescription
}
}
#endif
2026-02-24 14:44:43 +00:00
}
#endif
2026-03-26 18:19:45 +00:00
@ ViewBuilder
private var markdownPreviewHeader : some View {
#if os ( iOS )
if UIDevice . current . userInterfaceIdiom = = . phone {
VStack ( spacing : 16 ) {
VStack ( spacing : 10 ) {
2026-03-28 19:13:32 +00:00
markdownPreviewCombinedPickerCard
2026-03-26 18:19:45 +00:00
2026-03-28 19:13:32 +00:00
markdownPreviewPrimaryActionRow
2026-03-26 18:19:45 +00:00
. 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 ) {
2026-03-30 17:07:02 +00:00
Text ( NSLocalizedString ( " Markdown Preview " , comment : " " ) )
2026-03-26 18:19:45 +00:00
. font ( . headline )
VStack ( spacing : 10 ) {
2026-03-28 19:13:32 +00:00
markdownPreviewCombinedPickerCard
2026-03-26 18:19:45 +00:00
2026-03-28 19:13:32 +00:00
markdownPreviewPrimaryActionRow
. padding ( . top , 2 )
2026-03-26 18:19:45 +00:00
2026-03-28 19:13:32 +00:00
markdownPreviewSecondaryActionRow
2026-03-26 18:19:45 +00:00
. padding ( . top , 2 )
2026-03-30 17:07:02 +00:00
Text ( markdownPreviewExportSummaryText )
. font ( . caption )
. foregroundStyle ( . secondary )
. multilineTextAlignment ( . center )
. accessibilityLabel ( NSLocalizedString ( " Markdown preview export summary " , comment : " " ) )
markdownPreviewActionStatusView
2026-03-26 18:19:45 +00:00
}
#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 ) {
2026-03-30 17:07:02 +00:00
Text ( NSLocalizedString ( " Markdown Preview " , comment : " " ) )
2026-03-26 18:19:45 +00:00
. font ( . headline )
. frame ( maxWidth : . infinity , alignment : . center )
VStack ( spacing : 10 ) {
2026-03-28 19:13:32 +00:00
markdownPreviewPrimaryActionRow
. padding ( . top , 2 )
2026-03-26 18:19:45 +00:00
2026-03-29 13:57:01 +00:00
markdownPreviewCombinedPickerCard
2026-03-28 19:13:32 +00:00
markdownPreviewSecondaryActionRow
2026-03-26 18:19:45 +00:00
. padding ( . top , 2 )
2026-03-30 17:07:02 +00:00
Text ( markdownPreviewExportSummaryText )
. font ( . caption )
. foregroundStyle ( . secondary )
. multilineTextAlignment ( . center )
. accessibilityLabel ( NSLocalizedString ( " Markdown preview export summary " , comment : " " ) )
markdownPreviewActionStatusView
2026-03-26 18:19:45 +00:00
}
2026-03-28 19:13:32 +00:00
. frame ( maxWidth : 460 )
2026-03-26 18:19:45 +00:00
. padding ( 16 )
. background ( . thinMaterial , in : RoundedRectangle ( cornerRadius : 18 , style : . continuous ) )
}
. frame ( maxWidth : . infinity , alignment : . center )
}
private var markdownPreviewTemplatePicker : some View {
2026-03-30 17:07:02 +00:00
Picker ( NSLocalizedString ( " Template " , comment : " " ) , selection : $ markdownPreviewTemplateRaw ) {
Text ( NSLocalizedString ( " Default " , comment : " " ) ) . tag ( " default " )
Text ( NSLocalizedString ( " Docs " , comment : " " ) ) . tag ( " docs " )
Text ( NSLocalizedString ( " Article " , comment : " " ) ) . tag ( " article " )
Text ( NSLocalizedString ( " Compact " , comment : " " ) ) . tag ( " compact " )
Text ( NSLocalizedString ( " GitHub Docs " , comment : " " ) ) . tag ( " github-docs " )
Text ( NSLocalizedString ( " Academic Paper " , comment : " " ) ) . tag ( " academic-paper " )
Text ( NSLocalizedString ( " Terminal Notes " , comment : " " ) ) . tag ( " terminal-notes " )
Text ( NSLocalizedString ( " Magazine " , comment : " " ) ) . tag ( " magazine " )
Text ( NSLocalizedString ( " Minimal Reader " , comment : " " ) ) . tag ( " minimal-reader " )
Text ( NSLocalizedString ( " Presentation " , comment : " " ) ) . tag ( " presentation " )
Text ( NSLocalizedString ( " Night Contrast " , comment : " " ) ) . tag ( " night-contrast " )
Text ( NSLocalizedString ( " Warm Sepia " , comment : " " ) ) . tag ( " warm-sepia " )
Text ( NSLocalizedString ( " Dense Compact " , comment : " " ) ) . tag ( " dense-compact " )
Text ( NSLocalizedString ( " Developer Spec " , comment : " " ) ) . tag ( " developer-spec " )
2026-03-26 18:19:45 +00:00
}
. labelsHidden ( )
. pickerStyle ( . menu )
#if os ( iOS )
2026-03-28 19:13:32 +00:00
. frame ( maxWidth : . infinity , alignment : . center )
2026-03-26 18:19:45 +00:00
#else
. frame ( minWidth : 120 , idealWidth : 190 , maxWidth : 220 )
#endif
}
private var markdownPreviewPDFModePicker : some View {
2026-03-30 17:07:02 +00:00
Picker ( NSLocalizedString ( " PDF Mode " , comment : " " ) , selection : $ markdownPDFExportModeRaw ) {
Text ( NSLocalizedString ( " Paginated Fit " , comment : " " ) ) . tag ( MarkdownPDFExportMode . paginatedFit . rawValue )
Text ( NSLocalizedString ( " One Page Fit " , comment : " " ) ) . tag ( MarkdownPDFExportMode . onePageFit . rawValue )
2026-03-26 18:19:45 +00:00
}
. labelsHidden ( )
. pickerStyle ( . menu )
#if os ( iOS )
2026-03-28 19:13:32 +00:00
. frame ( maxWidth : . infinity , alignment : . center )
2026-03-26 18:19:45 +00:00
#else
. frame ( minWidth : 128 , idealWidth : 160 , maxWidth : 180 )
#endif
}
private var markdownPreviewExportButton : some View {
Button {
exportMarkdownPreviewPDF ( )
} label : {
2026-03-30 17:07:02 +00:00
Label ( NSLocalizedString ( " Export PDF " , comment : " " ) , systemImage : " square.and.arrow.down " )
2026-03-26 18:19:45 +00:00
. lineLimit ( 1 )
. fixedSize ( horizontal : true , vertical : false )
}
. buttonStyle ( . borderedProminent )
. tint ( NeonUIStyle . accentBlue )
. controlSize ( . regular )
. layoutPriority ( 1 )
2026-03-30 17:07:02 +00:00
. accessibilityLabel ( NSLocalizedString ( " Export Markdown preview as PDF " , comment : " " ) )
2026-03-26 18:19:45 +00:00
}
private var markdownPreviewShareButton : some View {
ShareLink (
item : markdownPreviewShareHTML ,
preview : SharePreview ( " \( suggestedMarkdownPreviewBaseName ( ) ) .html " )
) {
2026-03-30 17:07:02 +00:00
Label ( NSLocalizedString ( " Share " , comment : " " ) , systemImage : " square.and.arrow.up " )
2026-03-26 18:19:45 +00:00
. lineLimit ( 1 )
. fixedSize ( horizontal : true , vertical : false )
}
. buttonStyle ( . bordered )
. controlSize ( . regular )
. layoutPriority ( 1 )
2026-03-30 17:07:02 +00:00
. accessibilityLabel ( NSLocalizedString ( " Share Markdown preview HTML " , comment : " " ) )
2026-03-26 18:19:45 +00:00
}
private var markdownPreviewCopyHTMLButton : some View {
Button {
copyMarkdownPreviewHTML ( )
} label : {
2026-03-30 17:07:02 +00:00
Label ( NSLocalizedString ( " Copy HTML " , comment : " " ) , systemImage : " doc.on.doc " )
2026-03-26 18:19:45 +00:00
. lineLimit ( 1 )
. fixedSize ( horizontal : true , vertical : false )
}
. buttonStyle ( . bordered )
. controlSize ( . regular )
. layoutPriority ( 1 )
2026-03-30 17:07:02 +00:00
. accessibilityLabel ( NSLocalizedString ( " Copy Markdown preview HTML " , comment : " " ) )
2026-03-26 18:19:45 +00:00
}
private var markdownPreviewCopyMarkdownButton : some View {
Button {
copyMarkdownPreviewMarkdown ( )
} label : {
2026-03-30 17:07:02 +00:00
Label ( NSLocalizedString ( " Copy Markdown " , comment : " " ) , systemImage : " doc.on.clipboard " )
. lineLimit ( 1 )
. fixedSize ( horizontal : true , vertical : false )
}
. buttonStyle ( . bordered )
. controlSize ( . regular )
. layoutPriority ( 1 )
. accessibilityLabel ( NSLocalizedString ( " Copy Markdown source " , comment : " " ) )
}
private var markdownPreviewExportSummaryText : String {
" \( suggestedMarkdownPDFFilename ( ) ) • \( suggestedMarkdownPreviewBaseName ( ) ) .html "
}
@ ViewBuilder
private var markdownPreviewActionStatusView : some View {
if ! markdownPreviewActionStatusMessage . isEmpty {
Text ( markdownPreviewActionStatusMessage )
. font ( . caption2 . weight ( . semibold ) )
. foregroundStyle ( NeonUIStyle . accentBlue )
. multilineTextAlignment ( . center )
. accessibilityLabel ( NSLocalizedString ( " Markdown preview action status " , comment : " " ) )
. accessibilityValue ( markdownPreviewActionStatusMessage )
}
}
@ ViewBuilder
private var markdownPreviewMoreActionsMenu : some View {
Menu {
Button {
copyMarkdownPreviewHTML ( )
} label : {
Label ( NSLocalizedString ( " Copy HTML " , comment : " " ) , systemImage : " doc.on.doc " )
}
Button {
copyMarkdownPreviewMarkdown ( )
} label : {
Label ( NSLocalizedString ( " Copy Markdown " , comment : " " ) , systemImage : " doc.on.clipboard " )
}
} label : {
Label ( NSLocalizedString ( " More " , comment : " " ) , systemImage : " ellipsis.circle " )
2026-03-26 18:19:45 +00:00
. lineLimit ( 1 )
. fixedSize ( horizontal : true , vertical : false )
}
. buttonStyle ( . bordered )
. controlSize ( . regular )
. layoutPriority ( 1 )
2026-03-30 17:07:02 +00:00
. accessibilityLabel ( NSLocalizedString ( " More Markdown preview actions " , comment : " " ) )
2026-03-26 18:19:45 +00:00
}
@ ViewBuilder
2026-03-28 19:13:32 +00:00
private var markdownPreviewCombinedPickerCard : some View {
2026-03-29 13:57:01 +00:00
Group {
if markdownPreviewUsesStackedIPadPickerLayout {
HStack ( alignment : . top , spacing : markdownPreviewPickerCardSpacing ) {
markdownPreviewPickerColumn ( " Template " ) {
markdownPreviewTemplatePicker
}
markdownPreviewPickerColumn ( " PDF Mode " ) {
markdownPreviewPDFModePicker
}
}
} else {
HStack ( alignment : . top , spacing : markdownPreviewPickerCardSpacing ) {
markdownPreviewPickerColumn ( " Template " ) {
markdownPreviewTemplatePicker
}
if markdownPreviewShowsInlineExportControl {
markdownPreviewPickerColumn ( " Export " ) {
markdownPreviewExportButton
}
}
2026-03-28 19:13:32 +00:00
2026-03-29 13:57:01 +00:00
markdownPreviewPickerColumn ( " PDF Mode " ) {
markdownPreviewPDFModePicker
}
}
2026-03-28 19:13:32 +00:00
}
}
. padding ( . horizontal , markdownPreviewPickerCardHorizontalPadding )
. padding ( . vertical , 16 )
#if os ( iOS )
. frame ( maxWidth : markdownPreviewPickerCardMaxWidth , alignment : . center )
#else
. frame ( minWidth : 460 , maxWidth : 560 , alignment : . center )
2026-03-26 18:19:45 +00:00
#endif
2026-03-28 19:13:32 +00:00
. background ( . regularMaterial , in : RoundedRectangle ( cornerRadius : 16 , style : . continuous ) )
. overlay {
RoundedRectangle ( cornerRadius : 16 , style : . continuous )
. strokeBorder ( Color . white . opacity ( 0.08 ) , lineWidth : 1 )
2026-03-26 18:19:45 +00:00
}
}
2026-03-28 19:13:32 +00:00
#if os ( iOS )
private var markdownPreviewPickerCardSpacing : CGFloat {
2026-03-29 13:57:01 +00:00
if UIDevice . current . userInterfaceIdiom = = . phone {
return 18
}
if markdownPreviewUsesStackedIPadPickerLayout {
return 14
}
return markdownPreviewShowsInlineExportControl ? 10 : 12
2026-03-28 19:13:32 +00:00
}
2026-03-26 18:19:45 +00:00
2026-03-28 19:13:32 +00:00
private var markdownPreviewPickerCardHorizontalPadding : CGFloat {
2026-03-29 13:57:01 +00:00
if UIDevice . current . userInterfaceIdiom = = . phone {
return 18
}
if markdownPreviewUsesStackedIPadPickerLayout {
return 16
}
return markdownPreviewShowsInlineExportControl ? 10 : 12
2026-03-26 18:19:45 +00:00
}
2026-03-28 19:13:32 +00:00
private var markdownPreviewPickerCardMaxWidth : CGFloat ? {
UIDevice . current . userInterfaceIdiom = = . phone ? nil : 420
}
#else
2026-03-29 13:57:01 +00:00
private var markdownPreviewPickerCardSpacing : CGFloat { markdownPreviewShowsInlineExportControl ? 16 : 18 }
private var markdownPreviewPickerCardHorizontalPadding : CGFloat { markdownPreviewShowsInlineExportControl ? 16 : 18 }
#endif
private var markdownPreviewShowsInlineExportControl : Bool {
#if os ( iOS )
false
#else
true
#endif
}
private var markdownPreviewUsesStackedIPadPickerLayout : Bool {
#if os ( iOS )
UIDevice . current . userInterfaceIdiom = = . pad
#else
false
2026-03-28 19:13:32 +00:00
#endif
2026-03-29 13:57:01 +00:00
}
2026-03-28 19:13:32 +00:00
2026-03-26 18:19:45 +00:00
@ ViewBuilder
2026-03-28 19:13:32 +00:00
private func markdownPreviewPickerColumn < Content : View > ( _ title : String , @ ViewBuilder content : ( ) -> Content ) -> some View {
VStack ( spacing : 10 ) {
2026-03-30 17:07:02 +00:00
Text ( NSLocalizedString ( title , comment : " " ) )
2026-03-26 18:19:45 +00:00
. font ( . system ( size : 13 , weight : . semibold ) )
. foregroundStyle ( . secondary )
2026-03-28 19:13:32 +00:00
. frame ( maxWidth : . infinity , alignment : . center )
2026-03-26 18:19:45 +00:00
content ( )
2026-03-28 19:13:32 +00:00
. frame ( maxWidth : . infinity , alignment : . center )
2026-03-26 18:19:45 +00:00
}
2026-03-28 19:13:32 +00:00
. frame ( maxWidth : . infinity , alignment : . center )
2026-03-26 18:19:45 +00:00
}
@ ViewBuilder
private func markdownPreviewActionRow < Content : View > ( @ ViewBuilder content : ( ) -> Content ) -> some View {
HStack ( spacing : 14 ) {
content ( )
}
. frame ( maxWidth : . infinity , alignment : . center )
}
2026-03-28 19:13:32 +00:00
private var markdownPreviewPrimaryActionRow : some View {
markdownPreviewActionRow {
2026-03-29 13:57:01 +00:00
if ! markdownPreviewShowsInlineExportControl {
markdownPreviewExportButton
}
2026-03-28 19:13:32 +00:00
}
}
@ 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
2026-03-30 17:07:02 +00:00
markdownPreviewMoreActionsMenu
2026-03-28 19:13:32 +00:00
}
VStack ( spacing : 10 ) {
markdownPreviewShareButton
2026-03-30 17:07:02 +00:00
markdownPreviewMoreActionsMenu
2026-03-28 19:13:32 +00:00
}
}
} else {
HStack ( spacing : 10 ) {
markdownPreviewShareButton
2026-03-30 17:07:02 +00:00
markdownPreviewMoreActionsMenu
2026-03-28 19:13:32 +00:00
}
}
}
#endif
2026-03-26 18:19:45 +00:00
2026-02-20 10:27:55 +00:00
#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 )
)
}
2026-03-04 19:47:31 +00:00
private var floatingStatusPillText : String {
2026-03-13 14:13:44 +00:00
let base = effectiveLargeFileModeEnabled
? " \( caretStatus ) • Lines: \( statusLineCount ) \( vimStatusSuffix ) "
: " \( caretStatus ) • Lines: \( statusLineCount ) • Words: \( statusWordCount ) \( vimStatusSuffix ) "
2026-03-28 19:13:32 +00:00
let suffixes = [ largeFileStatusBadgeText , remoteSessionStatusBadgeText ] . filter { ! $0 . isEmpty }
if suffixes . isEmpty {
return base
2026-03-04 19:47:31 +00:00
}
2026-03-28 19:13:32 +00:00
return " \( base ) • \( suffixes . joined ( separator : " • " ) ) "
2026-03-04 19:47:31 +00:00
}
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 )
}
2026-02-20 10:27:55 +00:00
#endif
2026-01-25 12:46:33 +00:00
// S t a t u s l i n e : c a r e t l o c a t i o n + l i v e w o r d c o u n t f r o m t h e v i e w m o d e l .
2025-09-25 09:01:45 +00:00
@ ViewBuilder
2026-02-06 18:59:53 +00:00
var wordCountView : some View {
2026-02-08 11:14:49 +00:00
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 )
}
2026-03-13 14:13:44 +00:00
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 " )
2026-02-08 11:14:49 +00:00
}
2026-03-28 19:13:32 +00:00
if ! remoteSessionStatusBadgeText . isEmpty {
remoteSessionBadge
}
2026-03-30 17:07:02 +00:00
if ! selectedRemoteDocumentBadgeText . isEmpty {
selectedRemoteDocumentBadge
}
2025-09-25 09:01:45 +00:00
Spacer ( )
2026-03-13 14:13:44 +00:00
Text ( effectiveLargeFileModeEnabled
? " \( caretStatus ) • Lines: \( statusLineCount ) \( vimStatusSuffix ) "
: " \( caretStatus ) • Lines: \( statusLineCount ) • Words: \( statusWordCount ) \( vimStatusSuffix ) " )
2025-09-25 09:01:45 +00:00
. font ( . system ( size : 12 ) )
. foregroundColor ( . secondary )
. padding ( . bottom , 8 )
. padding ( . trailing , 16 )
2025-08-26 11:22:53 +00:00
}
2026-02-19 14:29:53 +00:00
. background ( editorSurfaceBackgroundStyle )
2026-02-18 22:56:46 +00:00
}
2026-03-13 14:13:44 +00:00
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 )
}
2026-03-28 19:13:32 +00:00
private var remoteSessionBadge : some View {
Text ( remoteSessionStatusBadgeText )
. font ( . system ( size : 11 , weight : . semibold ) )
2026-03-30 17:07:02 +00:00
. foregroundColor ( remoteSessionBadgeForegroundColor )
2026-03-28 19:13:32 +00:00
. padding ( . horizontal , 8 )
. padding ( . vertical , 3 )
. background (
Capsule ( style : . continuous )
2026-03-30 17:07:02 +00:00
. fill ( remoteSessionBadgeBackgroundColor )
2026-03-28 19:13:32 +00:00
)
. accessibilityLabel ( " Remote session status " )
2026-03-30 17:07:02 +00:00
. accessibilityValue ( remoteSessionBadgeAccessibilityValue )
}
private var selectedRemoteDocumentBadgeText : String {
guard let tab = viewModel . selectedTab , tab . isRemoteDocument else { return " " }
return tab . isReadOnlyPreview ? " Remote Document • Read-Only " : " Remote Document • Editable "
}
private var selectedRemoteDocumentBadge : some View {
Text ( selectedRemoteDocumentBadgeText )
. font ( . system ( size : 11 , weight : . semibold ) )
. foregroundColor ( . secondary )
. padding ( . horizontal , 8 )
. padding ( . vertical , 3 )
. background (
Capsule ( style : . continuous )
. fill ( Color . secondary . opacity ( 0.16 ) )
2026-03-28 19:13:32 +00:00
)
2026-03-30 17:07:02 +00:00
. accessibilityLabel ( " Selected document status " )
. accessibilityValue ( selectedRemoteDocumentBadgeText )
2026-03-28 19:13:32 +00:00
}
2026-03-13 14:13:44 +00:00
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 " )
}
}
}
2026-03-30 17:07:02 +00:00
@ ViewBuilder
private func tabRemoteBadge ( for tab : TabData ) -> some View {
if tab . isRemoteDocument {
Text ( " Remote " )
. font ( . system ( size : 9 , weight : . semibold ) )
. foregroundStyle ( viewModel . selectedTabID = = tab . id ? Color . accentColor : Color . secondary )
. padding ( . horizontal , 5 )
. padding ( . vertical , 2 )
. background (
Capsule ( style : . continuous )
. fill ( Color . accentColor . opacity ( viewModel . selectedTabID = = tab . id ? 0.16 : 0.10 ) )
)
}
}
private func tabAccessibilityLabel ( for tab : TabData ) -> String {
var parts : [ String ] = [ tab . name ]
if tab . isRemoteDocument {
parts . append ( tab . isReadOnlyPreview ? " remote read only document " : " remote editable document " )
} else {
parts . append ( " local document " )
}
if tab . isDirty {
parts . append ( " unsaved changes " )
}
return parts . joined ( separator : " , " )
}
2026-02-06 19:20:03 +00:00
@ ViewBuilder
var tabBarView : some View {
2026-02-20 00:31:14 +00:00
VStack ( spacing : 0 ) {
ScrollView ( . horizontal , showsIndicators : false ) {
HStack ( spacing : 6 ) {
if viewModel . tabs . isEmpty {
2026-02-06 19:20:03 +00:00
Button {
2026-02-20 00:31:14 +00:00
viewModel . addNewTab ( )
2026-02-06 19:20:03 +00:00
} label : {
2026-02-20 00:31:14 +00:00
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 ) )
)
2026-02-06 19:20:03 +00:00
}
. buttonStyle ( . plain )
2026-02-20 00:31:14 +00:00
} else {
ForEach ( viewModel . tabs ) { tab in
2026-02-20 23:05:45 +00:00
HStack ( spacing : 8 ) {
2026-02-20 16:34:27 +00:00
Button {
2026-02-25 13:07:05 +00:00
viewModel . selectTab ( id : tab . id )
2026-02-20 16:34:27 +00:00
} label : {
2026-03-29 10:39:47 +00:00
HStack ( spacing : 6 ) {
2026-03-30 17:07:02 +00:00
tabRemoteBadge ( for : tab )
2026-03-29 10:39:47 +00:00
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 )
2026-02-20 16:34:27 +00:00
}
. buttonStyle ( . plain )
2026-03-30 17:07:02 +00:00
. accessibilityLabel ( tabAccessibilityLabel ( for : tab ) )
. accessibilityHint ( " Selects this editor tab. " )
2026-03-10 15:14:21 +00:00
#if os ( macOS )
. simultaneousGesture (
TapGesture ( count : 2 )
. onEnded { requestCloseTab ( tab ) }
)
#endif
2026-02-06 19:20:03 +00:00
2026-02-20 00:31:14 +00:00
Button {
requestCloseTab ( tab )
} label : {
Image ( systemName : " xmark " )
. font ( . system ( size : 10 , weight : . bold ) )
2026-02-20 23:05:45 +00:00
. padding ( . trailing , 10 )
2026-02-20 00:31:14 +00:00
}
. buttonStyle ( . plain )
2026-02-20 23:05:45 +00:00
. contentShape ( Rectangle ( ) )
2026-02-20 00:31:14 +00:00
. help ( " Close \( tab . name ) " )
2026-02-20 15:43:14 +00:00
}
2026-02-20 00:31:14 +00:00
. background (
RoundedRectangle ( cornerRadius : 8 , style : . continuous )
. fill ( viewModel . selectedTabID = = tab . id ? Color . accentColor . opacity ( 0.18 ) : Color . secondary . opacity ( 0.10 ) )
)
2026-02-06 19:20:03 +00:00
}
}
}
2026-02-21 19:12:47 +00:00
. padding ( . leading , tabBarLeadingPadding )
. padding ( . trailing , 10 )
2026-02-20 00:31:14 +00:00
. padding ( . vertical , 6 )
2026-02-06 19:20:03 +00:00
}
2026-03-26 18:19:45 +00:00
#if os ( iOS )
iOSHorizontalSurfaceDivider . opacity ( 0.7 )
#else
2026-02-20 00:31:14 +00:00
Divider ( ) . opacity ( 0.45 )
2026-03-26 18:19:45 +00:00
#endif
2026-02-06 19:20:03 +00:00
}
2026-02-20 00:31:14 +00:00
. frame ( minHeight : 42 , maxHeight : 42 , alignment : . center )
2026-02-07 10:51:52 +00:00
#if os ( macOS )
2026-03-10 15:52:58 +00:00
. background ( editorSurfaceBackgroundStyle )
2026-02-07 10:51:52 +00:00
#else
2026-02-20 00:31:14 +00:00
. background (
enableTranslucentWindow
? AnyShapeStyle ( . ultraThinMaterial )
: ( useIOSUnifiedSolidSurfaces ? AnyShapeStyle ( iOSNonTranslucentSurfaceColor ) : AnyShapeStyle ( Color ( . systemBackground ) ) )
)
2026-02-20 02:41:08 +00:00
. contentShape ( Rectangle ( ) )
. zIndex ( 10 )
2026-02-07 10:51:52 +00:00
#endif
2026-02-06 19:20:03 +00:00
}
2026-02-08 00:06:06 +00:00
private var vimStatusSuffix : String {
#if os ( macOS )
2026-02-08 00:36:06 +00:00
guard vimModeEnabled else { return " • Vim: OFF " }
2026-02-08 00:06:06 +00:00
return vimInsertMode ? " • Vim: INSERT " : " • Vim: NORMAL "
#else
2026-03-26 18:19:45 +00:00
guard UIDevice . current . userInterfaceIdiom = = . pad else { return " " }
guard vimModeEnabled else { return " • Vim: OFF " }
return vimInsertMode ? " • Vim: INSERT " : " • Vim: NORMAL "
2026-02-08 00:06:06 +00:00
#endif
}
2026-02-08 11:14:49 +00:00
private var importProgressPercentText : String {
let clamped = min ( max ( droppedFileLoadProgress , 0 ) , 1 )
if clamped > 0 , clamped < 0.01 { return " 1% " }
return " \( Int ( clamped * 100 ) ) % "
}
2026-02-08 00:06:06 +00:00
private var quickSwitcherItems : [ QuickFileSwitcherPanel . Item ] {
2026-03-15 14:56:58 +00:00
_ = recentFilesRefreshToken
2026-02-08 00:06:06 +00:00
var items : [ QuickFileSwitcherPanel . Item ] = [ ]
let fileURLSet = Set ( viewModel . tabs . compactMap { $0 . fileURL ? . standardizedFileURL . path } )
2026-03-03 09:47:45 +00:00
let commandItems : [ QuickFileSwitcherPanel . Item ] = [
2026-03-15 14:56:58 +00:00
. 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 )
2026-03-03 09:47:45 +00:00
]
items . append ( contentsOf : commandItems )
2026-02-08 00:06:06 +00:00
for tab in viewModel . tabs {
let subtitle = tab . fileURL ? . path ? ? " Open tab "
items . append (
QuickFileSwitcherPanel . Item (
id : " tab: \( tab . id . uuidString ) " ,
title : tab . name ,
2026-03-15 14:56:58 +00:00
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
2026-02-08 00:06:06 +00:00
)
)
}
2026-03-26 18:19:45 +00:00
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
)
2026-02-08 00:06:06 +00:00
)
2026-03-26 18:19:45 +00:00
}
} 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
)
)
}
2026-02-08 00:06:06 +00:00
}
2026-03-03 09:47:45 +00:00
let query = quickSwitcherQuery . trimmingCharacters ( in : . whitespacesAndNewlines )
if query . isEmpty {
return Array (
items
2026-03-15 14:56:58 +00:00
. 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 )
}
2026-03-03 09:47:45 +00:00
. prefix ( 300 )
)
}
let ranked = items . compactMap { item -> ( QuickFileSwitcherPanel . Item , Int ) ? in
guard let score = quickSwitcherMatchScore ( for : item , query : query ) else { return nil }
2026-03-15 14:56:58 +00:00
let pinBoost = item . isPinned ? 400 : 0
return ( item , score + quickSwitcherRecencyScore ( for : item . id ) + pinBoost )
2026-03-03 09:47:45 +00:00
}
. sorted {
if $0 . 1 = = $1 . 1 {
return $0 . 0. title . localizedCaseInsensitiveCompare ( $1 . 0. title ) = = . orderedAscending
2026-02-08 00:06:06 +00:00
}
2026-03-03 09:47:45 +00:00
return $0 . 1 > $1 . 1
}
return Array ( ranked . prefix ( 300 ) . map ( \ . 0 ) )
2026-02-08 00:06:06 +00:00
}
2026-03-30 17:07:02 +00:00
private var quickSwitcherStatusMessage : String {
guard projectRootFolderURL != nil else { return " No project folder is open. " }
if isProjectFileIndexing {
if projectFileIndexSnapshot . entries . isEmpty {
return " Indexing project files for Quick Open… "
}
return " Refreshing indexed project files… "
}
if ! projectFileIndexSnapshot . entries . isEmpty {
let fileCount = projectFileIndexSnapshot . entries . count
return " Using indexed project files ( \( fileCount ) ). "
}
if ! quickSwitcherProjectFileURLs . isEmpty {
return " Using the current project tree until indexing is available. "
}
return " Project files will appear here after the folder is indexed. "
}
2026-02-08 00:06:06 +00:00
private func selectQuickSwitcherItem ( _ item : QuickFileSwitcherPanel . Item ) {
2026-03-03 09:47:45 +00:00
rememberQuickSwitcherSelection ( item . id )
if item . id . hasPrefix ( " cmd: " ) {
performQuickSwitcherCommand ( item . id )
return
}
2026-02-08 00:06:06 +00:00
if item . id . hasPrefix ( " tab: " ) {
let raw = String ( item . id . dropFirst ( 4 ) )
if let id = UUID ( uuidString : raw ) {
2026-02-25 13:07:05 +00:00
viewModel . selectTab ( id : id )
2026-02-08 00:06:06 +00:00
}
return
}
if item . id . hasPrefix ( " file: " ) {
let path = String ( item . id . dropFirst ( 5 ) )
openProjectFile ( url : URL ( fileURLWithPath : path ) )
}
}
2026-03-15 14:56:58 +00:00
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 ( )
}
2026-03-15 16:37:20 +00:00
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 )
}
2026-03-03 09:47:45 +00:00
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 ) )
}
2026-03-30 17:07:02 +00:00
private func quickSwitcherPathComponents ( for item : QuickFileSwitcherPanel . Item ) -> [ String ] {
item . subtitle
. split ( separator : " / " )
. map { String ( $0 ) . lowercased ( ) }
. filter { ! $0 . isEmpty }
}
private func quickSwitcherTitleStem ( for item : QuickFileSwitcherPanel . Item ) -> String {
URL ( fileURLWithPath : item . title ) . deletingPathExtension ( ) . lastPathComponent . lowercased ( )
}
private func quickSwitcherTokenPrefixScore ( for query : String , in value : String , score : Int ) -> Int ? {
let separators = CharacterSet . alphanumerics . inverted
let tokens = value
. components ( separatedBy : separators )
. filter { ! $0 . isEmpty }
return tokens . contains ( where : { $0 . hasPrefix ( query ) } ) ? score : nil
}
private func quickSwitcherQueryTokens ( for query : String ) -> [ String ] {
query
. lowercased ( )
. split ( whereSeparator : { $0 . isWhitespace || $0 = = " / " || $0 = = " _ " || $0 = = " - " || $0 = = " . " } )
. map ( String . init )
. filter { ! $0 . isEmpty }
}
private func quickSwitcherMultiTokenScore (
tokens : [ String ] ,
title : String ,
subtitle : String ,
pathComponents : [ String ]
) -> Int ? {
guard tokens . count > 1 else { return nil }
let titleTokens = title
. components ( separatedBy : CharacterSet . alphanumerics . inverted )
. filter { ! $0 . isEmpty }
let subtitleTokens = subtitle
. components ( separatedBy : CharacterSet . alphanumerics . inverted )
. filter { ! $0 . isEmpty }
let allTitlePrefix = tokens . allSatisfy { queryToken in
titleTokens . contains ( where : { $0 . hasPrefix ( queryToken ) } )
}
if allTitlePrefix {
return 390
}
let allPathPrefix = tokens . allSatisfy { queryToken in
pathComponents . contains ( where : { $0 . hasPrefix ( queryToken ) } )
}
if allPathPrefix {
return 340
}
let allDistributedPrefix = tokens . allSatisfy { queryToken in
titleTokens . contains ( where : { $0 . hasPrefix ( queryToken ) } ) ||
subtitleTokens . contains ( where : { $0 . hasPrefix ( queryToken ) } ) ||
pathComponents . contains ( where : { $0 . hasPrefix ( queryToken ) } )
}
if allDistributedPrefix {
return 300
}
return nil
}
2026-03-03 09:47:45 +00:00
private func quickSwitcherMatchScore ( for item : QuickFileSwitcherPanel . Item , query : String ) -> Int ? {
let normalizedQuery = query . lowercased ( )
2026-03-30 17:07:02 +00:00
let queryTokens = quickSwitcherQueryTokens ( for : query )
2026-03-03 09:47:45 +00:00
let title = item . title . lowercased ( )
let subtitle = item . subtitle . lowercased ( )
2026-03-30 17:07:02 +00:00
let titleStem = quickSwitcherTitleStem ( for : item )
let pathComponents = quickSwitcherPathComponents ( for : item )
if title = = normalizedQuery {
return 420
}
if titleStem = = normalizedQuery {
return 400
}
if let score = quickSwitcherMultiTokenScore (
tokens : queryTokens ,
title : title ,
subtitle : subtitle ,
pathComponents : pathComponents
) {
return score
}
if let score = quickSwitcherTokenPrefixScore ( for : normalizedQuery , in : title , score : 370 ) {
return score
}
2026-03-03 09:47:45 +00:00
if title . hasPrefix ( normalizedQuery ) {
2026-03-30 17:07:02 +00:00
return 350
}
if pathComponents . contains ( normalizedQuery ) {
2026-03-03 09:47:45 +00:00
return 320
}
2026-03-30 17:07:02 +00:00
if pathComponents . contains ( where : { $0 . hasPrefix ( normalizedQuery ) } ) {
return 290
}
2026-03-03 09:47:45 +00:00
if title . contains ( normalizedQuery ) {
return 240
}
2026-03-30 17:07:02 +00:00
if let score = quickSwitcherTokenPrefixScore ( for : normalizedQuery , in : subtitle , score : 210 ) {
return score
}
2026-03-03 09:47:45 +00:00
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
}
2026-02-25 13:07:05 +00:00
private func startFindInFiles ( ) {
guard let root = projectRootFolderURL else {
findInFilesResults = [ ]
findInFilesStatusMessage = " Open a project folder first. "
2026-03-30 17:07:02 +00:00
findInFilesSourceMessage = " "
2026-02-25 13:07:05 +00:00
return
}
let query = findInFilesQuery . trimmingCharacters ( in : . whitespacesAndNewlines )
guard ! query . isEmpty else {
findInFilesResults = [ ]
findInFilesStatusMessage = " Enter a search query. "
2026-03-30 17:07:02 +00:00
findInFilesSourceMessage = " "
2026-02-25 13:07:05 +00:00
return
}
findInFilesTask ? . cancel ( )
2026-03-26 18:19:45 +00:00
let indexedProjectFileURLs = projectFileIndexSnapshot . fileURLs
2026-03-17 17:40:32 +00:00
let candidateFiles = indexedProjectFileURLs . isEmpty ? nil : indexedProjectFileURLs
2026-03-30 17:07:02 +00:00
let searchSourceMessage : String
2026-03-17 17:40:32 +00:00
if candidateFiles = = nil , isProjectFileIndexing {
findInFilesStatusMessage = " Searching while project index updates… "
2026-03-30 17:07:02 +00:00
searchSourceMessage = " Live filesystem scan while the project index refreshes. "
2026-03-17 17:40:32 +00:00
} else {
findInFilesStatusMessage = " Searching… "
2026-03-30 17:07:02 +00:00
if let candidateFiles {
searchSourceMessage = " Searching \( candidateFiles . count ) indexed project files. "
} else {
searchSourceMessage = " Searching the live project tree because no index is available yet. "
}
2026-03-17 17:40:32 +00:00
}
2026-03-30 17:07:02 +00:00
findInFilesSourceMessage = searchSourceMessage
2026-02-25 13:07:05 +00:00
let caseSensitive = findInFilesCaseSensitive
findInFilesTask = Task {
let results = await ContentView . findInFiles (
root : root ,
2026-03-17 17:40:32 +00:00
candidateFiles : candidateFiles ,
2026-02-25 13:07:05 +00:00
query : query ,
caseSensitive : caseSensitive ,
maxResults : 500
)
guard ! Task . isCancelled else { return }
findInFilesResults = results
if results . isEmpty {
findInFilesStatusMessage = " No matches found. "
} else {
2026-03-30 17:07:02 +00:00
findInFilesStatusMessage = String . localizedStringWithFormat (
NSLocalizedString ( " %lld matches " , comment : " " ) ,
Int64 ( results . count )
)
2026-02-25 13:07:05 +00:00
}
2026-03-30 17:07:02 +00:00
findInFilesSourceMessage = searchSourceMessage
2026-02-25 13:07:05 +00:00
}
}
2026-03-30 17:07:02 +00:00
private func clearFindInFiles ( ) {
findInFilesTask ? . cancel ( )
findInFilesQuery = " "
findInFilesResults = [ ]
findInFilesStatusMessage = " "
findInFilesSourceMessage = " "
}
2026-02-25 13:07:05 +00:00
private func selectFindInFilesMatch ( _ match : FindInFilesMatch ) {
openProjectFile ( url : match . fileURL )
var userInfo : [ String : Any ] = [
EditorCommandUserInfo . rangeLocation : match . rangeLocation ,
2026-03-30 17:07:02 +00:00
EditorCommandUserInfo . rangeLength : match . rangeLength ,
EditorCommandUserInfo . focusEditor : true
2026-02-25 13:07:05 +00:00
]
#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 )
}
}
2026-02-19 14:29:53 +00:00
private func scheduleWordCountRefresh ( for text : String ) {
let snapshot = text
2026-03-13 14:13:44 +00:00
let shouldSkipWordCount = effectiveLargeFileModeEnabled || currentDocumentUTF16Length >= 300_000
2026-02-19 14:29:53 +00:00
wordCountTask ? . cancel ( )
wordCountTask = Task ( priority : . utility ) {
try ? await Task . sleep ( nanoseconds : 80_000_000 )
guard ! Task . isCancelled else { return }
2026-03-13 14:13:44 +00:00
let lineCount = Self . lineCount ( for : snapshot )
let wordCount = shouldSkipWordCount ? 0 : viewModel . wordCount ( for : snapshot )
2026-02-19 14:29:53 +00:00
await MainActor . run {
2026-03-13 14:13:44 +00:00
statusLineCount = lineCount
statusWordCount = wordCount
2026-02-19 14:29:53 +00:00
}
}
}
2026-01-17 11:11:26 +00:00
}