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-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-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-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-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-06 18:59:53 +00:00
@ EnvironmentObject var viewModel : EditorViewModel
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-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-14 20:57:32 +00:00
@ AppStorage ( " EnableTranslucentWindow " ) var enableTranslucentWindow : Bool = false
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-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 = " "
@ 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
@ AppStorage ( " SettingsShowKeyboardAccessoryBarIOS " ) var showKeyboardAccessoryBarIOS : Bool = true
@ AppStorage ( " SettingsShowBottomActionBarIOS " ) var showBottomActionBarIOS : Bool = true
@ AppStorage ( " SettingsUseLiquidGlassToolbarIOS " ) var shouldUseLiquidGlass : Bool = true
#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-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
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-06 18:59:53 +00:00
var activeProviderName : String { lastProviderUsed }
2026-02-19 08:44:24 +00:00
#if os ( macOS )
private let bracketHelperTokens : [ String ] = [ " ( " , " ) " , " { " , " } " , " [ " , " ] " , " < " , " > " , " ' " , " \" " , " ` " , " () " , " {} " , " [] " , " \" \" " , " '' " ]
2026-02-19 09:33:17 +00:00
#elseif os ( iOS )
var primaryGlassMaterial : Material { . ultraThinMaterial }
var toolbarFallbackColor : Color { Color ( . systemBackground ) }
var toolbarDensityScale : CGFloat { 1.0 }
var toolbarDensityOpacity : Double { 1.0 }
2026-02-19 08:44:24 +00:00
#endif
2026-02-05 21:30:21 +00:00
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-18 19:19:49 +00:00
let length = nsText ? . length ? ? ( currentContentBinding . wrappedValue as NSString ) . length
return length >= 120_000
}
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 ) {
#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
if let number {
WindowViewModelRegistry . shared . register ( viewModel , for : number )
}
}
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 )
}
. background ( . ultraThinMaterial )
}
#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-11 10:20:17 +00:00
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 )
if let tab = viewModel . selectedTab {
if let idx = viewModel . tabs . firstIndex ( where : { $0 . id = = tab . id } ) ,
! viewModel . tabs [ idx ] . languageLocked ,
viewModel . tabs [ idx ] . language = = " plain " ,
result . lang != " plain " {
viewModel . tabs [ idx ] . language = result . lang
2026-02-08 09:58:46 +00:00
}
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 ) {
if let tab = viewModel . selectedTab {
if let idx = viewModel . tabs . firstIndex ( where : { $0 . id = = tab . id } ) ,
! viewModel . tabs [ idx ] . languageLocked ,
viewModel . tabs [ idx ] . language = = " plain " {
viewModel . tabs [ idx ] . language = preferred
}
} 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 ) {
#if os ( iOS )
let isLarge = forceLargeFileMode || text . utf8 . count >= 2_000_000
#else
2026-02-19 08:09:35 +00:00
let isLarge = text . utf8 . count >= 2_000_000
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 }
viewModel . isBrainDumpMode . toggle ( )
UserDefaults . standard . set ( viewModel . isBrainDumpMode , forKey : " BrainDumpModeEnabled " )
}
. 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
}
. onReceive ( NotificationCenter . default . publisher ( for : . showQuickSwitcherRequested ) ) { notif in
guard matchesCurrentWindow ( notif ) else { return }
quickSwitcherQuery = " "
showQuickSwitcher = true
}
. onReceive ( NotificationCenter . default . publisher ( for : . showWelcomeTourRequested ) ) { notif in
guard matchesCurrentWindow ( notif ) else { return }
showWelcomeTour = true
}
. onReceive ( NotificationCenter . default . publisher ( for : . toggleProjectStructureSidebarRequested ) ) { notif in
guard matchesCurrentWindow ( notif ) else { return }
showProjectStructureSidebar . toggle ( )
}
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-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 08:09:35 +00:00
. background ( enableTranslucentWindow ? AnyShapeStyle ( . ultraThinMaterial ) : AnyShapeStyle ( Color . clear ) )
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-19 08:09:35 +00:00
. background ( enableTranslucentWindow ? AnyShapeStyle ( . ultraThinMaterial ) : 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-14 13:24:01 +00:00
AnyView ( platformLayout )
2026-02-19 08:09:35 +00:00
. 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 ? ? " " )
}
. navigationTitle ( " Neon Vision Editor " )
. onAppear {
if UserDefaults . standard . object ( forKey : " SettingsAutoIndent " ) = = nil {
autoIndentEnabled = true
2026-02-18 22:56:46 +00:00
}
2026-02-19 08:09:35 +00:00
// A l w a y s s t a r t w i t h c o m p l e t i o n d i s a b l e d o n a p p l a u n c h / o p e n .
isAutoCompletionEnabled = false
UserDefaults . standard . set ( false , forKey : " SettingsCompletionEnabled " )
// K e e p w h i t e s p a c e m a r k e r r e n d e r i n g d i s a b l e d b y d e f a u l t a n d a f t e r m i g r a t i o n s .
UserDefaults . standard . set ( false , forKey : " SettingsShowInvisibleCharacters " )
UserDefaults . standard . set ( false , forKey : " NSShowAllInvisibles " )
UserDefaults . standard . set ( false , forKey : " NSShowControlCharacters " )
viewModel . isLineWrapEnabled = settingsLineWrapEnabled
syncAppleCompletionAvailability ( )
}
. onChange ( of : settingsLineWrapEnabled ) { _ , enabled in
if viewModel . isLineWrapEnabled != enabled {
viewModel . isLineWrapEnabled = enabled
2026-02-18 22:56:46 +00:00
}
2026-02-19 08:09:35 +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-19 08:09:35 +00:00
}
. onChange ( of : viewModel . isLineWrapEnabled ) { _ , enabled in
if settingsLineWrapEnabled != enabled {
settingsLineWrapEnabled = enabled
2026-02-18 22:56:46 +00:00
}
2026-02-19 08:09:35 +00:00
}
. onChange ( of : appUpdateManager . automaticPromptToken ) { _ , _ in
if appUpdateManager . consumeAutomaticPromptIfNeeded ( ) {
showUpdaterDialog ( checkNow : false )
2026-02-18 22:56:46 +00:00
}
2026-02-19 08:09:35 +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 ( )
}
. onReceive ( viewModel . $ tabs ) { _ in
persistSessionIfReady ( )
}
. modifier ( ModalPresentationModifier ( contentView : self ) )
. onAppear {
// S t a r t w i t h s i d e b a r c o l l a p s e d b y d e f a u l t
viewModel . showSidebar = false
showProjectStructureSidebar = false
2026-02-12 22:20:39 +00:00
2026-02-19 08:09:35 +00:00
applyStartupBehaviorIfNeeded ( )
2026-02-18 22:56:46 +00:00
2026-02-19 08:09:35 +00:00
// R e s t o r e B r a i n D u m p m o d e f r o m d e f a u l t s
if UserDefaults . standard . object ( forKey : " BrainDumpModeEnabled " ) != nil {
viewModel . isBrainDumpMode = UserDefaults . standard . bool ( forKey : " BrainDumpModeEnabled " )
}
2026-02-06 13:29:34 +00:00
2026-02-19 08:09:35 +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-18 22:56:46 +00:00
#if os ( macOS )
2026-02-19 08:09:35 +00:00
. background (
WindowAccessor { window in
updateWindowRegistration ( window )
}
. frame ( width : 0 , height : 0 )
)
. onDisappear {
completionDebounceTask ? . cancel ( )
completionTask ? . cancel ( )
lastCompletionTriggerSignature = " "
pendingHighlightRefresh ? . cancel ( )
completionCache . removeAll ( keepingCapacity : false )
if let number = hostWindowNumber {
WindowViewModelRegistry . shared . unregister ( windowNumber : number )
2026-02-08 09:58:46 +00:00
}
2026-02-19 08:09:35 +00:00
}
2026-02-08 09:58:46 +00:00
#endif
2025-09-25 09:01:45 +00:00
}
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-18 19:19:49 +00:00
let length = nsText ? . length ? ? ( currentContentBinding . wrappedValue as NSString ) . length
return length >= 120_000
}
#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 )
2026-02-19 08:09:35 +00:00
. tint ( . blue )
2026-02-14 13:24:01 +00:00
#if os ( iOS )
. presentationDetents ( [ . large ] )
. presentationDragIndicator ( . visible )
. presentationContentInteraction ( . scrolls )
#endif
}
#endif
#if os ( iOS )
. sheet ( isPresented : contentView . $ showCompactSidebarSheet ) {
NavigationStack {
SidebarView ( content : contentView . currentContent , language : contentView . currentLanguage )
. navigationTitle ( " Sidebar " )
. toolbar {
ToolbarItem ( placement : . topBarTrailing ) {
Button ( " Done " ) {
contentView . $ showCompactSidebarSheet . wrappedValue = false
}
}
}
}
. presentationDetents ( [ . medium , . large ] )
}
#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 ) }
)
}
. sheet ( isPresented : contentView . $ showLanguageSetupPrompt ) {
contentView . languageSetupSheet
}
. sheet ( isPresented : contentView . $ showWelcomeTour ) {
WelcomeTourView {
contentView . $ hasSeenWelcomeTourV1 . wrappedValue = true
contentView . $ welcomeTourSeenRelease . wrappedValue = WelcomeTourView . releaseID
contentView . $ showWelcomeTour . wrappedValue = false
}
}
. 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 ,
allowedContentTypes : [ . text , . plainText , . sourceCode , . json , . xml , . yaml ] ,
allowsMultipleSelection : false
) { 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 )
return viewModel . showSidebar && ! viewModel . isBrainDumpMode
#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 .
return viewModel . showSidebar && ! viewModel . isBrainDumpMode && horizontalSizeClass = = . regular
#endif
}
2026-02-12 22:20:39 +00:00
private func applyStartupBehaviorIfNeeded ( ) {
guard ! didApplyStartupBehavior else { return }
if viewModel . tabs . contains ( where : { $0 . fileURL != nil } ) {
didApplyStartupBehavior = true
persistSessionIfReady ( )
return
}
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 {
let paths = UserDefaults . standard . stringArray ( forKey : " LastSessionFileURLs " ) ? ? [ ]
let selectedPath = UserDefaults . standard . string ( forKey : " LastSessionSelectedFileURL " )
let urls = paths . compactMap { URL ( string : $0 ) }
if ! urls . isEmpty {
viewModel . tabs . removeAll ( )
viewModel . selectedTabID = nil
for url in urls {
viewModel . openFile ( url : url )
}
if let selectedPath , let selectedURL = URL ( string : selectedPath ) {
_ = viewModel . focusTabIfOpen ( for : selectedURL )
}
if viewModel . tabs . isEmpty {
viewModel . addNewTab ( )
}
}
}
2026-02-16 19:02:43 +00:00
if openWithBlankDocument {
didApplyStartupBehavior = true
persistSessionIfReady ( )
return
}
2026-02-12 22:20:39 +00:00
didApplyStartupBehavior = true
persistSessionIfReady ( )
}
private func persistSessionIfReady ( ) {
guard didApplyStartupBehavior else { return }
let urls = viewModel . tabs . compactMap { $0 . fileURL ? . absoluteString }
UserDefaults . standard . set ( urls , forKey : " LastSessionFileURLs " )
UserDefaults . standard . set ( viewModel . selectedTab ? . fileURL ? . absoluteString , forKey : " LastSessionSelectedFileURL " )
}
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 {
2025-09-25 09:01:45 +00:00
if viewModel . showSidebar && ! viewModel . isBrainDumpMode {
2026-01-17 11:11:26 +00:00
SidebarView ( content : currentContent ,
language : currentLanguage )
. frame ( minWidth : 200 , idealWidth : 250 , maxWidth : 600 )
2025-09-25 09:01:45 +00:00
. animation ( . spring ( ) , value : viewModel . showSidebar )
. safeAreaInset ( edge : . bottom ) {
Divider ( )
}
2026-02-06 13:29:34 +00:00
. background ( enableTranslucentWindow ? AnyShapeStyle ( . ultraThinMaterial ) : AnyShapeStyle ( Color . clear ) )
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-01-17 11:11:26 +00:00
if let tab = viewModel . selectedTab {
return Binding (
get : { tab . content } ,
set : { newValue in viewModel . updateTabContent ( tab : tab , content : newValue ) }
)
} else {
return $ singleContent
}
}
2026-02-06 18:59:53 +00:00
var currentLanguageBinding : Binding < String > {
2026-01-17 11:11:26 +00:00
if let selectedID = viewModel . selectedTabID , let idx = viewModel . tabs . firstIndex ( where : { $0 . id = = selectedID } ) {
return Binding (
get : { viewModel . tabs [ idx ] . language } ,
set : { newValue in viewModel . tabs [ idx ] . language = newValue }
)
} 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 {
viewModel . updateTabLanguage ( tab : tab , language : newValue )
} 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-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 {
viewModel . updateTabLanguage ( tab : tab , language : language )
if insertTemplate , contentIsEmpty , let template = starterTemplate ( for : language ) {
viewModel . updateTabContent ( tab : tab , content : template )
}
} else {
singleLanguage = language
if insertTemplate , contentIsEmpty , let template = starterTemplate ( for : language ) {
singleContent = template
}
}
}
private var languageSetupSheet : some View {
let contentIsEmpty = currentContent . trimmingCharacters ( in : . whitespacesAndNewlines ) . isEmpty
let canInsertTemplate = contentIsEmpty
return VStack ( alignment : . leading , spacing : 16 ) {
Text ( " Choose a language for code completion " )
. font ( . headline )
Text ( " You can change this later from the Language picker. " )
. font ( . subheadline )
. foregroundColor ( . secondary )
Picker ( " Language " , selection : $ languagePromptSelection ) {
ForEach ( languageOptions , id : \ . self ) { lang in
Text ( languageLabel ( for : lang ) ) . tag ( lang )
}
}
. labelsHidden ( )
. frame ( maxWidth : 240 )
if canInsertTemplate {
Toggle ( " Insert starter template " , isOn : $ languagePromptInsertTemplate )
}
HStack {
Button ( " Use Plain Text " ) {
applyLanguageSelection ( language : " plain " , insertTemplate : false )
showLanguageSetupPrompt = false
}
Spacer ( )
Button ( " Use Selected Language " ) {
applyLanguageSelection ( language : languagePromptSelection , insertTemplate : languagePromptInsertTemplate )
showLanguageSetupPrompt = false
}
. keyboardShortcut ( . defaultAction )
}
}
. padding ( 20 )
. frame ( minWidth : 340 )
}
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 }
if let tab = viewModel . selectedTab {
let content = tab . content
let updated : String
if content . trimmingCharacters ( in : . whitespacesAndNewlines ) . isEmpty {
updated = template
} else {
updated = content + ( content . hasSuffix ( " \n " ) ? " \n " : " \n \n " ) + template
}
viewModel . updateTabContent ( tab : tab , content : updated )
} else {
if singleContent . trimmingCharacters ( in : . whitespacesAndNewlines ) . isEmpty {
singleContent = template
} else {
singleContent = singleContent + ( singleContent . hasSuffix ( " \n " ) ? " \n " : " \n \n " ) + template
}
}
}
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-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-06 19:20:03 +00:00
if ! viewModel . isBrainDumpMode {
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 ,
language : currentLanguage ,
colorScheme : colorScheme ,
fontSize : editorFontSize ,
isLineWrapEnabled : $ viewModel . isLineWrapEnabled ,
2026-02-08 11:14:49 +00:00
isLargeFileMode : largeFileModeEnabled ,
2026-02-11 10:20:17 +00:00
translucentBackgroundEnabled : enableTranslucentWindow ,
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 ,
highlightRefreshToken : highlightRefreshToken
2026-02-06 18:59:53 +00:00
)
. id ( currentLanguage )
2026-02-14 22:15:22 +00:00
. frame ( maxWidth : viewModel . isBrainDumpMode ? 920 : . infinity )
2026-02-06 18:59:53 +00:00
. frame ( maxHeight : . infinity )
2026-02-14 22:15:22 +00:00
. padding ( . horizontal , viewModel . isBrainDumpMode ? 24 : 0 )
2026-02-06 18:59:53 +00:00
. padding ( . vertical , viewModel . isBrainDumpMode ? 40 : 0 )
. background (
Group {
if enableTranslucentWindow {
2026-02-19 08:09:35 +00:00
Color . clear . background ( . ultraThinMaterial )
2026-02-18 22:56:46 +00:00
} else {
Color . clear
}
2026-02-06 13:29:34 +00:00
}
2026-02-06 18:59:53 +00:00
)
if ! viewModel . isBrainDumpMode {
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 ,
alignment : viewModel . isBrainDumpMode ? . top : . topLeading
)
2026-01-17 11:11:26 +00:00
2026-02-06 18:59:53 +00:00
if showProjectStructureSidebar && ! viewModel . isBrainDumpMode {
Divider ( )
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 )
2025-09-25 09:01:45 +00:00
}
}
2026-02-14 22:15:22 +00:00
. background (
Group {
2026-02-19 08:09:35 +00:00
if viewModel . isBrainDumpMode && enableTranslucentWindow {
Color . clear . background ( . ultraThinMaterial )
2026-02-18 22:56:46 +00:00
} else {
Color . clear
}
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-08 09:58:46 +00:00
let withEvents = withTypingEvents (
withCommandEvents (
withBaseEditorEvents ( content )
)
)
return withEvents
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
}
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-08 11:14:49 +00:00
. padding ( . top , viewModel . isBrainDumpMode ? 12 : 50 )
. padding ( . trailing , 12 )
}
}
2026-02-07 10:51:52 +00:00
#if os ( macOS )
2026-02-19 08:09:35 +00:00
. toolbarBackground ( AnyShapeStyle ( Color ( nsColor : . windowBackgroundColor ) ) , for : ToolbarPlacement . windowToolbar )
. toolbarBackgroundVisibility ( enableTranslucentWindow ? . hidden : . visible , for : ToolbarPlacement . windowToolbar )
2026-02-07 10:51:52 +00:00
#else
2026-02-19 08:09:35 +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
// 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 ) "
: " \( caretStatus ) • Words: \( viewModel . wordCount ( for : currentContent ) ) \( 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 08:09:35 +00:00
. background ( enableTranslucentWindow ? AnyShapeStyle ( . ultraThinMaterial ) : AnyShapeStyle ( Color . clear ) )
2026-02-18 22:56:46 +00:00
}
2026-02-06 19:20:03 +00:00
@ ViewBuilder
var tabBarView : some View {
ScrollView ( . horizontal , showsIndicators : false ) {
HStack ( spacing : 6 ) {
ForEach ( viewModel . tabs ) { tab in
HStack ( spacing : 6 ) {
Button {
viewModel . selectedTabID = tab . id
} label : {
Text ( tab . name + ( tab . isDirty ? " • " : " " ) )
. lineLimit ( 1 )
. font ( . system ( size : 12 , weight : viewModel . selectedTabID = = tab . id ? . semibold : . regular ) )
}
. buttonStyle ( . plain )
Button {
requestCloseTab ( tab )
} label : {
Image ( systemName : " xmark " )
. font ( . system ( size : 10 , weight : . bold ) )
}
. buttonStyle ( . plain )
. help ( " Close \( tab . name ) " )
}
. padding ( . horizontal , 10 )
. padding ( . vertical , 6 )
. background (
RoundedRectangle ( cornerRadius : 8 , style : . continuous )
. fill ( viewModel . selectedTabID = = tab . id ? Color . accentColor . opacity ( 0.18 ) : Color . secondary . opacity ( 0.10 ) )
)
}
}
. padding ( . horizontal , 10 )
. padding ( . vertical , 6 )
}
2026-02-07 10:51:52 +00:00
#if os ( macOS )
2026-02-19 08:09:35 +00:00
. background ( enableTranslucentWindow ? AnyShapeStyle ( . ultraThinMaterial ) : AnyShapeStyle ( Color ( nsColor : . windowBackgroundColor ) ) )
2026-02-07 10:51:52 +00:00
#else
2026-02-19 08:09:35 +00:00
. background ( enableTranslucentWindow ? AnyShapeStyle ( . ultraThinMaterial ) : AnyShapeStyle ( Color ( . systemBackground ) ) )
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
)
)
}
for url in projectFileURLs ( from : projectTreeNodes ) {
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 ) {
viewModel . selectedTabID = id
}
return
}
if item . id . hasPrefix ( " file: " ) {
let path = String ( item . id . dropFirst ( 5 ) )
openProjectFile ( url : URL ( fileURLWithPath : path ) )
}
}
2026-01-17 11:11:26 +00:00
}