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 .
// MARK: - I m p o r t s
2025-08-25 07:39:12 +00:00
import SwiftUI
2025-08-26 17:25:39 +00:00
import AppKit
2026-01-25 12:46:33 +00:00
import Foundation
2026-01-17 12:04:11 +00:00
#if USE_FOUNDATION_MODELS
import FoundationModels
#endif
2025-08-25 07:39:12 +00:00
2026-01-25 12:46:33 +00:00
// S u p p o r t e d A I p r o v i d e r s f o r s u g g e s t i o n s . E x t e n d a s n e e d e d .
2026-01-17 11:11:26 +00:00
enum AIModel : String , CaseIterable , Identifiable {
case appleIntelligence
case grok
2026-01-25 12:46:33 +00:00
case openAI
case gemini
2026-01-17 11:11:26 +00:00
var id : String { rawValue }
}
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 {
func width ( usingFont font : NSFont ) -> CGFloat {
let attributes = [ NSAttributedString . Key . font : font ]
let size = ( self as NSString ) . size ( withAttributes : attributes )
return size . width
}
}
2025-08-27 11:34:59 +00:00
2026-01-25 12:46:33 +00:00
// R o o t v i e w f o r t h e e d i t o r . 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-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
2025-09-25 09:01:45 +00:00
@ EnvironmentObject private var viewModel : EditorViewModel
@ Environment ( \ . colorScheme ) private var colorScheme
@ Environment ( \ . showGrokError ) private var showGrokError
@ Environment ( \ . grokErrorMessage ) private 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-01-17 11:11:26 +00:00
@ State private var selectedModel : AIModel = . appleIntelligence
2025-09-25 09:01:45 +00:00
@ State private var singleContent : String = " "
@ State private var singleLanguage : String = " swift "
2026-01-17 11:11:26 +00:00
@ State private var caretStatus : String = " Ln 1, Col 1 "
@ State private var editorFontSize : CGFloat = 14
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-01-20 23:46:04 +00:00
@ State private var grokAPIToken : String = UserDefaults . standard . string ( forKey : " GrokAPIToken " ) ? ? " "
2026-01-25 12:46:33 +00:00
@ State private var openAIAPIToken : String = UserDefaults . standard . string ( forKey : " OpenAIAPIToken " ) ? ? " "
@ State private var geminiAPIToken : String = UserDefaults . standard . string ( forKey : " GeminiAPIToken " ) ? ? " "
// D e b o u n c e h a n d l e f o r s u g g e s t i o n s t r e a m i n g
2026-01-20 23:46:04 +00:00
@ State private var lastSuggestionWorkItem : DispatchWorkItem ?
2026-01-25 12:46:33 +00:00
// U I s t a t e f o r A I s e l e c t o r a n d s e t t i n g s p o p o v e r s
2026-01-20 23:46:04 +00:00
@ State private var showAISelectorPopover : Bool = false
2026-01-25 12:46:33 +00:00
@ State private var showAPISettings : Bool = false
2026-01-20 23:46:04 +00:00
@ State private var aiButtonAnchor : NSPopover ? = nil
2026-01-25 12:46:33 +00:00
// / P r o m p t s t h e u s e r f o r a G r o k t o k e n i f n o n e i s s a v e d . P e r s i s t s t o U s e r D e f a u l t s .
// / R e t u r n s t r u e i f a t o k e n i s p r e s e n t / w a s s a v e d ; f a l s e i f c a n c e l l e d o r e m p t y .
2026-01-20 23:46:04 +00:00
private func promptForGrokTokenIfNeeded ( ) -> Bool {
if ! grokAPIToken . trimmingCharacters ( in : . whitespacesAndNewlines ) . isEmpty { return true }
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
UserDefaults . standard . set ( token , forKey : " GrokAPIToken " )
return true
}
return false
}
2026-01-25 12:46:33 +00:00
// / P r o m p t s t h e u s e r f o r a n O p e n A I t o k e n i f n o n e i s s a v e d . P e r s i s t s t o U s e r D e f a u l t s .
// / R e t u r n s t r u e i f a t o k e n i s p r e s e n t / w a s s a v e d ; f a l s e i f c a n c e l l e d o r e m p t y .
private func promptForOpenAITokenIfNeeded ( ) -> Bool {
if ! openAIAPIToken . trimmingCharacters ( in : . whitespacesAndNewlines ) . isEmpty { return true }
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
UserDefaults . standard . set ( token , forKey : " OpenAIAPIToken " )
return true
}
return false
}
// / P r o m p t s t h e u s e r f o r a G e m i n i t o k e n i f n o n e i s s a v e d . P e r s i s t s t o U s e r D e f a u l t s .
// / R e t u r n s t r u e i f a t o k e n i s p r e s e n t / w a s s a v e d ; f a l s e i f c a n c e l l e d o r e m p t y .
private func promptForGeminiTokenIfNeeded ( ) -> Bool {
if ! geminiAPIToken . trimmingCharacters ( in : . whitespacesAndNewlines ) . isEmpty { return true }
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
UserDefaults . standard . set ( token , forKey : " GeminiAPIToken " )
return true
}
return false
}
// / B u i l d s a p r o v i d e r - s p e c i f i c c l i e n t a n d b e g i n s s t r e a m i n g s u g g e s t i o n s b a s e d o n t h e c u r r e n t c o n t e n t .
// / P o s t s a . s t r e a m S u g g e s t i o n A s y n c S t r e a m t o b e h a n d l e d b y t h e e d i t o r c o o r d i n a t o r .
2026-01-20 23:46:04 +00:00
private func triggerSuggestion ( ) {
2026-01-25 12:46:33 +00:00
let prompt = " Provide a short inline code suggestion for the following \( currentLanguage ) code. Return only the suggestion text, no preface. \n \n \( currentContent ) "
2026-01-20 23:46:04 +00:00
switch selectedModel {
case . grok :
guard promptForGrokTokenIfNeeded ( ) else { return }
2026-01-25 12:46:33 +00:00
case . openAI :
guard promptForOpenAITokenIfNeeded ( ) else { return }
case . gemini :
guard promptForGeminiTokenIfNeeded ( ) else { return }
case . appleIntelligence :
break
2026-01-20 23:46:04 +00:00
}
2026-01-25 12:46:33 +00:00
let client = AIClientFactory . makeClient (
for : selectedModel ,
grokAPITokenProvider : { self . grokAPIToken } ,
openAIKeyProvider : { self . openAIAPIToken } ,
geminiKeyProvider : { self . geminiAPIToken }
)
guard let client else { return }
let stream = client . streamSuggestions ( prompt : prompt )
NotificationCenter . default . post ( name : . streamSuggestion , object : stream )
2026-01-20 23:46:04 +00:00
}
2026-01-25 12:46:33 +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 .
2025-08-27 11:33:45 +00:00
var body : some View {
2025-08-27 11:34:59 +00:00
NavigationSplitView {
2025-09-25 09:01:45 +00:00
sidebarView
2025-08-27 11:34:59 +00:00
} detail : {
2025-09-25 09:01:45 +00:00
editorView
}
2026-01-17 11:11:26 +00:00
. navigationSplitViewColumnWidth ( min : 200 , ideal : 250 , max : 600 )
2025-09-25 09:01:45 +00:00
. frame ( minWidth : 600 , minHeight : 400 )
. alert ( " AI Error " , isPresented : showGrokError ) {
Button ( " OK " ) { }
} message : {
Text ( grokErrorMessage . wrappedValue )
}
. navigationTitle ( " NeonVision Editor " )
2026-01-25 12:46:33 +00:00
. sheet ( isPresented : $ showAPISettings ) {
APISupportSettingsView (
grokAPIToken : $ grokAPIToken ,
openAIAPIToken : $ openAIAPIToken ,
geminiAPIToken : $ geminiAPIToken
)
. frame ( width : 420 )
}
2025-09-25 09:01:45 +00:00
}
2026-01-17 11:11:26 +00:00
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
private var sidebarView : some View {
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-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-01-17 11:11:26 +00:00
private var currentContentBinding : Binding < String > {
if let tab = viewModel . selectedTab {
return Binding (
get : { tab . content } ,
set : { newValue in viewModel . updateTabContent ( tab : tab , content : newValue ) }
)
} else {
return $ singleContent
}
}
private var currentLanguageBinding : Binding < String > {
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
}
}
private var currentContent : String { currentContentBinding . wrappedValue }
private var currentLanguage : String { currentLanguageBinding . wrappedValue }
2026-01-25 12:46:33 +00:00
// / D e t e c t s l a n g u a g e u s i n g A p p l e F o u n d a t i o n M o d e l s w h e n a v a i l a b l e , w i t h a h e u r i s t i c f a l l b a c k .
// / R e t u r n s a s u p p o r t e d l a n g u a g e s t r i n g u s e d b y s y n t a x h i g h l i g h t i n g a n d t h e l a n g u a g e p i c k e r .
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-01-23 11:49:52 +00:00
let supported = [ " swift " , " python " , " javascript " , " html " , " css " , " c " , " cpp " , " json " , " markdown " , " bash " , " zsh " ]
2026-01-17 12:04:11 +00:00
// T r y o n - d e v i c e F o u n d a t i o n M o d e l f i r s t
#if USE_FOUNDATION_MODELS
do {
// C r e a t e a s m a l l , f a s t m o d e l s u i t a b l e f o r c l a s s i f i c a t i o n
// N O T E : A d j u s t t h e i n i t i a l i z e r a n d e n u m c a s e s t o m a t c h y o u r S D K .
let model = try FMTextModel ( . small )
let 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: "
let response = try await model . generate ( prompt )
let detectedRaw = response . trimmingCharacters ( in : CharacterSet . whitespacesAndNewlines ) . lowercased ( )
if let match = supported . first ( where : { detectedRaw . contains ( $0 ) } ) {
return match
}
} catch {
// F a l l t h r o u g h t o h e u r i s t i c
}
#endif
// H e u r i s t i c f a l l b a c k
let lower = text . lowercased ( )
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 "
}
if lower . contains ( " <html " ) || lower . contains ( " <div " ) || lower . contains ( " </ " ) {
return " html "
}
if lower . contains ( " { " ) && lower . contains ( " } " ) && lower . contains ( " : " ) && ! lower . contains ( " ; " ) && ! lower . contains ( " function " ) {
return " json "
}
if lower . contains ( " # " ) || lower . contains ( " ## " ) {
return " markdown "
}
if lower . contains ( " #include " ) || lower . contains ( " int " ) || lower . contains ( " void " ) {
return " c "
}
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 )
if lower . contains ( " #!/bin/bash " ) || lower . contains ( " #!/usr/bin/env bash " ) || lower . contains ( " declare -a " ) || lower . contains ( " [[ " ) || lower . contains ( " ]] " ) || lower . contains ( " $(( " ) {
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-01-17 12:04:11 +00:00
return " swift "
}
2026-01-25 12:46:33 +00:00
// M a i n e d i t o r s t a c k : h o s t s t h e N S T e x t V i e w - b a c k e d e d i t o r , s t a t u s l i n e , a n d t o o l b a r .
2025-09-25 09:01:45 +00:00
@ ViewBuilder
private var editorView : some View {
VStack ( spacing : 0 ) {
2026-01-17 11:11:26 +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-01-17 11:36:31 +00:00
. id ( currentLanguage )
2026-01-17 11:11:26 +00:00
. frame ( maxWidth : viewModel . isBrainDumpMode ? 800 : . infinity )
. frame ( maxHeight : . infinity )
. padding ( . horizontal , viewModel . isBrainDumpMode ? 100 : 0 )
. padding ( . vertical , viewModel . isBrainDumpMode ? 40 : 0 )
2025-09-25 09:01:45 +00:00
if ! viewModel . isBrainDumpMode {
wordCountView
}
}
2026-01-17 11:11:26 +00:00
. onReceive ( NotificationCenter . default . publisher ( for : . caretPositionDidChange ) ) { notif in
2026-01-25 12:46:33 +00:00
// U p d a t e s t a t u s l i n e w h e n c a r e t m o v e s
2026-01-17 11:11:26 +00:00
if let line = notif . userInfo ? [ " line " ] as ? Int , let col = notif . userInfo ? [ " column " ] as ? Int {
caretStatus = " Ln \( line ) , Col \( col ) "
}
}
2026-01-17 12:04:11 +00:00
. onReceive ( NotificationCenter . default . publisher ( for : . pastedText ) ) { notif in
2026-01-25 12:46:33 +00:00
// A u t o - d e t e c t l a n g u a g e o n p a s t e
2026-01-17 12:04:11 +00:00
if let pasted = notif . object as ? String {
Task { @ MainActor in
let detected = await detectLanguageWithAppleIntelligence ( pasted )
currentLanguageBinding . wrappedValue = detected
}
}
}
2026-01-20 23:46:04 +00:00
. onReceive ( NotificationCenter . default . publisher ( for : . triggerSuggestion ) ) { _ in
2026-01-25 12:46:33 +00:00
// D e b o u n c e A I s u g g e s t i o n t o a v o i d t h r a s h w h i l e t y p i n g
2026-01-20 23:46:04 +00:00
lastSuggestionWorkItem ? . cancel ( )
let work = DispatchWorkItem {
// O n l y t r i g g e r w h e n n o t i n B r a i n D u m p m o d e t o a v o i d n o i s e ; s t i l l a l l o w i f d e s i r e d
triggerSuggestion ( )
}
lastSuggestionWorkItem = work
DispatchQueue . main . asyncAfter ( deadline : . now ( ) + 0.6 , execute : work )
}
2026-01-25 12:46:33 +00:00
// T o o l b a r : g r o u p e d i t e m s w i t h c l i c k a c t i o n s a n d h o v e r - t r i g g e r e d p o p o v e r s .
2025-09-25 09:01:45 +00:00
. toolbar {
2026-01-20 23:46:04 +00:00
ToolbarItemGroup ( placement : . automatic ) {
2026-01-25 12:46:33 +00:00
Picker ( " Language " , selection : currentLanguageBinding ) {
ForEach ( [ " swift " , " python " , " javascript " , " html " , " css " , " c " , " cpp " , " json " , " markdown " , " bash " , " zsh " ] , id : \ . self ) { lang in
Text ( lang . capitalized ) . tag ( lang )
2026-01-20 23:46:04 +00:00
}
2026-01-25 12:46:33 +00:00
}
. labelsHidden ( )
. controlSize ( . large )
. frame ( width : 140 )
. padding ( . vertical , 2 )
. hoverPopover { Text ( " Language " ) }
Button ( action : {
showAISelectorPopover . toggle ( )
} ) {
Image ( systemName : " brain.head.profile " )
}
// C l i c k p o p o v e r t o c h o o s e p r o v i d e r a n d o p e n A P I s e t t i n g s .
. popover ( isPresented : $ showAISelectorPopover ) {
VStack ( alignment : . leading , spacing : 8 ) {
Text ( " AI Model " ) . font ( . headline )
Picker ( " AI Model " , selection : $ selectedModel ) {
HStack ( spacing : 6 ) {
Image ( systemName : " brain.head.profile " )
Text ( " Apple Intelligence " )
2026-01-20 23:46:04 +00:00
}
2026-01-25 12:46:33 +00:00
. tag ( AIModel . appleIntelligence )
Text ( " Grok " ) . tag ( AIModel . grok )
Text ( " OpenAI " ) . tag ( AIModel . openAI )
Text ( " Gemini " ) . tag ( AIModel . gemini )
2026-01-20 23:46:04 +00:00
}
2026-01-25 12:46:33 +00:00
. labelsHidden ( )
. frame ( width : 170 )
. controlSize ( . large )
2026-01-20 23:46:04 +00:00
2026-01-25 12:46:33 +00:00
Button ( " API Settings… " ) {
showAISelectorPopover = false
showAPISettings = true
}
. buttonStyle ( . bordered )
2026-01-20 23:46:04 +00:00
}
2026-01-25 12:46:33 +00:00
. padding ( 12 )
}
. hoverPopover { Text ( " AI Model & Settings " ) }
2026-01-20 23:46:04 +00:00
2026-01-25 12:46:33 +00:00
Button ( action : { showAPISettings = true } ) {
Image ( systemName : " gearshape " )
}
. help ( " API Settings " )
. hoverPopover { Text ( " API Settings " ) }
Button ( action : { editorFontSize = max ( 8 , editorFontSize - 1 ) } ) {
Image ( systemName : " textformat.size.smaller " )
}
. help ( " Decrease Font Size " )
. hoverPopover { Text ( " Decrease Font Size " ) }
Button ( action : { editorFontSize = min ( 48 , editorFontSize + 1 ) } ) {
Image ( systemName : " textformat.size.larger " )
}
. help ( " Increase Font Size " )
. hoverPopover { Text ( " Increase Font Size " ) }
Button ( action : { currentContentBinding . wrappedValue = " " } ) {
Image ( systemName : " trash " )
}
. help ( " Clear Editor " )
. hoverPopover { Text ( " Clear Editor " ) }
Button ( action : { triggerSuggestion ( ) } ) {
Image ( systemName : " bolt.horizontal.circle " )
}
. help ( " Generate AI Suggestion " )
. hoverPopover { Text ( " Generate AI Suggestion " ) }
Button ( action : { viewModel . openFile ( ) } ) {
Image ( systemName : " folder " )
2025-08-27 11:34:59 +00:00
}
2026-01-25 12:46:33 +00:00
. help ( " Open File… " )
. hoverPopover { Text ( " Open File… " ) }
Button ( action : {
if let tab = viewModel . selectedTab { viewModel . saveFile ( tab : tab ) }
} ) {
Image ( systemName : " square.and.arrow.down " )
}
. disabled ( viewModel . selectedTab = = nil )
. help ( " Save File " )
. hoverPopover { Text ( " Save File " ) }
Button ( action : { viewModel . showSidebar . toggle ( ) } ) {
Image ( systemName : viewModel . showSidebar ? " sidebar.left " : " sidebar.right " )
}
. help ( " Toggle Sidebar " )
. hoverPopover { Text ( viewModel . showSidebar ? " Hide Sidebar " : " Show Sidebar " ) }
Button ( action : { viewModel . isBrainDumpMode . toggle ( ) } ) {
Image ( systemName : " note.text " )
}
. help ( " Toggle Brain Dump Mode " )
. hoverPopover { Text ( " Toggle Brain Dump Mode " ) }
2025-08-27 11:33:45 +00:00
}
}
2026-01-17 11:11:26 +00:00
. toolbarBackground ( . visible , for : . windowToolbar )
. toolbarBackground ( Color ( nsColor : . windowBackgroundColor ) , for : . windowToolbar )
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
private var wordCountView : some View {
HStack {
Spacer ( )
2026-01-17 11:11:26 +00:00
Text ( " \( caretStatus ) • Words: \( viewModel . wordCount ( for : currentContent ) ) " )
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
}
}
2025-08-27 11:34:59 +00:00
}
2026-01-25 12:46:33 +00:00
// S i d e b a r V i e w : G e n e r a t e s a s i m p l e T O C p e r l a n g u a g e a n d s u p p o r t s j u m p i n g t o l i n e s .
2025-09-25 09:01:45 +00:00
struct SidebarView : View {
let content : String
let language : String
@ State private var selectedTOCItem : String ?
2026-01-17 11:11:26 +00:00
2025-09-25 09:01:45 +00:00
var body : some View {
List ( generateTableOfContents ( ) , id : \ . self , selection : $ selectedTOCItem ) { item in
2026-01-17 11:11:26 +00:00
Button ( action : {
// E x p e c t i t e m f o r m a t : " . . . ( L i n e N ) "
if let startRange = item . range ( of : " (Line " ) ,
let endRange = item . range ( of : " ) " , range : startRange . upperBound . . < item . endIndex ) {
let numberStr = item [ startRange . upperBound . . < endRange . lowerBound ]
if let lineOneBased = Int ( numberStr . trimmingCharacters ( in : . whitespaces ) ) , lineOneBased > 0 {
DispatchQueue . main . async {
NotificationCenter . default . post ( name : . moveCursorToLine , object : lineOneBased )
}
2025-09-25 09:01:45 +00:00
}
}
2026-01-17 11:11:26 +00:00
} ) {
Text ( item )
. font ( . system ( size : 13 ) )
. foregroundColor ( . primary )
. padding ( . vertical , 4 )
. padding ( . horizontal , 8 )
. tag ( item )
}
. buttonStyle ( . plain )
2025-08-27 11:34:59 +00:00
}
2025-09-25 09:01:45 +00:00
. listStyle ( . sidebar )
. frame ( maxWidth : . infinity , alignment : . leading )
2026-01-25 12:46:33 +00:00
. onChange ( of : selectedTOCItem ) { _ , newValue in
2026-01-17 11:11:26 +00:00
guard let item = newValue else { return }
if let startRange = item . range ( of : " (Line " ) ,
let endRange = item . range ( of : " ) " , range : startRange . upperBound . . < item . endIndex ) {
let numberStr = item [ startRange . upperBound . . < endRange . lowerBound ]
if let lineOneBased = Int ( numberStr . trimmingCharacters ( in : . whitespaces ) ) , lineOneBased > 0 {
DispatchQueue . main . async {
NotificationCenter . default . post ( name : . moveCursorToLine , object : lineOneBased )
}
}
}
}
2025-09-25 09:01:45 +00:00
}
2026-01-17 11:11:26 +00:00
2026-01-25 12:46:33 +00:00
// N a i v e l i n e - s c a n n i n g T O C : l o o k s f o r l a n g u a g e - s p e c i f i c d e c l a r a t i o n s o r h e a d e r s .
2025-09-25 09:01:45 +00:00
func generateTableOfContents ( ) -> [ String ] {
guard ! content . isEmpty else { return [ " No content available " ] }
let lines = content . components ( separatedBy : . newlines )
var toc : [ String ] = [ ]
2026-01-17 11:11:26 +00:00
2025-09-25 09:01:45 +00:00
switch language {
case " swift " :
toc = lines . enumerated ( ) . compactMap { index , line in
let trimmed = line . trimmingCharacters ( in : . whitespaces )
if trimmed . hasPrefix ( " func " ) || trimmed . hasPrefix ( " struct " ) ||
trimmed . hasPrefix ( " class " ) || trimmed . hasPrefix ( " enum " ) {
return " \( trimmed ) (Line \( index + 1 ) ) "
}
return nil
2025-08-27 11:34:59 +00:00
}
2025-09-25 09:01:45 +00:00
case " python " :
toc = lines . enumerated ( ) . compactMap { index , line in
let trimmed = line . trimmingCharacters ( in : . whitespaces )
if trimmed . hasPrefix ( " def " ) || trimmed . hasPrefix ( " class " ) {
return " \( trimmed ) (Line \( index + 1 ) ) "
}
return nil
}
case " javascript " :
toc = lines . enumerated ( ) . compactMap { index , line in
let trimmed = line . trimmingCharacters ( in : . whitespaces )
if trimmed . hasPrefix ( " function " ) || trimmed . hasPrefix ( " class " ) {
return " \( trimmed ) (Line \( index + 1 ) ) "
}
return nil
}
case " c " , " cpp " :
toc = lines . enumerated ( ) . compactMap { index , line in
let trimmed = line . trimmingCharacters ( in : . whitespaces )
if trimmed . contains ( " ( " ) && ! trimmed . contains ( " ; " ) && ( trimmed . hasPrefix ( " void " ) || trimmed . hasPrefix ( " int " ) || trimmed . hasPrefix ( " float " ) || trimmed . hasPrefix ( " double " ) || trimmed . hasPrefix ( " char " ) || trimmed . contains ( " { " ) ) {
return " \( trimmed ) (Line \( index + 1 ) ) "
}
return nil
}
2026-01-23 11:49:52 +00:00
case " bash " , " zsh " :
toc = lines . enumerated ( ) . compactMap { index , line in
let trimmed = line . trimmingCharacters ( in : . whitespaces )
// S i m p l e f u n c t i o n d e t e c t i o n : n a m e ( ) { o r f u n c t i o n n a m e { o r n a m e ( ) \ n {
if trimmed . range ( of : " ^([A-Za-z_][A-Za-z0-9_]*) \\ s* \\ ( \\ ) \\ s* \\ { " , options : . regularExpression ) != nil ||
trimmed . range ( of : " ^function \\ s+[A-Za-z_][A-Za-z0-9_]* \\ s* \\ { " , options : . regularExpression ) != nil {
return " \( trimmed ) (Line \( index + 1 ) ) "
}
return nil
}
2025-09-25 09:01:45 +00:00
case " html " , " css " , " json " , " markdown " :
toc = lines . enumerated ( ) . compactMap { index , line in
let trimmed = line . trimmingCharacters ( in : . whitespaces )
if ! trimmed . isEmpty && ( trimmed . hasPrefix ( " # " ) || trimmed . hasPrefix ( " <h " ) ) {
return " \( trimmed ) (Line \( index + 1 ) ) "
}
return nil
}
default :
return [ " Unsupported language " ]
2025-08-25 07:39:12 +00:00
}
2026-01-17 11:11:26 +00:00
2025-09-25 09:01:45 +00:00
return toc . isEmpty ? [ " No headers found " ] : toc
2025-08-25 07:39:12 +00:00
}
2026-01-17 11:11:26 +00:00
}
2026-01-25 12:46:33 +00:00
// A c c e p t i n g T e x t V i e w : N S T e x t V i e w s u b c l a s s w i t h e n h a n c e d D n D , p a s t e b e h a v i o r , a u t o - i n d e n t ,
// b r a c k e t / q u o t e c o m p l e t i o n , a n d c a r e t c o n t r o l d u r i n g p a s t e .
2026-01-17 11:11:26 +00:00
final class AcceptingTextView : NSTextView {
override var acceptsFirstResponder : Bool { true }
override var mouseDownCanMoveWindow : Bool { false }
override var isOpaque : Bool { false }
2026-01-20 23:46:04 +00:00
// W e w a n t t h e c a r e t a t t h e * s t a r t * o f t h e p a s t e .
private var pendingPasteCaretLocation : Int ?
2026-01-25 12:46:33 +00:00
// MARK: - D r a g & D r o p : i n s e r t f i l e c o n t e n t s i n s t e a d o f f i l e p a t h
override func draggingEntered ( _ sender : NSDraggingInfo ) -> NSDragOperation {
let canRead = sender . draggingPasteboard . canReadObject ( forClasses : [ NSURL . self ] , options : [
. urlReadingFileURLsOnly : true
] )
return canRead ? . copy : [ ]
}
override func performDragOperation ( _ sender : NSDraggingInfo ) -> Bool {
let pb = sender . draggingPasteboard
if let nsurls = pb . readObjects ( forClasses : [ NSURL . self ] , options : [ . urlReadingFileURLsOnly : true ] ) as ? [ NSURL ] ,
let first = nsurls . first {
let url : URL = first as URL
let didAccess = url . startAccessingSecurityScopedResource ( )
defer { if didAccess { url . stopAccessingSecurityScopedResource ( ) } }
do {
// R e a d f i l e c o n t e n t s w i t h s e c u r i t y - s c o p e d a c c e s s
let content : String
if let data = try ? Data ( contentsOf : url ) {
if let s = String ( data : data , encoding : . utf8 ) {
content = s
} else if let s = String ( data : data , encoding : . utf16 ) {
content = s
} else {
content = try String ( contentsOf : url , encoding : . utf8 )
}
} else {
content = try String ( contentsOf : url , encoding : . utf8 )
}
// R e p l a c e c u r r e n t s e l e c t i o n w i t h t h e d r o p p e d f i l e c o n t e n t s
let nsContent = content as NSString
let sel = selectedRange ( )
undoManager ? . disableUndoRegistration ( )
textStorage ? . beginEditing ( )
textStorage ? . mutableString . replaceCharacters ( in : sel , with : nsContent as String )
textStorage ? . endEditing ( )
undoManager ? . enableUndoRegistration ( )
// N o t i f y t h e t e x t s y s t e m s o d e l e g a t e s / S w i f t U I b i n d i n g u p d a t e
self . didChangeText ( )
// M o v e c a r e t t o t h e e n d o f i n s e r t e d c o n t e n t a n d r e v e a l r a n g e
let newLoc = sel . location + nsContent . length
setSelectedRange ( NSRange ( location : newLoc , length : 0 ) )
// E n s u r e t h e f u l l i n s e r t e d r a n g e i s v i s i b l e
let insertedRange = NSRange ( location : sel . location , length : nsContent . length )
scrollRangeToVisible ( insertedRange )
return true
} catch {
return false
}
}
return false
}
2026-01-20 23:46:04 +00:00
// MARK: - T y p i n g h e l p e r s ( y o u r e x i s t i n g b e h a v i o r )
2026-01-17 11:11:26 +00:00
override func insertText ( _ insertString : Any , replacementRange : NSRange ) {
guard let s = insertString as ? String else {
super . insertText ( insertString , replacementRange : replacementRange )
return
}
2026-01-20 23:46:04 +00:00
2026-01-25 12:46:33 +00:00
// A u t o - i n d e n t b y c o p y i n g l e a d i n g w h i t e s p a c e
2026-01-17 11:11:26 +00:00
if s = = " \n " {
// A u t o - i n d e n t : c o p y l e a d i n g w h i t e s p a c e f r o m c u r r e n t l i n e
let ns = ( string as NSString )
let sel = selectedRange ( )
let lineRange = ns . lineRange ( for : NSRange ( location : sel . location , length : 0 ) )
2026-01-20 23:46:04 +00:00
let currentLine = ns . substring ( with : NSRange (
location : lineRange . location ,
length : max ( 0 , sel . location - lineRange . location )
) )
2026-01-17 11:11:26 +00:00
let indent = currentLine . prefix { $0 = = " " || $0 = = " \t " }
super . insertText ( " \n " + indent , replacementRange : replacementRange )
return
}
2026-01-20 23:46:04 +00:00
2026-01-25 12:46:33 +00:00
// A u t o - c l o s e c o m m o n b r a c k e t / q u o t e p a i r s
2026-01-17 11:11:26 +00:00
let pairs : [ String : String ] = [ " ( " : " ) " , " [ " : " ] " , " { " : " } " , " \" " : " \" " , " ' " : " ' " ]
if let closing = pairs [ s ] {
let sel = selectedRange ( )
super . insertText ( s + closing , replacementRange : replacementRange )
setSelectedRange ( NSRange ( location : sel . location + 1 , length : 0 ) )
return
}
2026-01-20 23:46:04 +00:00
2026-01-17 11:11:26 +00:00
super . insertText ( insertString , replacementRange : replacementRange )
2025-08-27 11:34:59 +00:00
}
2026-01-17 11:36:31 +00:00
2026-01-25 12:46:33 +00:00
// P a s t e : c a p t u r e i n s e r t i o n p o i n t a n d e n f o r c e c a r e t p o s i t i o n a f t e r p a s t e a c r o s s a s y n c u p d a t e s .
2026-01-17 11:36:31 +00:00
override func paste ( _ sender : Any ? ) {
2026-01-20 23:46:04 +00:00
// C a p t u r e w h e r e p a s t e b e g i n s ( s t a r t o f i n s e r t i o n / r e p l a c e m e n t )
pendingPasteCaretLocation = selectedRange ( ) . location
// K e e p y o u r e x i s t i n g n o t i f i c a t i o n b e h a v i o r
2026-01-17 12:04:11 +00:00
let pastedString = NSPasteboard . general . string ( forType : . string )
2026-01-20 23:46:04 +00:00
2026-01-17 11:36:31 +00:00
super . paste ( sender )
2026-01-20 23:46:04 +00:00
2026-01-17 12:04:11 +00:00
if let pastedString , ! pastedString . isEmpty {
NotificationCenter . default . post ( name : . pastedText , object : pastedString )
}
2026-01-20 23:46:04 +00:00
// E n f o r c e c a r e t a f t e r p a s t e ( m u l t i p l e t i c k s b e a t s l a t e s e l e c t i o n c h a n g e s )
schedulePasteCaretEnforcement ( )
}
override func didChangeText ( ) {
super . didChangeText ( )
// P a s t i n g t r i g g e r s d i d C h a n g e T e x t ; s c h e d u l e e n f o r c e m e n t a g a i n .
schedulePasteCaretEnforcement ( )
}
2026-01-25 12:46:33 +00:00
// R e - a p p l y t h e d e s i r e d c a r e t p o s i t i o n o v e r m u l t i p l e r u n l o o p t i c k s t o b e a t l a t e l a y o u t / a s y n c w o r k .
2026-01-20 23:46:04 +00:00
private func schedulePasteCaretEnforcement ( ) {
guard pendingPasteCaretLocation != nil else { return }
// C a n c e l p r e v i o u s l y q u e u e d e n f o r c e m e n t t o a v o i d s p a m m i n g
NSObject . cancelPreviousPerformRequests ( withTarget : self , selector : #selector ( applyPendingPasteCaret ) , object : nil )
// R u n n e x t t u r n
perform ( #selector ( applyPendingPasteCaret ) , with : nil , afterDelay : 0 )
// R u n a g a i n n e x t r u n l o o p t i c k ( b e a t s " s n a p b a c k " f r o m l a t e a s y n c w o r k )
DispatchQueue . main . async { [ weak self ] in
self ? . applyPendingPasteCaret ( )
}
// R u n o n c e m o r e w i t h a t i n y d e l a y ( b e a t s s l o w e r a s y n c h i g h l i g h t p a s s e s )
DispatchQueue . main . asyncAfter ( deadline : . now ( ) + 0.02 ) { [ weak self ] in
self ? . applyPendingPasteCaret ( )
}
}
@objc private func applyPendingPasteCaret ( ) {
guard let desired = pendingPasteCaretLocation else { return }
let length = ( string as NSString ) . length
let loc = min ( max ( 0 , desired ) , length )
let range = NSRange ( location : loc , length : 0 )
// S e t c a r e t a n d k e e p i t v i s i b l e
2026-01-17 11:36:31 +00:00
setSelectedRange ( range )
2026-01-20 23:46:04 +00:00
if let container = textContainer {
layoutManager ? . ensureLayout ( for : container )
}
2026-01-17 11:36:31 +00:00
scrollRangeToVisible ( range )
2026-01-20 23:46:04 +00:00
// I m p o r t a n t : c l e a r o n l y a f t e r w e ' v e e n f o r c e d a t l e a s t o n c e .
// T h e d e l a y e d c a l l s w i l l n o - o p o n c e t h i s i s n i l .
pendingPasteCaretLocation = nil
2026-01-17 11:36:31 +00:00
}
2025-08-27 11:34:59 +00:00
}
2026-01-25 12:46:33 +00:00
// N S V i e w R e p r e s e n t a b l e w r a p p e r a r o u n d N S T e x t V i e w t o i n t e g r a t e w i t h S w i f t U I .
2025-09-25 09:01:45 +00:00
struct CustomTextEditor : NSViewRepresentable {
2025-08-27 11:34:59 +00:00
@ Binding var text : String
let language : String
2025-09-25 09:01:45 +00:00
let colorScheme : ColorScheme
2026-01-17 11:11:26 +00:00
let fontSize : CGFloat
@ Binding var isLineWrapEnabled : Bool
2026-01-25 12:46:33 +00:00
// T o g g l e s o f t - w r a p p i n g b y a d j u s t i n g t e x t c o n t a i n e r s i z i n g a n d s c r o l l e r v i s i b i l i t y .
2026-01-17 11:11:26 +00:00
private func applyWrapMode ( isWrapped : Bool , textView : NSTextView , scrollView : NSScrollView ) {
if isWrapped {
// W r a p : t r a c k t h e t e x t v i e w w i d t h , n o h o r i z o n t a l s c r o l l i n g
textView . isHorizontallyResizable = false
textView . textContainer ? . widthTracksTextView = true
textView . textContainer ? . heightTracksTextView = false
scrollView . hasHorizontalScroller = false
// E n s u r e t h e c o n t a i n e r w i d t h m a t c h e s t h e v i s i b l e c o n t e n t w i d t h
let contentWidth = scrollView . contentSize . width
let width = contentWidth > 0 ? contentWidth : scrollView . frame . size . width
textView . textContainer ? . containerSize = NSSize ( width : width , height : CGFloat . greatestFiniteMagnitude )
} else {
// N o w r a p : a l l o w h o r i z o n t a l e x p a n s i o n a n d h o r i z o n t a l s c r o l l i n g
textView . isHorizontallyResizable = true
textView . textContainer ? . widthTracksTextView = false
textView . textContainer ? . heightTracksTextView = false
scrollView . hasHorizontalScroller = true
textView . textContainer ? . containerSize = NSSize ( width : CGFloat . greatestFiniteMagnitude , height : CGFloat . greatestFiniteMagnitude )
}
}
2025-08-26 17:25:39 +00:00
func makeNSView ( context : Context ) -> NSScrollView {
2026-01-25 12:46:33 +00:00
// B u i l d s c r o l l v i e w a n d t e x t v i e w
2026-01-17 11:36:31 +00:00
let scrollView = NSScrollView ( )
2026-01-17 11:11:26 +00:00
scrollView . drawsBackground = false
scrollView . autohidesScrollers = true
scrollView . hasVerticalScroller = true
scrollView . contentView . postsBoundsChangedNotifications = true
2025-09-25 09:01:45 +00:00
2026-01-17 11:36:31 +00:00
let textView = AcceptingTextView ( frame : . zero )
2026-01-25 12:46:33 +00:00
// C o n f i g u r e e d i t i n g b e h a v i o r a n d v i s u a l s
2026-01-17 11:11:26 +00:00
textView . isEditable = true
2025-08-27 11:34:59 +00:00
textView . isRichText = false
2025-09-25 09:01:45 +00:00
textView . usesFindBar = true
2026-01-17 11:11:26 +00:00
textView . isVerticallyResizable = true
textView . isHorizontallyResizable = false
textView . font = NSFont . monospacedSystemFont ( ofSize : fontSize , weight : . regular )
textView . backgroundColor = . textBackgroundColor
textView . textContainerInset = NSSize ( width : 12 , height : 12 )
textView . minSize = NSSize ( width : 0 , height : 0 )
textView . maxSize = NSSize ( width : CGFloat . greatestFiniteMagnitude , height : CGFloat . greatestFiniteMagnitude )
textView . isSelectable = true
2025-09-25 09:01:45 +00:00
textView . allowsUndo = true
2026-01-17 11:11:26 +00:00
textView . textColor = . labelColor
textView . insertionPointColor = . controlAccentColor
textView . drawsBackground = true
textView . isAutomaticTextCompletionEnabled = false
// D i s a b l e s m a r t s u b s t i t u t i o n s / d e t e c t i o n s t h a t c a n i n t e r f e r e w i t h s e l e c t i o n w h e n r e c o l o r i n g
2025-09-25 09:01:45 +00:00
textView . isAutomaticQuoteSubstitutionEnabled = false
2026-01-17 11:11:26 +00:00
textView . isAutomaticDashSubstitutionEnabled = false
2025-09-25 09:01:45 +00:00
textView . isAutomaticDataDetectionEnabled = false
textView . isAutomaticLinkDetectionEnabled = false
2026-01-17 11:11:26 +00:00
textView . isGrammarCheckingEnabled = false
textView . isContinuousSpellCheckingEnabled = false
textView . smartInsertDeleteEnabled = false
2025-09-25 09:01:45 +00:00
2026-01-25 12:46:33 +00:00
textView . registerForDraggedTypes ( [ . fileURL , . URL ] )
2026-01-17 11:36:31 +00:00
// E m b e d t h e t e x t v i e w i n t h e s c r o l l v i e w
scrollView . documentView = textView
// C o n f i g u r e t h e t e x t v i e w d e l e g a t e
2026-01-17 11:11:26 +00:00
textView . delegate = context . coordinator
2025-09-25 09:01:45 +00:00
2026-01-25 12:46:33 +00:00
// I n s t a l l l i n e n u m b e r r u l e r
2026-01-17 11:11:26 +00:00
scrollView . hasVerticalRuler = true
scrollView . rulersVisible = true
scrollView . verticalRulerView = LineNumberRulerView ( textView : textView )
2025-09-25 09:01:45 +00:00
2026-01-25 12:46:33 +00:00
// A p p l y w r a p p i n g a n d s e e d i n i t i a l c o n t e n t
2026-01-17 11:11:26 +00:00
applyWrapMode ( isWrapped : isLineWrapEnabled , textView : textView , scrollView : scrollView )
2025-09-25 09:01:45 +00:00
2026-01-17 11:11:26 +00:00
// S e e d i n i t i a l t e x t
textView . string = text
DispatchQueue . main . async { [ weak scrollView , weak textView ] in
guard let sv = scrollView , let tv = textView else { return }
sv . window ? . makeFirstResponder ( tv )
}
context . coordinator . scheduleHighlightIfNeeded ( currentText : text )
2025-09-25 09:01:45 +00:00
2026-01-25 12:46:33 +00:00
// K e e p c o n t a i n e r w i d t h i n s y n c w h e n t h e s c r o l l v i e w r e s i z e s
2026-01-17 11:11:26 +00:00
NotificationCenter . default . addObserver ( forName : NSView . boundsDidChangeNotification , object : scrollView . contentView , queue : . main ) { [ weak textView , weak scrollView ] _ in
guard let tv = textView , let sv = scrollView else { return }
if tv . textContainer ? . widthTracksTextView = = true {
tv . textContainer ? . containerSize . width = sv . contentSize . width
if let container = tv . textContainer {
tv . layoutManager ? . ensureLayout ( for : container )
}
}
2025-09-25 09:01:45 +00:00
}
2026-01-17 11:11:26 +00:00
context . coordinator . textView = textView
2025-08-26 17:25:39 +00:00
return scrollView
2025-08-25 07:39:12 +00:00
}
2026-01-17 11:11:26 +00:00
2026-01-25 12:46:33 +00:00
// K e e p N S T e x t V i e w i n s y n c w i t h S w i f t U I s t a t e a n d s c h e d u l e h i g h l i g h t i n g w h e n n e e d e d .
2025-08-26 17:25:39 +00:00
func updateNSView ( _ nsView : NSScrollView , context : Context ) {
2025-08-27 11:34:59 +00:00
if let textView = nsView . documentView as ? NSTextView {
2026-01-17 11:11:26 +00:00
if textView . string != text {
textView . string = text
}
if textView . font ? . pointSize != fontSize {
textView . font = NSFont . monospacedSystemFont ( ofSize : fontSize , weight : . regular )
2025-09-25 09:01:45 +00:00
}
2026-01-17 11:11:26 +00:00
// K e e p t h e t e x t c o n t a i n e r w i d t h i n s y n c & r e l a y o u t
applyWrapMode ( isWrapped : isLineWrapEnabled , textView : textView , scrollView : nsView )
2026-01-23 11:49:52 +00:00
if let textContainer = textView . textContainer {
textView . layoutManager ? . ensureLayout ( for : textContainer )
2025-08-27 11:34:59 +00:00
}
2025-09-25 09:01:45 +00:00
textView . invalidateIntrinsicContentSize ( )
2026-01-17 11:11:26 +00:00
// O n l y s c h e d u l e h i g h l i g h t i f n e e d e d ( e . g . , l a n g u a g e / c o l o r s c h e m e c h a n g e s o r e x t e r n a l t e x t u p d a t e s )
2026-01-17 11:36:31 +00:00
context . coordinator . parent = self
2026-01-17 11:11:26 +00:00
context . coordinator . scheduleHighlightIfNeeded ( )
2025-08-26 09:49:52 +00:00
}
}
2026-01-17 11:11:26 +00:00
2025-08-26 09:49:52 +00:00
func makeCoordinator ( ) -> Coordinator {
Coordinator ( self )
}
2026-01-17 11:11:26 +00:00
2026-01-25 12:46:33 +00:00
// C o o r d i n a t o r : N S T e x t V i e w D e l e g a t e t h a t b r i d g e s N S T e x t c h a n g e s t o S w i f t U I a n d m a n a g e s h i g h l i g h t i n g .
2025-08-26 09:49:52 +00:00
class Coordinator : NSObject , NSTextViewDelegate {
2025-09-25 09:01:45 +00:00
var parent : CustomTextEditor
weak var textView : NSTextView ?
2026-01-17 11:11:26 +00:00
2026-01-25 12:46:33 +00:00
// B a c k g r o u n d q u e u e + d e b o u n c e r f o r r e g e x - b a s e d h i g h l i g h t i n g
2026-01-17 11:11:26 +00:00
private let highlightQueue = DispatchQueue ( label : " NeonVision.SyntaxHighlight " , qos : . userInitiated )
2026-01-25 12:46:33 +00:00
// S n a p s h o t s o f l a s t h i g h l i g h t e d s t a t e t o a v o i d r e d u n d a n t w o r k
2026-01-17 11:11:26 +00:00
private var pendingHighlight : DispatchWorkItem ?
private var lastHighlightedText : String = " "
private var lastLanguage : String ?
private var lastColorScheme : ColorScheme ?
2025-09-25 09:01:45 +00:00
init ( _ parent : CustomTextEditor ) {
2025-08-26 09:49:52 +00:00
self . parent = parent
2025-09-25 09:01:45 +00:00
super . init ( )
NotificationCenter . default . addObserver ( self , selector : #selector ( moveToLine ( _ : ) ) , name : . moveCursorToLine , object : nil )
NotificationCenter . default . addObserver ( self , selector : #selector ( streamSuggestion ( _ : ) ) , name : . streamSuggestion , object : nil )
2025-08-26 09:49:52 +00:00
}
2026-01-17 11:11:26 +00:00
deinit {
NotificationCenter . default . removeObserver ( self )
2025-08-25 07:39:12 +00:00
}
2026-01-17 11:11:26 +00:00
2026-01-25 12:46:33 +00:00
// / S c h e d u l e s h i g h l i g h t i n g i f t e x t / l a n g u a g e / t h e m e c h a n g e d . S k i p s v e r y l a r g e d o c u m e n t s
// / a n d d e f e r s w h e n a m o d a l s h e e t i s p r e s e n t e d .
2026-01-17 11:11:26 +00:00
func scheduleHighlightIfNeeded ( currentText : String ? = nil ) {
guard textView != nil else { return }
2026-01-23 11:49:52 +00:00
// Q u e r y N S A p p . m o d a l W i n d o w o n t h e m a i n t h r e a d t o a v o i d t h r e a d - c h e c k w a r n i n g s
let isModalPresented : Bool = {
if Thread . isMainThread {
return NSApp . modalWindow != nil
} else {
var result = false
DispatchQueue . main . sync { result = ( NSApp . modalWindow != nil ) }
return result
}
} ( )
if isModalPresented {
2026-01-17 11:11:26 +00:00
pendingHighlight ? . cancel ( )
let work = DispatchWorkItem { [ weak self ] in
self ? . scheduleHighlightIfNeeded ( currentText : currentText )
2025-08-27 11:34:59 +00:00
}
2026-01-17 11:11:26 +00:00
pendingHighlight = work
highlightQueue . asyncAfter ( deadline : . now ( ) + 0.3 , execute : work )
return
2025-08-27 11:34:59 +00:00
}
2026-01-17 11:11:26 +00:00
let lang = parent . language
let scheme = parent . colorScheme
2026-01-25 13:06:31 +00:00
let text : String = {
if let currentText = currentText {
return currentText
}
if Thread . isMainThread {
return textView ? . string ? ? " "
}
var result = " "
DispatchQueue . main . sync {
result = textView ? . string ? ? " "
}
return result
} ( )
2026-01-25 12:46:33 +00:00
// S k i p e x p e n s i v e h i g h l i g h t i n g f o r v e r y l a r g e d o c u m e n t s
let nsLen = ( text as NSString ) . length
if nsLen > 200_000 { // ~ 2 0 0 k U T F - 1 6 c o d e u n i t s
self . lastHighlightedText = text
self . lastLanguage = lang
self . lastColorScheme = scheme
return
}
2026-01-17 11:11:26 +00:00
if text = = lastHighlightedText && lastLanguage = = lang && lastColorScheme = = scheme {
return
2025-08-27 11:34:59 +00:00
}
2026-01-17 11:11:26 +00:00
rehighlight ( )
2025-09-25 09:01:45 +00:00
}
2026-01-17 11:11:26 +00:00
2026-01-25 12:46:33 +00:00
// / P e r f o r m r e g e x - b a s e d t o k e n c o l o r i n g o f f - m a i n , t h e n a p p l y a t t r i b u t e s o n t h e m a i n t h r e a d .
2026-01-17 11:11:26 +00:00
func rehighlight ( ) {
guard let textView = textView else { return }
// S n a p s h o t c u r r e n t s t a t e
let textSnapshot = textView . string
let language = parent . language
let scheme = parent . colorScheme
let selected = textView . selectedRange ( )
let colors = SyntaxColors . fromVibrantLightTheme ( colorScheme : scheme )
let patterns = getSyntaxPatterns ( for : language , colors : colors )
// C a n c e l a n y i n - f l i g h t w o r k
pendingHighlight ? . cancel ( )
let work = DispatchWorkItem { [ weak self ] in
// C o m p u t e m a t c h e s o f f t h e m a i n t h r e a d
let nsText = textSnapshot as NSString
let fullRange = NSRange ( location : 0 , length : nsText . length )
var coloredRanges : [ ( NSRange , Color ) ] = [ ]
for ( pattern , color ) in patterns {
guard let regex = try ? NSRegularExpression ( pattern : pattern , options : [ . anchorsMatchLines ] ) else { continue }
let matches = regex . matches ( in : textSnapshot , range : fullRange )
for match in matches {
coloredRanges . append ( ( match . range , color ) )
}
}
DispatchQueue . main . async { [ weak self ] in
guard let self = self , let tv = self . textView else { return }
// D i s c a r d i f t e x t c h a n g e d s i n c e w e s t a r t e d
guard tv . string = = textSnapshot else { return }
tv . textStorage ? . beginEditing ( )
// C l e a r p r e v i o u s c o l o r i n g a n d a p p l y b a s e c o l o r
tv . textStorage ? . removeAttribute ( . foregroundColor , range : fullRange )
tv . textStorage ? . addAttribute ( . foregroundColor , value : tv . textColor ? ? NSColor . labelColor , range : fullRange )
// A p p l y c o l o r e d r a n g e s
for ( range , color ) in coloredRanges {
tv . textStorage ? . addAttribute ( . foregroundColor , value : NSColor ( color ) , range : range )
}
tv . textStorage ? . endEditing ( )
// R e s t o r e s e l e c t i o n o n l y i f i t h a s n ' t c h a n g e d s i n c e w e s t a r t e d
if NSEqualRanges ( tv . selectedRange ( ) , selected ) {
tv . setSelectedRange ( selected )
}
// U p d a t e l a s t h i g h l i g h t e d s t a t e
self . lastHighlightedText = textSnapshot
self . lastLanguage = language
self . lastColorScheme = scheme
}
2025-08-27 11:34:59 +00:00
}
2026-01-17 11:11:26 +00:00
pendingHighlight = work
// D e b o u n c e s l i g h t l y t o a v o i d t h r a s h i n g w h i l e t y p i n g
highlightQueue . asyncAfter ( deadline : . now ( ) + 0.12 , execute : work )
2025-09-25 09:01:45 +00:00
}
2026-01-17 11:11:26 +00:00
2025-09-25 09:01:45 +00:00
func textDidChange ( _ notification : Notification ) {
guard let textView = notification . object as ? NSTextView else { return }
2026-01-25 12:46:33 +00:00
// U p d a t e S w i f t U I b i n d i n g , c a r e t s t a t u s , t r i g g e r s u g g e s t i o n , a n d r e h i g h l i g h t .
2025-09-25 09:01:45 +00:00
parent . text = textView . string
2026-01-17 11:11:26 +00:00
updateCaretStatusAndHighlight ( )
2026-01-20 23:46:04 +00:00
// A u t o - s u g g e s t w h i l e t y p i n g ( d e b o u n c e d )
DispatchQueue . main . async {
NotificationCenter . default . post ( name : . triggerSuggestion , object : nil )
}
2026-01-17 11:11:26 +00:00
scheduleHighlightIfNeeded ( currentText : parent . text )
2025-08-27 11:34:59 +00:00
}
2026-01-17 11:11:26 +00:00
func textViewDidChangeSelection ( _ notification : Notification ) {
updateCaretStatusAndHighlight ( )
}
2026-01-25 12:46:33 +00:00
// C o m p u t e ( l i n e , c o l u m n ) , b r o a d c a s t , a n d h i g h l i g h t t h e c u r r e n t l i n e .
2026-01-17 11:11:26 +00:00
private func updateCaretStatusAndHighlight ( ) {
guard let tv = textView else { return }
let ns = tv . string as NSString
let sel = tv . selectedRange ( )
let location = sel . location
let prefix = ns . substring ( to : min ( location , ns . length ) )
let line = prefix . reduce ( 1 ) { $1 = = " \n " ? $0 + 1 : $0 }
let col : Int = {
if let lastNL = prefix . lastIndex ( of : " \n " ) {
return prefix . distance ( from : lastNL , to : prefix . endIndex ) - 1
} else {
return prefix . count
2025-09-25 09:01:45 +00:00
}
2026-01-17 11:11:26 +00:00
} ( )
NotificationCenter . default . post ( name : . caretPositionDidChange , object : nil , userInfo : [ " line " : line , " column " : col ] )
// H i g h l i g h t c u r r e n t l i n e
let lineRange = ns . lineRange ( for : NSRange ( location : location , length : 0 ) )
let fullRange = NSRange ( location : 0 , length : ns . length )
tv . textStorage ? . beginEditing ( )
tv . textStorage ? . removeAttribute ( . backgroundColor , range : fullRange )
tv . textStorage ? . addAttribute ( . backgroundColor , value : NSColor . selectedTextBackgroundColor . withAlphaComponent ( 0.12 ) , range : lineRange )
tv . textStorage ? . endEditing ( )
2025-08-26 17:25:39 +00:00
}
2025-08-27 11:34:59 +00:00
2026-01-25 12:46:33 +00:00
// / M o v e c a r e t t o a 1 - b a s e d l i n e n u m b e r , c l a m p i n g t o b o u n d s , a n d e m p h a s i z e t h e l i n e .
2026-01-17 11:11:26 +00:00
@objc func moveToLine ( _ notification : Notification ) {
guard let lineOneBased = notification . object as ? Int ,
let textView = textView else { return }
2025-08-26 17:25:39 +00:00
2026-01-17 11:11:26 +00:00
// I f t h e r e ' s n o t e x t , n o t h i n g t o d o
let currentText = textView . string
guard ! currentText . isEmpty else { return }
2025-08-27 11:34:59 +00:00
2026-01-17 11:11:26 +00:00
// C a n c e l a n y i n - f l i g h t h i g h l i g h t t o p r e v e n t i t f r o m r e s t o r i n g a n o l d s e l e c t i o n
pendingHighlight ? . cancel ( )
2025-09-25 09:01:45 +00:00
2026-01-17 11:11:26 +00:00
// W o r k w i t h N S S t r i n g / U T F - 1 6 i n d i c e s t o m a t c h N S T e x t V i e w e x p e c t a t i o n s
let ns = currentText as NSString
let totalLength = ns . length
2025-08-25 07:39:12 +00:00
2026-01-17 11:11:26 +00:00
// C l a m p t a r g e t l i n e t o a v a i l a b l e l i n e c o u n t ( 1 - b a s e d i n p u t )
let linesArray = currentText . components ( separatedBy : . newlines )
let clampedLineIndex = max ( 1 , min ( lineOneBased , linesArray . count ) ) - 1 // 0 - b a s e d i n d e x
// C o m p u t e t h e U T F - 1 6 l o c a t i o n b y s u m m i n g U T F - 1 6 l e n g t h s o f p r e c e d i n g l i n e s + n e w l i n e c h a r a c t e r s
var location = 0
if clampedLineIndex > 0 {
for i in 0. . < ( clampedLineIndex ) {
let lineNSString = linesArray [ i ] as NSString
location += lineNSString . length
// A d d o n e f o r t h e n e w l i n e t h a t s e p a r a t e s l i n e s , a s c o m p o n e n t s ( s e p a r a t e d B y : ) d r o p s s e p a r a t o r s
location += 1
2025-09-25 09:01:45 +00:00
}
2026-01-17 11:11:26 +00:00
}
// S a f e t y c l a m p
location = max ( 0 , min ( location , totalLength ) )
2025-08-27 11:34:59 +00:00
2026-01-17 11:11:26 +00:00
// M o v e c a r e t a n d s c r o l l i n t o v i e w o n t h e m a i n t h r e a d
DispatchQueue . main . async { [ weak self ] in
guard let self = self , let tv = self . textView else { return }
tv . window ? . makeFirstResponder ( tv )
// E n s u r e l a y o u t i s u p - t o - d a t e b e f o r e s c r o l l i n g
2026-01-23 11:49:52 +00:00
if let textContainer = tv . textContainer {
tv . layoutManager ? . ensureLayout ( for : textContainer )
2025-09-25 09:01:45 +00:00
}
2026-01-17 11:11:26 +00:00
tv . setSelectedRange ( NSRange ( location : location , length : 0 ) )
tv . scrollRangeToVisible ( NSRange ( location : location , length : 0 ) )
// S t r o n g e r h i g h l i g h t f o r t h e e n t i r e t a r g e t l i n e
let lineRange = ns . lineRange ( for : NSRange ( location : location , length : 0 ) )
let fullRange = NSRange ( location : 0 , length : totalLength )
tv . textStorage ? . beginEditing ( )
tv . textStorage ? . removeAttribute ( . backgroundColor , range : fullRange )
tv . textStorage ? . addAttribute ( . backgroundColor , value : NSColor . selectedTextBackgroundColor . withAlphaComponent ( 0.18 ) , range : lineRange )
tv . textStorage ? . endEditing ( )
}
}
@objc func streamSuggestion ( _ notification : Notification ) {
guard let stream = notification . object as ? AsyncStream < String > ,
let textView = textView else { return }
2026-01-20 23:46:04 +00:00
Task { [ weak textView , weak self ] in
2026-01-25 12:46:33 +00:00
// N O T E : A l l N S T e x t V i e w i n t e r a c t i o n s m u s t r u n o n t h e m a i n t h r e a d .
2026-01-20 23:46:04 +00:00
guard let textView , let self else { return }
2026-01-17 11:11:26 +00:00
for await chunk in stream {
2026-01-20 23:46:04 +00:00
// S n a p s h o t c u r r e n t c a r e t / s e l e c t i o n + w h a t ’ s v i s i b l e B E F O R E w e m o d i f y a n y t h i n g
let oldSelection = textView . selectedRange ( )
let oldVisibleRect = textView . visibleRect
// A p p e n d s t r e a m e d s u g g e s t i o n
2026-01-17 11:11:26 +00:00
textView . textStorage ? . append ( NSAttributedString ( string : chunk ) )
2026-01-20 23:46:04 +00:00
self . parent . text = textView . string
// R e s t o r e s e l e c t i o n a n d v i e w p o r t s o w e d o n ' t j u m p t o t h e e n d
DispatchQueue . main . async {
textView . setSelectedRange ( oldSelection )
textView . scroll ( oldVisibleRect . origin )
}
2025-08-27 11:34:59 +00:00
}
}
}
}
2025-08-25 07:39:12 +00:00
}
2025-08-27 11:34:59 +00:00
2026-01-25 12:46:33 +00:00
// V e r t i c a l r u l e r t h a t p a i n t s l i n e n u m b e r s a l i g n e d t o v i s i b l e t e x t l i n e s .
2026-01-17 11:11:26 +00:00
final class LineNumberRulerView : NSRulerView {
private weak var textView : NSTextView ?
private let font = NSFont . monospacedSystemFont ( ofSize : 11 , weight : . regular )
private let textColor = NSColor . secondaryLabelColor
private let inset : CGFloat = 4
init ( textView : NSTextView ) {
self . textView = textView
super . init ( scrollView : textView . enclosingScrollView , orientation : . verticalRuler )
self . clientView = textView
self . ruleThickness = 44
NotificationCenter . default . addObserver ( self , selector : #selector ( redraw ) , name : NSText . didChangeNotification , object : textView )
NotificationCenter . default . addObserver ( self , selector : #selector ( redraw ) , name : NSView . boundsDidChangeNotification , object : scrollView ? . contentView )
2026-01-20 23:46:04 +00:00
NotificationCenter . default . addObserver ( self , selector : #selector ( redraw ) , name : NSView . boundsDidChangeNotification , object : textView . enclosingScrollView ? . contentView )
2026-01-17 11:11:26 +00:00
}
required init ( coder : NSCoder ) { fatalError ( " init(coder:) has not been implemented " ) }
@objc private func redraw ( ) { needsDisplay = true }
override func drawHashMarksAndLabels ( in rect : NSRect ) {
2026-01-23 11:49:52 +00:00
guard let tv = textView , let lm = tv . layoutManager , tv . textContainer != nil else { return }
2026-01-20 23:46:04 +00:00
// U s e t h e t e x t v i e w ' s v i s i b l e r e c t ( a l r e a d y i n t h e c o r r e c t c o o r d i n a t e s p a c e & r e s p e c t s f l i p p i n g / i n s e t s )
let visibleRect = tv . visibleRect
let tcOrigin = tv . textContainerOrigin // a c c o u n t s f o r t e x t C o n t a i n e r I n s e t
2026-01-25 12:46:33 +00:00
// D e t e r m i n e f i r s t v i s i b l e c h a r a c t e r a n d l i n e n u m b e r
2026-01-20 23:46:04 +00:00
let probePoint = NSPoint ( x : visibleRect . minX + 2 , y : visibleRect . minY + 2 )
let firstVisibleCharIndex = tv . characterIndexForInsertion ( at : probePoint )
// C o m p u t e t h e f i r s t v i s i b l e l i n e n u m b e r b y c o u n t i n g n e w l i n e s u p t o t h a t c h a r a c t e r i n d e x
let fullString = tv . string as NSString
let clampedCharIndex = min ( max ( firstVisibleCharIndex , 0 ) , fullString . length )
let prefix = fullString . substring ( to : clampedCharIndex )
var currentLineNumber = prefix . reduce ( 1 ) { $1 = = " \n " ? $0 + 1 : $0 }
// E n s u r e l a y o u t i s a v a i l a b l e a r o u n d t h e f i r s t v i s i b l e c h a r a c t e r
lm . ensureLayout ( forCharacterRange : NSRange ( location : clampedCharIndex , length : 0 ) )
2026-01-25 12:46:33 +00:00
// I t e r a t e l i n e f r a g m e n t s a n d c o m p u t e d r a w p o s i t i o n s
2026-01-20 23:46:04 +00:00
var glyphIndex = lm . glyphIndexForCharacter ( at : clampedCharIndex )
while glyphIndex < lm . numberOfGlyphs {
var effectiveGlyphRange = NSRange ( location : 0 , length : 0 )
// A l l o w l a y o u t m a n a g e r t o l a y o u t a d d i t i o n a l t e x t a s n e e d e d w h i l e w e s c r o l l
let lineRectInContainer = lm . lineFragmentRect (
forGlyphAt : glyphIndex ,
effectiveRange : & effectiveGlyphRange ,
withoutAdditionalLayout : false
)
let usedRectInContainer = lm . lineFragmentUsedRect (
forGlyphAt : glyphIndex ,
effectiveRange : nil ,
withoutAdditionalLayout : false
)
// C o n v e r t c o n t a i n e r r e c t s - > t e x t v i e w c o o r d i n a t e s
let lineRectInView = NSRect (
x : lineRectInContainer . origin . x + tcOrigin . x ,
y : lineRectInContainer . origin . y + tcOrigin . y ,
width : lineRectInContainer . size . width ,
height : lineRectInContainer . size . height
)
let usedRectInView = NSRect (
x : usedRectInContainer . origin . x + tcOrigin . x ,
y : usedRectInContainer . origin . y + tcOrigin . y ,
width : usedRectInContainer . size . width ,
height : usedRectInContainer . size . height
)
// S t o p o n c e w e ' r e b e l o w t h e v i s i b l e a r e a
if lineRectInView . minY > visibleRect . maxY { break }
2026-01-25 12:46:33 +00:00
// D r a w l i n e n u m b e r s a l i g n e d w i t h b a s e l i n e s
2026-01-20 23:46:04 +00:00
// C o m p u t e a s t a b l e v e r t i c a l p o s i t i o n ( b a s e l i n e - i s h i f p o s s i b l e , o t h e r w i s e c e n t e r )
var drawYInView : CGFloat
if effectiveGlyphRange . length > 0 {
let baselinePoint = lm . location ( forGlyphAt : glyphIndex )
drawYInView = ( lineRectInView . minY + baselinePoint . y )
} else {
drawYInView = usedRectInView . midY
}
// C o n v e r t t e x t v i e w Y - > r u l e r v i e w Y ( r u l e r i s s y n c e d t o v i s i b l e R e c t )
let drawY = ( drawYInView - visibleRect . minY ) + bounds . minY
let numberString = NSString ( string : " \( currentLineNumber ) " )
let attributes : [ NSAttributedString . Key : Any ] = [
. font : font ,
. foregroundColor : textColor
]
2026-01-17 11:11:26 +00:00
let size = numberString . size ( withAttributes : attributes )
2026-01-20 23:46:04 +00:00
// C e n t e r t h e l a b e l v e r t i c a l l y a r o u n d c o m p u t e d Y
let drawPoint = NSPoint ( x : bounds . maxX - size . width - inset , y : drawY - size . height / 2.0 )
2026-01-17 11:11:26 +00:00
numberString . draw ( at : drawPoint , withAttributes : attributes )
2026-01-20 23:46:04 +00:00
// A d v a n c e t o n e x t l i n e f r a g m e n t
glyphIndex = max ( effectiveGlyphRange . upperBound , glyphIndex + 1 )
currentLineNumber += 1
2026-01-17 11:11:26 +00:00
}
}
2025-09-25 09:01:45 +00:00
}
2025-08-25 07:39:12 +00:00
2026-01-25 12:46:33 +00:00
// S y n t a x C o l o r s : p a l e t t e f o r t o k e n t y p e s ; d e r i v e d f r o m a v i b r a n t t h e m e a n d r e s p e c t s d a r k m o d e .
2025-09-25 09:01:45 +00:00
struct SyntaxColors {
let keyword : Color
let string : Color
let number : Color
let comment : Color
let attribute : Color
let variable : Color
let def : Color
let property : Color
let meta : Color
let tag : Color
let atom : Color
let builtin : Color
let type : Color
2026-01-17 11:11:26 +00:00
2025-09-25 09:01:45 +00:00
static func fromVibrantLightTheme ( colorScheme : ColorScheme ) -> SyntaxColors {
let baseColors : [ String : ( light : Color , dark : Color ) ] = [
" keyword " : ( light : Color ( red : 251 / 255 , green : 0 / 255 , blue : 186 / 255 ) , dark : Color ( red : 251 / 255 , green : 0 / 255 , blue : 186 / 255 ) ) ,
" string " : ( light : Color ( red : 190 / 255 , green : 0 / 255 , blue : 255 / 255 ) , dark : Color ( red : 190 / 255 , green : 0 / 255 , blue : 255 / 255 ) ) ,
" number " : ( light : Color ( red : 28 / 255 , green : 0 / 255 , blue : 207 / 255 ) , dark : Color ( red : 28 / 255 , green : 0 / 255 , blue : 207 / 255 ) ) ,
" comment " : ( light : Color ( red : 93 / 255 , green : 108 / 255 , blue : 121 / 255 ) , dark : Color ( red : 150 / 255 , green : 160 / 255 , blue : 170 / 255 ) ) ,
" attribute " : ( light : Color ( red : 57 / 255 , green : 0 / 255 , blue : 255 / 255 ) , dark : Color ( red : 57 / 255 , green : 0 / 255 , blue : 255 / 255 ) ) ,
" variable " : ( light : Color ( red : 19 / 255 , green : 0 / 255 , blue : 255 / 255 ) , dark : Color ( red : 19 / 255 , green : 0 / 255 , blue : 255 / 255 ) ) ,
" def " : ( light : Color ( red : 29 / 255 , green : 196 / 255 , blue : 83 / 255 ) , dark : Color ( red : 29 / 255 , green : 196 / 255 , blue : 83 / 255 ) ) ,
" property " : ( light : Color ( red : 29 / 255 , green : 196 / 255 , blue : 83 / 255 ) , dark : Color ( red : 29 / 255 , green : 0 / 255 , blue : 160 / 255 ) ) ,
" meta " : ( light : Color ( red : 255 / 255 , green : 16 / 255 , blue : 0 / 255 ) , dark : Color ( red : 255 / 255 , green : 16 / 255 , blue : 0 / 255 ) ) ,
" tag " : ( light : Color ( red : 170 / 255 , green : 0 / 255 , blue : 160 / 255 ) , dark : Color ( red : 170 / 255 , green : 0 / 255 , blue : 160 / 255 ) ) ,
" atom " : ( light : Color ( red : 28 / 255 , green : 0 / 255 , blue : 207 / 255 ) , dark : Color ( red : 28 / 255 , green : 0 / 255 , blue : 207 / 255 ) ) ,
" builtin " : ( light : Color ( red : 255 / 255 , green : 130 / 255 , blue : 0 / 255 ) , dark : Color ( red : 255 / 255 , green : 130 / 255 , blue : 0 / 255 ) ) ,
" type " : ( light : Color ( red : 170 / 255 , green : 0 / 255 , blue : 160 / 255 ) , dark : Color ( red : 170 / 255 , green : 0 / 255 , blue : 160 / 255 ) )
]
2026-01-17 11:11:26 +00:00
2025-09-25 09:01:45 +00:00
return SyntaxColors (
keyword : colorScheme = = . dark ? baseColors [ " keyword " ] ! . dark : baseColors [ " keyword " ] ! . light ,
string : colorScheme = = . dark ? baseColors [ " string " ] ! . dark : baseColors [ " string " ] ! . light ,
number : colorScheme = = . dark ? baseColors [ " number " ] ! . dark : baseColors [ " number " ] ! . light ,
comment : colorScheme = = . dark ? baseColors [ " comment " ] ! . dark : baseColors [ " comment " ] ! . light ,
attribute : colorScheme = = . dark ? baseColors [ " attribute " ] ! . dark : baseColors [ " attribute " ] ! . light ,
variable : colorScheme = = . dark ? baseColors [ " variable " ] ! . dark : baseColors [ " variable " ] ! . light ,
def : colorScheme = = . dark ? baseColors [ " def " ] ! . dark : baseColors [ " def " ] ! . light ,
property : colorScheme = = . dark ? baseColors [ " property " ] ! . dark : baseColors [ " property " ] ! . light ,
meta : colorScheme = = . dark ? baseColors [ " meta " ] ! . dark : baseColors [ " meta " ] ! . light ,
tag : colorScheme = = . dark ? baseColors [ " tag " ] ! . dark : baseColors [ " tag " ] ! . light ,
atom : colorScheme = = . dark ? baseColors [ " atom " ] ! . dark : baseColors [ " atom " ] ! . light ,
builtin : colorScheme = = . dark ? baseColors [ " builtin " ] ! . dark : baseColors [ " builtin " ] ! . light ,
type : colorScheme = = . dark ? baseColors [ " type " ] ! . dark : baseColors [ " type " ] ! . light
)
2025-08-26 17:25:39 +00:00
}
2025-09-25 09:01:45 +00:00
}
2025-08-27 11:34:59 +00:00
2026-01-25 12:46:33 +00:00
// R e g e x p a t t e r n s p e r l a n g u a g e m a p p e d t o c o l o r s . K e e p l i g h t - w e i g h t f o r p e r f o r m a n c e .
2025-09-25 09:01:45 +00:00
func getSyntaxPatterns ( for language : String , colors : SyntaxColors ) -> [ String : Color ] {
switch language {
case " swift " :
return [
2026-01-17 11:11:26 +00:00
// K e y w o r d s ( e x t e n d e d t o i n c l u d e ` i m p o r t ` )
" \\ b(func|struct|class|enum|protocol|extension|if|else|for|while|switch|case|default|guard|defer|throw|try|catch|return|init|deinit|import) \\ b " : colors . keyword ,
// S t r i n g s a n d C h a r a c t e r s
2025-09-25 09:01:45 +00:00
" \" [^ \" ]* \" " : colors . string ,
2026-01-17 11:11:26 +00:00
" '[^' \\ ](?: \\ .[^' \\ ])*' " : colors . string ,
// N u m b e r s
2025-09-25 09:01:45 +00:00
" \\ b([0-9]+( \\ .[0-9]+)?) \\ b " : colors . number ,
2026-01-17 11:11:26 +00:00
// C o m m e n t s ( s i n g l e a n d m u l t i - l i n e )
2025-09-25 09:01:45 +00:00
" //.* " : colors . comment ,
" / \\ *([^*]|( \\ *+[^*/]))* \\ *+/ " : colors . comment ,
2026-01-17 11:11:26 +00:00
// D o c u m e n t a t i o n m a r k u p ( t r i p l e s l a s h a n d d o c b l o c k s )
" (?m)^(///).*$ " : colors . comment ,
" / \\ * \\ *([ \\ s \\ S]*?) \\ *+/ " : colors . comment ,
// D o c u m e n t a t i o n k e y w o r d s i n s i d e d o c s ( e . g . , - P a r a m e t e r : , - R e t u r n s : )
" (?m) \\ - \\ s*(Parameter|Parameters|Returns|Throws|Note|Warning|See \\ salso) \\ s*: " : colors . meta ,
// M a r k s / T O D O / F I X M E
" (?m)// \\ s*(MARK|TODO|FIXME) \\ s*:.*$ " : colors . meta ,
// U R L s
" https?://[A-Za-z0-9._~:/?#@!$&'()*+,;=%-]+ " : colors . atom ,
" file://[A-Za-z0-9._~:/?#@!$&'()*+,;=%-]+ " : colors . atom ,
// P r e p r o c e s s o r s t a t e m e n t s ( c o n d i t i o n a l s a n d d i r e c t i v e s )
" (?m)^#(if|elseif|else|endif|warning|error|available) \\ b.*$ " : colors . keyword ,
// A t t r i b u t e s l i k e @ a v a i l a b l e , @ M a i n A c t o r , e t c .
2025-09-25 09:01:45 +00:00
" @ \\ w+ " : colors . attribute ,
2026-01-17 11:11:26 +00:00
// V a r i a b l e d e c l a r a t i o n s
2025-09-25 09:01:45 +00:00
" \\ b(var|let) \\ b " : colors . variable ,
2026-01-17 11:11:26 +00:00
// C o m m o n S w i f t t y p e s
" \\ b(String|Int|Double|Bool) \\ b " : colors . type ,
// R e g e x l i t e r a l s a n d c o m p o n e n t s ( S w i f t / … / )
" /[^/ \\ n]*/ " : colors . builtin , // w h o l e r e g e x l i t e r a l
" \\ ( \\ ?<([A-Za-z_][A-Za-z0-9_]*)> " : colors . def , // n a m e d c a p t u r e s t a r t ( ? < n a m e >
" \\ [[^ \\ ]]* \\ ] " : colors . property , // c h a r a c t e r c l a s s e s
" [|*+?] " : colors . meta , // r e g e x o p e r a t o r s
// C o m m o n S w i f t U I p r o p e r t y n a m e s l i k e ` b o d y `
" \\ bbody \\ b " : colors . property ,
// P r o j e c t - s p e c i f i c i d e n t i f i e r y o u m e n t i o n e d : ` v i e w M o d e l `
" \\ bviewModel \\ b " : colors . property
2025-09-25 09:01:45 +00:00
]
case " python " :
return [
" \\ b(def|class|if|else|for|while|try|except|with|as|import|from) \\ b " : colors . keyword ,
" \\ b(int|str|float|bool|list|dict) \\ b " : colors . type ,
" \" [^ \" ]* \" |'[^']*' " : colors . string ,
" \\ b([0-9]+( \\ .[0-9]+)?) \\ b " : colors . number ,
" #.* " : colors . comment
]
case " javascript " :
return [
" \\ b(function|var|let|const|if|else|for|while|do|try|catch) \\ b " : colors . keyword ,
" \\ b(Number|String|Boolean|Object|Array) \\ b " : colors . type ,
" \" [^ \" ]* \" |'[^']*'| \\ `[^ \\ `]* \\ ` " : colors . string ,
" \\ b([0-9]+( \\ .[0-9]+)?) \\ b " : colors . number ,
" //.*|/ \\ *([^*]|( \\ *+[^*/]))* \\ *+/ " : colors . comment
]
case " html " :
return [ " <[^>]+> " : colors . tag ]
case " css " :
return [ " \\ b([a-zA-Z-]+ \\ s*: \\ s*[^;]+;) " : colors . property ]
case " c " , " cpp " :
return [
" \\ b(int|float|double|char|void|if|else|for|while|do|switch|case|return) \\ b " : colors . keyword ,
" \\ b(int|float|double|char) \\ b " : colors . type ,
" \" [^ \" ]* \" " : colors . string ,
" \\ b([0-9]+( \\ .[0-9]+)?) \\ b " : colors . number ,
" //.*|/ \\ *([^*]|( \\ *+[^*/]))* \\ *+/ " : colors . comment
]
case " json " :
return [
" \" [^ \" ]+ \" \\ s*: " : colors . property ,
" \" [^ \" ]* \" " : colors . string ,
" \\ b([0-9]+( \\ .[0-9]+)?) \\ b " : colors . number ,
" \\ b(true|false|null) \\ b " : colors . keyword
]
case " markdown " :
return [
" ^#+ \\ s*[^#]+ " : colors . keyword ,
" \\ * \\ *[^ \\ * \\ *]+ \\ * \\ * " : colors . def ,
" \\ _[^ \\ _]+ \\ _ " : colors . def
]
2026-01-23 11:49:52 +00:00
case " bash " :
return [
" \\ b(if|then|else|elif|fi|for|while|do|done|case|esac|function|in) \\ b " : colors . keyword ,
" \\ $[A-Za-z_][A-Za-z0-9_]*| \\ ${[^}]+} " : colors . variable ,
" \\ b[0-9]+ \\ b " : colors . number ,
" \\ \" [^ \\ \" ]* \\ \" |'[^']*' " : colors . string ,
" #.* " : colors . comment
]
case " zsh " :
return [
" \\ b(if|then|else|elif|fi|for|while|do|done|case|esac|function|in|autoload|typeset|setopt|unsetopt) \\ b " : colors . keyword ,
" \\ $[A-Za-z_][A-Za-z0-9_]*| \\ ${[^}]+} " : colors . variable ,
" \\ b[0-9]+ \\ b " : colors . number ,
" \\ \" [^ \\ \" ]* \\ \" |'[^']*' " : colors . string ,
" #.* " : colors . comment
]
2025-09-25 09:01:45 +00:00
default :
return [ : ]
2025-08-26 17:25:39 +00:00
}
2025-09-25 09:01:45 +00:00
}
2025-08-26 17:25:39 +00:00
2026-01-25 12:46:33 +00:00
// S i m p l e s h e e t t o e d i t a n d p e r s i s t A P I t o k e n s f o r e x t e r n a l A I p r o v i d e r s .
struct APISupportSettingsView : View {
@ Binding var grokAPIToken : String
@ Binding var openAIAPIToken : String
@ Binding var geminiAPIToken : String
var body : some View {
VStack ( alignment : . leading , spacing : 16 ) {
Text ( " AI Provider API Keys " ) . font ( . headline )
Group {
LabeledContent ( " Grok " ) {
SecureField ( " sk-… " , text : $ grokAPIToken )
. textFieldStyle ( . roundedBorder )
. onChange ( of : grokAPIToken ) { _ , new in
UserDefaults . standard . set ( new , forKey : " GrokAPIToken " )
}
}
LabeledContent ( " OpenAI " ) {
SecureField ( " sk-… " , text : $ openAIAPIToken )
. textFieldStyle ( . roundedBorder )
. onChange ( of : openAIAPIToken ) { _ , new in
UserDefaults . standard . set ( new , forKey : " OpenAIAPIToken " )
}
}
LabeledContent ( " Gemini " ) {
SecureField ( " AIza… " , text : $ geminiAPIToken )
. textFieldStyle ( . roundedBorder )
. onChange ( of : geminiAPIToken ) { _ , new in
UserDefaults . standard . set ( new , forKey : " GeminiAPIToken " )
}
}
}
. labelStyle ( . titleAndIcon )
HStack {
Spacer ( )
Button ( " Close " ) {
NSApp . keyWindow ? . endSheet ( NSApp . keyWindow ! )
}
}
}
. padding ( 20 )
}
}
2025-09-25 09:01:45 +00:00
extension Notification . Name {
static let moveCursorToLine = Notification . Name ( " moveCursorToLine " )
2026-01-17 11:11:26 +00:00
static let streamSuggestion = Notification . Name ( " streamSuggestion " )
static let caretPositionDidChange = Notification . Name ( " caretPositionDidChange " )
2026-01-17 12:04:11 +00:00
static let pastedText = Notification . Name ( " pastedText " )
2026-01-20 23:46:04 +00:00
static let triggerSuggestion = Notification . Name ( " triggerSuggestion " )
2025-08-27 11:34:59 +00:00
}
2026-01-17 11:11:26 +00:00
2026-01-25 12:46:33 +00:00
// MARK: - H o v e r - t r i g g e r e d p o p o v e r h e l p e r
// S h o w s a s m a l l t r a n s i e n t p o p o v e r o n h o v e r w i t h a c o n f i g u r a b l e d e l a y . C o m p l e m e n t s . h e l p t o o l t i p s .
private struct HoverPopoverModifier < PopoverContent : View > : ViewModifier {
let delay : TimeInterval
let content : ( ) -> PopoverContent
@ State private var isHovering = false
@ State private var isPresented = false
func body ( content base : Content ) -> some View {
base
. onHover { hovering in
isHovering = hovering
if hovering {
// S h o w a f t e r a s h o r t d e l a y t o a v o i d f l i c k e r
DispatchQueue . main . asyncAfter ( deadline : . now ( ) + delay ) {
if isHovering {
isPresented = true
}
}
} else {
isPresented = false
}
}
. popover ( isPresented : $ isPresented , arrowEdge : . bottom ) {
self . content ( )
. padding ( 8 )
}
}
}
private extension View {
func hoverPopover < Content : View > ( delay : TimeInterval = 0.5 , @ ViewBuilder _ content : @ escaping ( ) -> Content ) -> some View {
modifier ( HoverPopoverModifier ( delay : delay , content : content ) )
}
}