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-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
}
let startupBehavior : StartupBehavior
init ( startupBehavior : StartupBehavior = . standard ) {
self . startupBehavior = startupBehavior
}
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-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-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-02-22 13:31:05 +00:00
#if os ( iOS )
private struct IOSSavedDraftTab : Codable {
let name : String
let content : String
let language : String
let fileURLString : String ?
}
private struct IOSSavedDraftSnapshot : Codable {
let tabs : [ IOSSavedDraftTab ]
let selectedIndex : Int ?
}
#endif
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 "
@ AppStorage ( " SettingsTemplateLanguage " ) private var settingsTemplateLanguage : String = " swift "
@ AppStorage ( " SettingsThemeName " ) private var settingsThemeName : String = " Neon Glow "
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
#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-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-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-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
@ State var iosExportDocument : PlainTextDocument = PlainTextDocument ( text : " " )
@ State var iosExportFilename : String = " Untitled.txt "
@ State var iosExportTabID : UUID ? = nil
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-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 = " "
@ State private var findInFilesTask : Task < Void , Never > ?
2026-02-19 14:29:53 +00:00
@ State private var statusWordCount : Int = 0
@ State private var wordCountTask : Task < Void , Never > ?
2026-02-08 00:06:06 +00:00
@ State var vimModeEnabled : Bool = UserDefaults . standard . bool ( forKey : " EditorVimModeEnabled " )
@ State var vimInsertMode : Bool = true
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-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-02-08 00:06:06 +00:00
@ State var showWelcomeTour : 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-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-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-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
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-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 {
case . subtle : return 0.98
case . balanced : return 0.93
case . vibrant : return 0.90
}
}
}
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 ) )
}
private var macChromeBackgroundStyle : AnyShapeStyle {
if enableTranslucentWindow {
return macUnifiedTranslucentMaterialStyle
}
return AnyShapeStyle ( Color ( nsColor : . textBackgroundColor ) )
}
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
}
return AnyShapeStyle ( Color ( nsColor : . textBackgroundColor ) )
#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 {
#if os ( iOS )
canShowMarkdownPreviewOnCurrentDevice
#else
true
#endif
}
private var settingsSheetDetents : Set < PresentationDetent > {
#if os ( iOS )
if UIDevice . current . userInterfaceIdiom = = . pad {
return [ . fraction ( 0.96 ) ]
}
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
}
// M o d e l - b a c k e d c o m p l e t i o n a t t e m p t
let doc = textView . string
2026-02-18 18:59:25 +00:00
// 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 .
2026-01-25 19:02:59 +00:00
let nsDoc = doc as NSString
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-02-18 19:19:49 +00:00
storeCompletionInCache ( suggestion , for : cacheKey )
2026-02-18 18:59:25 +00:00
2026-02-18 19:19:49 +00:00
applyInlineSuggestion ( suggestion , 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-02-19 08:09:35 +00:00
if largeFileModeEnabled { 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 ) )
let triggerChars : Set < String > = [ " . " , " ( " , " ) " , " { " , " } " , " [ " , " ] " , " : " , " , " , " \n " , " \t " , " " ]
if triggerChars . contains ( prevChar ) { return true }
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 {
// R e m o v e c o d e f e n c e s a n d p r o s e , k e e p f i r s t f e w l i n e s o f c o d e o n l y
var result = raw . trimmingCharacters ( in : . whitespacesAndNewlines )
// R e m o v e o p e n i n g a n d c l o s i n g c o d e f e n c e s i f p r e s e n t
while result . hasPrefix ( " ``` " ) {
if let fenceEndIndex = result . firstIndex ( of : " \n " ) {
result = String ( result [ fenceEndIndex . . . ] ) . trimmingCharacters ( in : . whitespacesAndNewlines )
} else {
break
}
}
if let closingFenceRange = result . range ( of : " ``` " ) {
result = String ( result [ . . < closingFenceRange . lowerBound ] ) . trimmingCharacters ( in : . whitespacesAndNewlines )
}
2026-02-09 10:21:50 +00:00
// K e e p a s i n g l e l i n e o n l y
if let firstLine = result . components ( separatedBy : . newlines ) . first {
result = firstLine
}
// T r i m l e a d i n g w h i t e s p a c e s o t h e g h o s t t e x t a l i g n s a t t h e c a r e t
result = result . trimmingCharacters ( in : . whitespacesAndNewlines )
// K e e p t h e c o m p l e t i o n s h o r t a n d c o d e - l i k e
if result . count > 40 {
let idx = result . index ( result . startIndex , offsetBy : 40 )
result = String ( result [ . . < idx ] )
if let lastSpace = result . lastIndex ( of : " " ) {
result = String ( result [ . . < lastSpace ] )
}
}
// F i l t e r o u t s u g g e s t i o n s t h a t a r e m o s t l y p r o s e
let allowed = CharacterSet ( charactersIn : " abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_()[]{}.,;:+-/*=<>!|&%? \" '` \t " )
if result . unicodeScalars . contains ( where : { ! allowed . contains ( $0 ) } ) {
return " "
2026-01-25 19:02:59 +00:00
}
2026-01-25 12:46:33 +00:00
2026-01-25 19:02:59 +00:00
return result
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-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-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
Button ( token ) {
requestBracketHelperInsert ( token )
}
. buttonStyle ( . plain )
. font ( . system ( size : 13 , weight : . semibold , design : . monospaced ) )
. padding ( . horizontal , 10 )
. padding ( . vertical , 5 )
. background (
RoundedRectangle ( cornerRadius : 8 , style : . continuous )
. fill ( Color . accentColor . opacity ( 0.14 ) )
)
}
}
. 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
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-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
return viewWithClipboardEvents
. onReceive ( NotificationCenter . default . publisher ( for : . droppedFileLoadStarted ) ) { notif in
droppedFileLoadInProgress = true
droppedFileProgressDeterminate = ( notif . userInfo ? [ " isDeterminate " ] as ? Bool ) ? ? true
droppedFileLoadProgress = 0
droppedFileLoadLabel = " Reading file "
largeFileModeEnabled = ( notif . userInfo ? [ " largeFileMode " ] as ? Bool ) ? ? false
}
. onReceive ( NotificationCenter . default . publisher ( for : . droppedFileLoadProgress ) ) { notif in
// 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
}
}
. onChange ( of : viewModel . selectedTab ? . id ) { _ , _ in
2026-02-20 19:32:03 +00:00
if viewModel . selectedTab ? . isLargeFileCandidate = = true {
if ! largeFileModeEnabled {
largeFileModeEnabled = true
}
} else {
updateLargeFileMode ( for : currentContentBinding . wrappedValue )
}
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-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 {
updateLargeFileMode ( for : currentContentBinding . wrappedValue )
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 {
updateLargeFileMode ( for : currentContentBinding . wrappedValue )
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-02-11 10:20:17 +00:00
updateLargeFileMode ( for : currentContentBinding . wrappedValue )
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 {
updateLargeFileMode ( for : currentContentBinding . wrappedValue )
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-02-20 19:32:03 +00:00
if viewModel . selectedTab ? . isLargeFileCandidate = = true {
if ! largeFileModeEnabled {
largeFileModeEnabled = true
scheduleHighlightRefresh ( )
}
return
}
let lowerLanguage = currentLanguage . lowercased ( )
let isHTMLLike = [ " html " , " htm " , " xml " , " svg " , " xhtml " ] . contains ( lowerLanguage )
let isCSVLike = [ " csv " , " tsv " ] . contains ( lowerLanguage )
let useAggressiveThresholds = isHTMLLike || isCSVLike
let byteThreshold = useAggressiveThresholds
? EditorPerformanceThresholds . largeFileBytesHTMLCSV
: EditorPerformanceThresholds . largeFileBytes
let lineThreshold = useAggressiveThresholds
? EditorPerformanceThresholds . largeFileLineBreaksHTMLCSV
: EditorPerformanceThresholds . largeFileLineBreaks
let byteCount = text . utf8 . count
let exceedsByteThreshold = byteCount >= byteThreshold
let exceedsLineThreshold : Bool = {
if exceedsByteThreshold { return true }
var lineBreaks = 0
for codeUnit in text . utf16 {
if codeUnit = = 10 { // ' \ n '
lineBreaks += 1
if lineBreaks >= lineThreshold {
return true
}
}
}
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-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
}
. 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
let viewWithPanels = viewWithEditorActions
. 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-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-02-20 01:16:58 +00:00
. 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-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
}
. navigationSplitViewColumnWidth ( min : 200 , ideal : 250 , max : 600 )
2026-02-20 00:31:14 +00:00
. background (
enableTranslucentWindow
? AnyShapeStyle ( . ultraThinMaterial )
: ( useIOSUnifiedSolidSurfaces ? AnyShapeStyle ( iOSNonTranslucentSurfaceColor ) : AnyShapeStyle ( Color . clear ) )
)
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 )
)
. onDisappear {
handleWindowDisappear ( )
2026-02-19 08:09:35 +00:00
}
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-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-02-19 14:29:53 +00:00
#endif
2026-02-25 13:07:05 +00:00
}
private var lifecycleConfiguredRootView : some View {
basePlatformRootView
. onAppear {
handleSettingsAndEditorDefaultsOnAppear ( )
2026-02-19 14:29:53 +00:00
}
2026-02-25 13:07:05 +00:00
. onChange ( of : settingsLineWrapEnabled ) { _ , enabled in
if viewModel . isLineWrapEnabled != enabled {
viewModel . isLineWrapEnabled = enabled
}
2026-02-18 22:56:46 +00:00
}
2026-02-25 13:07:05 +00:00
. onReceive ( NotificationCenter . default . publisher ( for : . whitespaceScalarInspectionResult ) ) { notif in
guard matchesCurrentWindow ( notif ) else { return }
if let msg = notif . userInfo ? [ EditorCommandUserInfo . inspectionMessage ] as ? String {
whitespaceInspectorMessage = msg
}
2026-02-18 22:56:46 +00:00
}
2026-02-25 13:07:05 +00:00
. onChange ( of : viewModel . isLineWrapEnabled ) { _ , enabled in
if settingsLineWrapEnabled != enabled {
settingsLineWrapEnabled = enabled
}
2026-02-18 22:56:46 +00:00
}
2026-02-25 13:07:05 +00:00
. onChange ( of : appUpdateManager . automaticPromptToken ) { _ , _ in
if appUpdateManager . consumeAutomaticPromptIfNeeded ( ) {
showUpdaterDialog ( checkNow : false )
}
2026-02-18 22:56:46 +00:00
}
2026-02-25 13:07:05 +00:00
. onChange ( of : settingsThemeName ) { _ , _ in
scheduleHighlightRefresh ( )
}
. onChange ( of : highlightMatchingBrackets ) { _ , _ in
scheduleHighlightRefresh ( )
}
. onChange ( of : showScopeGuides ) { _ , _ in
scheduleHighlightRefresh ( )
}
. onChange ( of : highlightScopeBackground ) { _ , _ in
scheduleHighlightRefresh ( )
}
. onChange ( of : viewModel . isLineWrapEnabled ) { _ , _ in
scheduleHighlightRefresh ( )
}
. onChange ( of : viewModel . tabsObservationToken ) { _ , _ in
persistSessionIfReady ( )
2026-02-22 13:31:05 +00:00
#if os ( iOS )
2026-02-25 13:07:05 +00:00
persistUnsavedDraftSnapshotIfNeeded ( )
2026-02-22 13:31:05 +00:00
#endif
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-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-02-25 13:07:05 +00:00
viewModel . isLineWrapEnabled = settingsLineWrapEnabled
syncAppleCompletionAvailability ( )
}
2026-02-12 22:20:39 +00:00
2026-02-25 13:07:05 +00:00
private func handleStartupOnAppear ( ) {
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
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-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 ( )
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-02-19 08:09:35 +00:00
if largeFileModeEnabled { 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
func body ( content : Content ) -> some View {
content
. sheet ( isPresented : contentView . $ showFindReplace ) {
FindReplacePanel (
findQuery : contentView . $ findQuery ,
replaceQuery : contentView . $ replaceQuery ,
useRegex : contentView . $ findUsesRegex ,
caseSensitive : contentView . $ findCaseSensitive ,
statusMessage : contentView . $ findStatusMessage ,
onFindNext : { contentView . findNext ( ) } ,
onReplace : { contentView . replaceSelection ( ) } ,
onReplaceAll : { contentView . replaceAll ( ) }
)
#if canImport ( UIKit )
. frame ( maxWidth : 420 )
#if os ( iOS )
. presentationDetents ( [ . height ( 280 ) , . medium ] )
. presentationDragIndicator ( . visible )
. presentationContentInteraction ( . scrolls )
#endif
#else
. frame ( width : 420 )
#endif
}
#if canImport ( UIKit )
. sheet ( isPresented : contentView . $ showSettingsSheet ) {
NeonSettingsView (
supportsOpenInTabs : false ,
supportsTranslucency : false
)
. environmentObject ( contentView . supportPurchaseManager )
#if os ( iOS )
2026-02-28 19:48:06 +00:00
. presentationDetents ( contentView . settingsSheetDetents )
2026-02-14 13:24:01 +00:00
. presentationDragIndicator ( . visible )
. presentationContentInteraction ( . scrolls )
#endif
}
#endif
#if os ( iOS )
. sheet ( isPresented : contentView . $ showCompactSidebarSheet ) {
NavigationStack {
2026-02-19 14:29:53 +00:00
SidebarView (
content : contentView . currentContent ,
language : contentView . currentLanguage ,
translucentBackgroundEnabled : false
)
2026-02-14 13:24:01 +00:00
. navigationTitle ( " Sidebar " )
. toolbar {
ToolbarItem ( placement : . topBarTrailing ) {
Button ( " Done " ) {
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 ,
translucentBackgroundEnabled : false ,
onOpenFile : { contentView . openFileFromToolbar ( ) } ,
onOpenFolder : { contentView . openProjectFolder ( ) } ,
onOpenProjectFile : { contentView . openProjectFile ( url : $0 ) } ,
onRefreshTree : { contentView . refreshProjectTree ( ) }
)
. navigationTitle ( " Project Structure " )
. toolbar {
ToolbarItem ( placement : . topBarTrailing ) {
Button ( " Done " ) {
contentView . $ showCompactProjectSidebarSheet . wrappedValue = false
}
}
}
}
. presentationDetents ( [ . medium , . large ] )
}
2026-02-14 13:24:01 +00:00
#endif
#if canImport ( UIKit )
. sheet ( isPresented : contentView . $ showProjectFolderPicker ) {
ProjectFolderPicker (
onPick : { url in
contentView . setProjectFolder ( url )
contentView . $ showProjectFolderPicker . wrappedValue = false
} ,
onCancel : { contentView . $ showProjectFolderPicker . wrappedValue = false }
)
}
#endif
. sheet ( isPresented : contentView . $ showQuickSwitcher ) {
QuickFileSwitcherPanel (
query : contentView . $ quickSwitcherQuery ,
items : contentView . quickSwitcherItems ,
onSelect : { contentView . selectQuickSwitcherItem ( $0 ) }
)
}
2026-02-25 13:07:05 +00:00
. sheet ( isPresented : contentView . $ showFindInFiles ) {
FindInFilesPanel (
query : contentView . $ findInFilesQuery ,
caseSensitive : contentView . $ findInFilesCaseSensitive ,
results : contentView . findInFilesResults ,
statusMessage : contentView . findInFilesStatusMessage ,
onSearch : { contentView . startFindInFiles ( ) } ,
onSelect : { contentView . selectFindInFilesMatch ( $0 ) }
)
}
2026-02-14 13:24:01 +00:00
. sheet ( isPresented : contentView . $ showLanguageSetupPrompt ) {
contentView . languageSetupSheet
}
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-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. " )
}
}
. 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. " )
}
#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-02-12 22:20:39 +00:00
private func applyStartupBehaviorIfNeeded ( ) {
guard ! didApplyStartupBehavior else { return }
2026-02-28 18:26:00 +00:00
if startupBehavior = = . forceBlankDocument {
viewModel . resetTabsForSessionRestore ( )
viewModel . addNewTab ( )
projectRootFolderURL = nil
projectTreeNodes = [ ]
quickSwitcherProjectFileURLs = [ ]
didApplyStartupBehavior = true
persistSessionIfReady ( )
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-02-27 17:13:12 +00:00
if openWithBlankDocument {
viewModel . resetTabsForSessionRestore ( )
viewModel . addNewTab ( )
projectRootFolderURL = nil
projectTreeNodes = [ ]
quickSwitcherProjectFileURLs = [ ]
didApplyStartupBehavior = true
persistSessionIfReady ( )
return
}
#if os ( iOS )
if restoreUnsavedDraftSnapshotIfAvailable ( ) {
2026-02-12 22:20:39 +00:00
didApplyStartupBehavior = true
persistSessionIfReady ( )
return
}
2026-02-27 17:13:12 +00:00
#endif
2026-02-12 22:20:39 +00:00
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 )
}
if viewModel . tabs . isEmpty {
viewModel . addNewTab ( )
}
}
}
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-02-12 22:20:39 +00:00
didApplyStartupBehavior = true
persistSessionIfReady ( )
}
private func persistSessionIfReady ( ) {
guard didApplyStartupBehavior 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-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-02-25 15:19:58 +00:00
private var lastSessionProjectFolderURLKey : String { " LastSessionProjectFolderURL " }
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-02-22 12:38:31 +00:00
#if os ( iOS )
2026-02-22 13:31:05 +00:00
private var unsavedDraftSnapshotKey : String { " IOSUnsavedDraftSnapshotV1 " }
2026-02-22 12:38:31 +00:00
private var lastSessionBookmarksKey : String { " LastSessionFileBookmarks " }
private var lastSessionSelectedBookmarkKey : String { " LastSessionSelectedFileBookmark " }
2026-02-25 15:19:58 +00:00
private var lastSessionProjectFolderBookmarkKey : String { " LastSessionProjectFolderBookmark " }
2026-02-22 13:31:05 +00:00
private var maxPersistedDraftTabs : Int { 20 }
private var maxPersistedDraftUTF16Length : Int { 2_000_000 }
private func persistUnsavedDraftSnapshotIfNeeded ( ) {
let dirtyTabs = viewModel . tabs . filter ( \ . isDirty )
guard ! dirtyTabs . isEmpty else {
UserDefaults . standard . removeObject ( forKey : unsavedDraftSnapshotKey )
return
}
var savedTabs : [ IOSSavedDraftTab ] = [ ]
savedTabs . reserveCapacity ( min ( dirtyTabs . count , maxPersistedDraftTabs ) )
for tab in dirtyTabs . prefix ( maxPersistedDraftTabs ) {
let content = tab . content
let nsContent = content as NSString
let clampedContent : String
if nsContent . length > maxPersistedDraftUTF16Length {
clampedContent = nsContent . substring ( to : maxPersistedDraftUTF16Length )
} else {
clampedContent = content
}
savedTabs . append (
IOSSavedDraftTab (
name : tab . name ,
content : clampedContent ,
language : tab . language ,
fileURLString : tab . fileURL ? . absoluteString
)
)
}
let selectedIndex : Int ? = {
guard let selectedID = viewModel . selectedTabID else { return nil }
return dirtyTabs . firstIndex ( where : { $0 . id = = selectedID } )
} ( )
let snapshot = IOSSavedDraftSnapshot ( tabs : savedTabs , selectedIndex : selectedIndex )
guard let encoded = try ? JSONEncoder ( ) . encode ( snapshot ) else { return }
UserDefaults . standard . set ( encoded , forKey : unsavedDraftSnapshotKey )
}
private func restoreUnsavedDraftSnapshotIfAvailable ( ) -> Bool {
guard let data = UserDefaults . standard . data ( forKey : unsavedDraftSnapshotKey ) ,
let snapshot = try ? JSONDecoder ( ) . decode ( IOSSavedDraftSnapshot . self , from : data ) ,
! snapshot . tabs . isEmpty else {
return false
}
let restoredTabs = snapshot . tabs . 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 ,
lastSavedFingerprint : nil
)
}
2026-02-25 13:07:05 +00:00
viewModel . restoreTabsFromSnapshot ( restoredTabs , selectedIndex : snapshot . selectedIndex )
2026-02-22 13:31:05 +00:00
return true
}
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 ) {
Divider ( )
}
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-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
}
private var sidebarTOCContent : String {
if largeFileModeEnabled || currentDocumentUTF16Length >= 400_000 {
return " "
}
return currentContent
}
2026-02-20 10:27:55 +00:00
private var brainDumpLayoutEnabled : Bool {
#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 )
}
private var languageOptions : [ String ] {
2026-02-13 11:02:39 +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 " , " bash " , " zsh " , " powershell " , " standard " , " plain " ]
2026-02-09 10:21:50 +00:00
}
private func languageLabel ( for lang : String ) -> String {
switch lang {
case " php " : return " PHP "
case " cobol " : return " COBOL "
case " dotenv " : return " Dotenv "
case " proto " : return " Proto "
case " graphql " : return " GraphQL "
case " rst " : return " reStructuredText "
case " nginx " : return " Nginx "
case " objective-c " : return " Objective-C "
case " csharp " : return " C# "
case " c " : return " C "
case " cpp " : return " C++ "
case " json " : return " JSON "
case " xml " : return " XML "
case " yaml " : return " YAML "
case " toml " : return " TOML "
case " csv " : return " CSV "
case " ini " : return " INI "
case " sql " : return " SQL "
case " vim " : return " Vim "
case " log " : return " Log "
case " ipynb " : return " Jupyter Notebook "
case " 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
}
}
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 "
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-02-13 11:02:39 +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 " , " 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 "
}
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-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 ( )
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-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-02-06 18:59:53 +00:00
// S i n g l e e d i t o r ( n o T a b V i e w )
CustomTextEditor (
text : currentContentBinding ,
2026-02-25 00:21:58 +00:00
documentID : viewModel . selectedTabID ,
2026-02-28 18:26:00 +00:00
externalEditRevision : editorExternalMutationRevision ,
2026-02-06 18:59:53 +00:00
language : currentLanguage ,
colorScheme : colorScheme ,
fontSize : editorFontSize ,
2026-02-25 13:07:05 +00:00
isLineWrapEnabled : $ bindableViewModel . isLineWrapEnabled ,
2026-02-08 11:14:49 +00:00
isLargeFileMode : largeFileModeEnabled ,
2026-02-11 10:20:17 +00:00
translucentBackgroundEnabled : enableTranslucentWindow ,
2026-02-19 14:29:53 +00:00
showKeyboardAccessoryBar : {
#if os ( iOS )
showKeyboardAccessoryBarIOS
#else
true
#endif
} ( ) ,
2026-02-11 10:20:17 +00:00
showLineNumbers : showLineNumbers ,
showInvisibleCharacters : false ,
highlightCurrentLine : highlightCurrentLine ,
2026-02-18 19:19:49 +00:00
highlightMatchingBrackets : effectiveBracketHighlight ,
showScopeGuides : effectiveScopeGuides ,
highlightScopeBackground : effectiveScopeBackground ,
2026-02-11 10:20:17 +00:00
indentStyle : indentStyle ,
indentWidth : indentWidth ,
autoIndentEnabled : autoIndentEnabled ,
autoCloseBracketsEnabled : autoCloseBracketsEnabled ,
2026-02-20 22:04:03 +00:00
highlightRefreshToken : highlightRefreshToken ,
2026-02-25 13:07:05 +00:00
isTabLoadingContent : viewModel . selectedTab ? . isLoadingContent ? ? false ,
onTextMutation : { mutation in
viewModel . applyTabContentEdit (
tabID : mutation . documentID ,
range : mutation . range ,
replacement : mutation . replacement
)
}
2026-02-06 18:59:53 +00:00
)
. id ( currentLanguage )
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-02-20 10:27:55 +00:00
if ! brainDumpLayoutEnabled {
2026-02-06 18:59:53 +00:00
wordCountView
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-02-28 19:48:06 +00:00
if canShowMarkdownPreviewPane && showMarkdownPreviewPane && currentLanguage = = " markdown " && ! brainDumpLayoutEnabled {
2026-02-24 14:44:43 +00:00
Divider ( )
markdownPreviewPane
. frame ( minWidth : 280 , idealWidth : 420 , maxWidth : 680 , maxHeight : . infinity )
}
2026-02-20 10:27:55 +00:00
if showProjectStructureSidebar && ! brainDumpLayoutEnabled {
2026-02-19 14:29:53 +00:00
#if os ( macOS )
VStack ( spacing : 0 ) {
Rectangle ( )
. fill ( macChromeBackgroundStyle )
. frame ( height : macTabBarStripHeight )
ProjectStructureSidebarView (
rootFolderURL : projectRootFolderURL ,
nodes : projectTreeNodes ,
selectedFileURL : viewModel . selectedTab ? . fileURL ,
translucentBackgroundEnabled : enableTranslucentWindow ,
onOpenFile : { openFileFromToolbar ( ) } ,
onOpenFolder : { openProjectFolder ( ) } ,
onOpenProjectFile : { openProjectFile ( url : $0 ) } ,
onRefreshTree : { refreshProjectTree ( ) }
)
}
. frame ( minWidth : 220 , idealWidth : 260 , maxWidth : 340 )
#else
2026-02-06 18:59:53 +00:00
ProjectStructureSidebarView (
rootFolderURL : projectRootFolderURL ,
2026-02-19 08:09:35 +00:00
nodes : projectTreeNodes ,
2026-02-06 18:59:53 +00:00
selectedFileURL : viewModel . selectedTab ? . fileURL ,
2026-02-19 08:09:35 +00:00
translucentBackgroundEnabled : enableTranslucentWindow ,
2026-02-07 10:51:52 +00:00
onOpenFile : { openFileFromToolbar ( ) } ,
2026-02-06 18:59:53 +00:00
onOpenFolder : { openProjectFolder ( ) } ,
onOpenProjectFile : { openProjectFile ( url : $0 ) } ,
2026-02-19 08:09:35 +00:00
onRefreshTree : { refreshProjectTree ( ) }
2026-02-06 18:59:53 +00:00
)
. frame ( minWidth : 220 , idealWidth : 260 , maxWidth : 340 )
2026-02-19 14:29:53 +00:00
#endif
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 )
useIOSUnifiedSolidSurfaces ? iOSNonTranslucentSurfaceColor : Color . clear
#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 {
scheduleWordCountRefresh ( for : currentContent )
}
. onChange ( of : currentContent ) { _ , newValue in
scheduleWordCountRefresh ( for : newValue )
}
. onDisappear {
wordCountTask ? . cancel ( )
}
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
if newClass != . regular && showMarkdownPreviewPane {
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 .
highlightRefreshToken &+= 1
}
#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-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 (
macChromeBackgroundStyle ,
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-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 ) {
HStack {
Text ( " Markdown Preview " )
. font ( . headline )
Spacer ( )
Picker ( " Template " , selection : $ markdownPreviewTemplateRaw ) {
Text ( " Default " ) . tag ( " default " )
Text ( " Docs " ) . tag ( " docs " )
Text ( " Article " ) . tag ( " article " )
Text ( " Compact " ) . tag ( " compact " )
}
. labelsHidden ( )
. pickerStyle ( . menu )
. frame ( width : 120 )
}
. padding ( . horizontal , 12 )
. padding ( . vertical , 10 )
. background ( editorSurfaceBackgroundStyle )
MarkdownPreviewWebView ( html : markdownPreviewHTML ( from : currentContent ) )
. accessibilityLabel ( " Markdown Preview Content " )
}
. background ( editorSurfaceBackgroundStyle )
}
private var markdownPreviewTemplate : String {
switch markdownPreviewTemplateRaw {
case " docs " , " article " , " compact " :
return markdownPreviewTemplateRaw
default :
return " default "
}
}
private func markdownPreviewHTML ( from markdownText : String ) -> String {
let bodyHTML = renderedMarkdownBodyHTML ( from : markdownText ) ? ? " <pre> \( escapedHTML ( markdownText ) ) </pre> "
return " " "
<! doctype html >
< html lang = " en " >
< head >
< meta charset = " utf-8 " >
< meta name = " viewport " content = " width=device-width, initial-scale=1 " >
< style >
\ ( markdownPreviewCSS ( template : markdownPreviewTemplate ) )
</ style >
</ head >
< body class = " \( markdownPreviewTemplate ) " >
< main class = " content " >
\ ( bodyHTML )
</ main >
</ body >
</ html >
" " "
}
private func renderedMarkdownBodyHTML ( from markdownText : String ) -> String ? {
let html = simpleMarkdownToHTML ( markdownText ) . trimmingCharacters ( in : . whitespacesAndNewlines )
return html . isEmpty ? nil : html
}
private func simpleMarkdownToHTML ( _ markdown : String ) -> String {
let lines = markdown . replacingOccurrences ( of : " \r \n " , with : " \n " ) . components ( separatedBy : " \n " )
var result : [ String ] = [ ]
var paragraphLines : [ String ] = [ ]
var insideCodeFence = false
var codeFenceLanguage : String ?
var insideUnorderedList = false
var insideOrderedList = false
var insideBlockquote = false
func flushParagraph ( ) {
guard ! paragraphLines . isEmpty else { return }
let paragraph = paragraphLines . map { inlineMarkdownToHTML ( $0 ) } . joined ( separator : " <br/> " )
result . append ( " <p> \( paragraph ) </p> " )
paragraphLines . removeAll ( keepingCapacity : true )
}
func closeLists ( ) {
if insideUnorderedList {
result . append ( " </ul> " )
insideUnorderedList = false
}
if insideOrderedList {
result . append ( " </ol> " )
insideOrderedList = false
}
}
func closeBlockquote ( ) {
if insideBlockquote {
flushParagraph ( )
closeLists ( )
result . append ( " </blockquote> " )
insideBlockquote = false
}
}
func closeParagraphAndInlineContainers ( ) {
flushParagraph ( )
closeLists ( )
}
for rawLine in lines {
let line = rawLine
let trimmed = line . trimmingCharacters ( in : . whitespaces )
if trimmed . hasPrefix ( " ``` " ) {
if insideCodeFence {
result . append ( " </code></pre> " )
insideCodeFence = false
codeFenceLanguage = nil
} else {
closeBlockquote ( )
closeParagraphAndInlineContainers ( )
insideCodeFence = true
let lang = String ( trimmed . dropFirst ( 3 ) ) . trimmingCharacters ( in : . whitespaces )
codeFenceLanguage = lang . isEmpty ? nil : lang
if let codeFenceLanguage {
result . append ( " <pre><code class= \" language- \( escapedHTML ( codeFenceLanguage ) ) \" > " )
} else {
result . append ( " <pre><code> " )
}
}
continue
}
if insideCodeFence {
result . append ( " \( escapedHTML ( line ) ) \n " )
continue
}
if trimmed . isEmpty {
closeParagraphAndInlineContainers ( )
closeBlockquote ( )
continue
}
if let heading = markdownHeading ( from : trimmed ) {
closeBlockquote ( )
closeParagraphAndInlineContainers ( )
result . append ( " <h \( heading . level ) > \( inlineMarkdownToHTML ( heading . text ) ) </h \( heading . level ) > " )
continue
}
if isMarkdownHorizontalRule ( trimmed ) {
closeBlockquote ( )
closeParagraphAndInlineContainers ( )
result . append ( " <hr/> " )
continue
}
var workingLine = trimmed
let isBlockquoteLine = workingLine . hasPrefix ( " > " )
if isBlockquoteLine {
if ! insideBlockquote {
closeParagraphAndInlineContainers ( )
result . append ( " <blockquote> " )
insideBlockquote = true
}
workingLine = workingLine . dropFirst ( ) . trimmingCharacters ( in : . whitespaces )
} else {
closeBlockquote ( )
}
if let unordered = markdownUnorderedListItem ( from : workingLine ) {
flushParagraph ( )
if insideOrderedList {
result . append ( " </ol> " )
insideOrderedList = false
}
if ! insideUnorderedList {
result . append ( " <ul> " )
insideUnorderedList = true
}
result . append ( " <li> \( inlineMarkdownToHTML ( unordered ) ) </li> " )
continue
}
if let ordered = markdownOrderedListItem ( from : workingLine ) {
flushParagraph ( )
if insideUnorderedList {
result . append ( " </ul> " )
insideUnorderedList = false
}
if ! insideOrderedList {
result . append ( " <ol> " )
insideOrderedList = true
}
result . append ( " <li> \( inlineMarkdownToHTML ( ordered ) ) </li> " )
continue
}
closeLists ( )
paragraphLines . append ( workingLine )
}
closeBlockquote ( )
closeParagraphAndInlineContainers ( )
if insideCodeFence {
result . append ( " </code></pre> " )
}
return result . joined ( separator : " \n " )
}
private func markdownHeading ( from line : String ) -> ( level : Int , text : String ) ? {
let pattern = " ^(#{1,6}) \\ s+(.+)$ "
guard let regex = try ? NSRegularExpression ( pattern : pattern ) else { return nil }
let range = NSRange ( line . startIndex . . . , in : line )
guard let match = regex . firstMatch ( in : line , options : [ ] , range : range ) ,
let hashesRange = Range ( match . range ( at : 1 ) , in : line ) ,
let textRange = Range ( match . range ( at : 2 ) , in : line ) else {
return nil
}
return ( line [ hashesRange ] . count , String ( line [ textRange ] ) )
}
private func isMarkdownHorizontalRule ( _ line : String ) -> Bool {
let compact = line . replacingOccurrences ( of : " " , with : " " )
return compact = = " *** " || compact = = " --- " || compact = = " ___ "
}
private func markdownUnorderedListItem ( from line : String ) -> String ? {
let pattern = " ^[-*+] \\ s+(.+)$ "
guard let regex = try ? NSRegularExpression ( pattern : pattern ) else { return nil }
let range = NSRange ( line . startIndex . . . , in : line )
guard let match = regex . firstMatch ( in : line , options : [ ] , range : range ) ,
let textRange = Range ( match . range ( at : 1 ) , in : line ) else {
return nil
}
return String ( line [ textRange ] )
}
private func markdownOrderedListItem ( from line : String ) -> String ? {
let pattern = " ^ \\ d+[ \\ .)] \\ s+(.+)$ "
guard let regex = try ? NSRegularExpression ( pattern : pattern ) else { return nil }
let range = NSRange ( line . startIndex . . . , in : line )
guard let match = regex . firstMatch ( in : line , options : [ ] , range : range ) ,
let textRange = Range ( match . range ( at : 1 ) , in : line ) else {
return nil
}
return String ( line [ textRange ] )
}
private func inlineMarkdownToHTML ( _ text : String ) -> String {
var html = escapedHTML ( text )
var codeSpans : [ String ] = [ ]
html = replacingRegex ( in : html , pattern : " `([^`]+)` " ) { match in
let content = String ( match . dropFirst ( ) . dropLast ( ) )
let token = " __CODE_SPAN_ \( codeSpans . count ) __ "
codeSpans . append ( " <code> \( content ) </code> " )
return token
}
html = replacingRegex ( in : html , pattern : " ! \\ [([^ \\ ]]*) \\ ] \\ (([^ \\ ) \\ s]+) \\ ) " ) { match in
let parts = captureGroups ( in : match , pattern : " ! \\ [([^ \\ ]]*) \\ ] \\ (([^ \\ ) \\ s]+) \\ ) " )
guard parts . count = = 2 else { return match }
return " <img src= \" \( parts [ 1 ] ) \" alt= \" \( parts [ 0 ] ) \" /> "
}
html = replacingRegex ( in : html , pattern : " \\ [([^ \\ ]]+) \\ ] \\ (([^ \\ ) \\ s]+) \\ ) " ) { match in
let parts = captureGroups ( in : match , pattern : " \\ [([^ \\ ]]+) \\ ] \\ (([^ \\ ) \\ s]+) \\ ) " )
guard parts . count = = 2 else { return match }
return " <a href= \" \( parts [ 1 ] ) \" > \( parts [ 0 ] ) </a> "
}
html = replacingRegex ( in : html , pattern : " \\ * \\ *([^*]+) \\ * \\ * " ) { " <strong> \( String ( $0 . dropFirst ( 2 ) . dropLast ( 2 ) ) ) </strong> " }
html = replacingRegex ( in : html , pattern : " __([^_]+)__ " ) { " <strong> \( String ( $0 . dropFirst ( 2 ) . dropLast ( 2 ) ) ) </strong> " }
html = replacingRegex ( in : html , pattern : " \\ *([^*]+) \\ * " ) { " <em> \( String ( $0 . dropFirst ( ) . dropLast ( ) ) ) </em> " }
html = replacingRegex ( in : html , pattern : " _([^_]+)_ " ) { " <em> \( String ( $0 . dropFirst ( ) . dropLast ( ) ) ) </em> " }
for ( index , codeHTML ) in codeSpans . enumerated ( ) {
html = html . replacingOccurrences ( of : " __CODE_SPAN_ \( index ) __ " , with : codeHTML )
}
return html
}
private func replacingRegex ( in text : String , pattern : String , transform : ( String ) -> String ) -> String {
guard let regex = try ? NSRegularExpression ( pattern : pattern ) else { return text }
let matches = regex . matches ( in : text , options : [ ] , range : NSRange ( text . startIndex . . . , in : text ) )
guard ! matches . isEmpty else { return text }
var output = text
for match in matches . reversed ( ) {
guard let range = Range ( match . range , in : output ) else { continue }
let segment = String ( output [ range ] )
output . replaceSubrange ( range , with : transform ( segment ) )
}
return output
}
private func captureGroups ( in text : String , pattern : String ) -> [ String ] {
guard let regex = try ? NSRegularExpression ( pattern : pattern ) ,
let match = regex . firstMatch ( in : text , options : [ ] , range : NSRange ( text . startIndex . . . , in : text ) ) else {
return [ ]
}
var groups : [ String ] = [ ]
for idx in 1. . < match . numberOfRanges {
if let range = Range ( match . range ( at : idx ) , in : text ) {
groups . append ( String ( text [ range ] ) )
}
}
return groups
}
private func markdownPreviewCSS ( template : String ) -> String {
let basePadding : String
let fontSize : String
let lineHeight : String
let maxWidth : String
switch template {
case " docs " :
basePadding = " 22px 30px "
fontSize = " 15px "
lineHeight = " 1.7 "
maxWidth = " 900px "
case " article " :
basePadding = " 32px 48px "
fontSize = " 17px "
lineHeight = " 1.8 "
maxWidth = " 760px "
case " compact " :
basePadding = " 14px 16px "
fontSize = " 13px "
lineHeight = " 1.5 "
maxWidth = " none "
default :
basePadding = " 18px 22px "
fontSize = " 14px "
lineHeight = " 1.6 "
maxWidth = " 860px "
}
return " " "
: root { color - scheme : light dark ; }
html , body {
margin : 0 ;
padding : 0 ;
background : transparent ;
font - family : - apple - system , BlinkMacSystemFont , " SF Pro Text " , " Helvetica Neue " , sans - serif ;
font - size : \ ( fontSize ) ;
line - height : \ ( lineHeight ) ;
}
. content {
max - width : \ ( maxWidth ) ;
padding : \ ( basePadding ) ;
margin : 0 auto ;
}
h1 , h2 , h3 , h4 , h5 , h6 {
line - height : 1.25 ;
margin : 1.1 em 0 0.55 em ;
font - weight : 700 ;
}
h1 { font - size : 1.85 em ; border - bottom : 1 px solid color - mix ( in srgb , currentColor 18 % , transparent ) ; padding - bottom : 0.25 em ; }
h2 { font - size : 1.45 em ; border - bottom : 1 px solid color - mix ( in srgb , currentColor 13 % , transparent ) ; padding - bottom : 0.2 em ; }
h3 { font - size : 1.2 em ; }
p , ul , ol , blockquote , table , pre { margin : 0.65 em 0 ; }
ul , ol { padding - left : 1.3 em ; }
li { margin : 0.2 em 0 ; }
blockquote {
margin - left : 0 ;
padding : 0.45 em 0.9 em ;
border - left : 3 px solid color - mix ( in srgb , currentColor 30 % , transparent ) ;
background : color - mix ( in srgb , currentColor 6 % , transparent ) ;
border - radius : 6 px ;
}
code {
font - family : " SF Mono " , " Menlo " , " Monaco " , monospace ;
font - size : 0.9 em ;
padding : 0.12 em 0.35 em ;
border - radius : 5 px ;
background : color - mix ( in srgb , currentColor 10 % , transparent ) ;
}
pre {
overflow - x : auto ;
padding : 0.8 em 0.95 em ;
border - radius : 9 px ;
background : color - mix ( in srgb , currentColor 8 % , transparent ) ;
border : 1 px solid color - mix ( in srgb , currentColor 14 % , transparent ) ;
line - height : 1.35 ;
white - space : pre ;
}
pre code {
display : block ;
padding : 0 ;
background : transparent ;
border - radius : 0 ;
font - size : 0.88 em ;
line - height : 1.35 ;
white - space : pre ;
}
table {
border - collapse : collapse ;
width : 100 % ;
border : 1 px solid color - mix ( in srgb , currentColor 16 % , transparent ) ;
border - radius : 8 px ;
overflow : hidden ;
}
th , td {
text - align : left ;
padding : 0.45 em 0.55 em ;
border - bottom : 1 px solid color - mix ( in srgb , currentColor 10 % , transparent ) ;
}
th {
background : color - mix ( in srgb , currentColor 7 % , transparent ) ;
font - weight : 600 ;
}
a {
color : # 2 f7cf6 ;
text - decoration : none ;
border - bottom : 1 px solid color - mix ( in srgb , # 2 f7cf6 45 % , transparent ) ;
}
img {
max - width : 100 % ;
height : auto ;
border - radius : 8 px ;
}
hr {
border : 0 ;
border - top : 1 px solid color - mix ( in srgb , currentColor 15 % , transparent ) ;
margin : 1.1 em 0 ;
}
" " "
}
private func escapedHTML ( _ text : String ) -> String {
text
. replacingOccurrences ( of : " & " , with : " & " )
. replacingOccurrences ( of : " < " , with : " < " )
. replacingOccurrences ( of : " > " , with : " > " )
. replacingOccurrences ( of : " \" " , with : " " " )
. replacingOccurrences ( of : " ' " , with : " ' " )
}
#endif
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 )
)
}
#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-02-19 08:09:35 +00:00
if largeFileModeEnabled {
Text ( " Large File Mode " )
2026-02-08 11:14:49 +00:00
. font ( . system ( size : 11 , weight : . semibold ) )
. foregroundColor ( . secondary )
. padding ( . horizontal , 8 )
. padding ( . vertical , 3 )
. background (
Capsule ( style : . continuous )
. fill ( Color . secondary . opacity ( 0.16 ) )
)
}
2025-09-25 09:01:45 +00:00
Spacer ( )
2026-02-08 11:14:49 +00:00
Text ( largeFileModeEnabled
? " \( caretStatus ) \( vimStatusSuffix ) "
2026-02-19 14:29:53 +00:00
: " \( caretStatus ) • 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-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 : {
Text ( tab . name + ( tab . isDirty ? " • " : " " ) )
. lineLimit ( 1 )
. font ( . system ( size : 12 , weight : viewModel . selectedTabID = = tab . id ? . semibold : . regular ) )
. padding ( . leading , 10 )
. padding ( . vertical , 6 )
}
. buttonStyle ( . plain )
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-02-20 00:31:14 +00:00
Divider ( ) . opacity ( 0.45 )
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-02-19 14:29:53 +00:00
. background ( macChromeBackgroundStyle )
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
return " "
#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 ] {
var items : [ QuickFileSwitcherPanel . Item ] = [ ]
let fileURLSet = Set ( viewModel . tabs . compactMap { $0 . fileURL ? . standardizedFileURL . path } )
for tab in viewModel . tabs {
let subtitle = tab . fileURL ? . path ? ? " Open tab "
items . append (
QuickFileSwitcherPanel . Item (
id : " tab: \( tab . id . uuidString ) " ,
title : tab . name ,
subtitle : subtitle
)
)
}
2026-02-19 14:29:53 +00:00
for url in quickSwitcherProjectFileURLs {
2026-02-08 00:06:06 +00:00
let standardized = url . standardizedFileURL . path
if fileURLSet . contains ( standardized ) { continue }
items . append (
QuickFileSwitcherPanel . Item (
id : " file: \( standardized ) " ,
title : url . lastPathComponent ,
subtitle : standardized
)
)
}
let query = quickSwitcherQuery . trimmingCharacters ( in : . whitespacesAndNewlines ) . lowercased ( )
guard ! query . isEmpty else { return Array ( items . prefix ( 300 ) ) }
return Array (
items . filter {
$0 . title . lowercased ( ) . contains ( query ) || $0 . subtitle . lowercased ( ) . contains ( query )
}
. prefix ( 300 )
)
}
private func selectQuickSwitcherItem ( _ item : QuickFileSwitcherPanel . Item ) {
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-02-25 13:07:05 +00:00
private func startFindInFiles ( ) {
guard let root = projectRootFolderURL else {
findInFilesResults = [ ]
findInFilesStatusMessage = " Open a project folder first. "
return
}
let query = findInFilesQuery . trimmingCharacters ( in : . whitespacesAndNewlines )
guard ! query . isEmpty else {
findInFilesResults = [ ]
findInFilesStatusMessage = " Enter a search query. "
return
}
findInFilesTask ? . cancel ( )
findInFilesStatusMessage = " Searching… "
let caseSensitive = findInFilesCaseSensitive
findInFilesTask = Task {
let results = await ContentView . findInFiles (
root : root ,
query : query ,
caseSensitive : caseSensitive ,
maxResults : 500
)
guard ! Task . isCancelled else { return }
findInFilesResults = results
if results . isEmpty {
findInFilesStatusMessage = " No matches found. "
} else {
findInFilesStatusMessage = " \( results . count ) matches "
}
}
}
private func selectFindInFilesMatch ( _ match : FindInFilesMatch ) {
openProjectFile ( url : match . fileURL )
var userInfo : [ String : Any ] = [
EditorCommandUserInfo . rangeLocation : match . rangeLocation ,
EditorCommandUserInfo . rangeLength : match . rangeLength
]
#if os ( macOS )
if let hostWindowNumber {
userInfo [ EditorCommandUserInfo . windowNumber ] = hostWindowNumber
}
#endif
DispatchQueue . main . asyncAfter ( deadline : . now ( ) + 0.08 ) {
NotificationCenter . default . post ( name : . moveCursorToRange , object : nil , userInfo : userInfo )
}
}
2026-02-19 14:29:53 +00:00
private func scheduleWordCountRefresh ( for text : String ) {
2026-02-20 22:04:03 +00:00
if largeFileModeEnabled || currentDocumentUTF16Length >= 300_000 {
wordCountTask ? . cancel ( )
if statusWordCount != 0 {
statusWordCount = 0
}
return
}
2026-02-19 14:29:53 +00:00
let snapshot = text
wordCountTask ? . cancel ( )
wordCountTask = Task ( priority : . utility ) {
try ? await Task . sleep ( nanoseconds : 80_000_000 )
guard ! Task . isCancelled else { return }
let count = viewModel . wordCount ( for : snapshot )
await MainActor . run {
statusWordCount = count
}
}
}
2026-01-17 11:11:26 +00:00
}