Polish settings UI and stabilize iOS/iPad/macOS behavior

This commit is contained in:
h3p 2026-02-19 09:09:35 +01:00
parent 3715bbb39e
commit 3c1e811e5f
11 changed files with 899 additions and 1074 deletions

View file

@ -358,7 +358,7 @@
CODE_SIGNING_ALLOWED = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 270;
CURRENT_PROJECT_VERSION = 266;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = CS727NF72U;
ENABLE_APP_SANDBOX = YES;
@ -402,7 +402,7 @@
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 0.4.26;
MARKETING_VERSION = 0.4.25;
PRODUCT_BUNDLE_IDENTIFIER = "h3p.Neon-Vision-Editor";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -439,7 +439,7 @@
CODE_SIGNING_ALLOWED = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 270;
CURRENT_PROJECT_VERSION = 266;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = CS727NF72U;
ENABLE_APP_SANDBOX = YES;
@ -483,7 +483,7 @@
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 0.4.26;
MARKETING_VERSION = 0.4.25;
PRODUCT_BUNDLE_IDENTIFIER = "h3p.Neon-Vision-Editor";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";

View file

@ -265,6 +265,7 @@ struct NeonVisionEditorApp: App {
.onChange(of: openInTabs) { _, _ in applyOpenInTabsPreference() }
.environment(\.showGrokError, $showGrokError)
.environment(\.grokErrorMessage, $grokErrorMessage)
.tint(.blue)
.preferredColorScheme(preferredAppearance)
.frame(minWidth: 600, minHeight: 400)
.task {
@ -301,6 +302,7 @@ struct NeonVisionEditorApp: App {
.onAppear { applyOpenInTabsPreference() }
.onChange(of: appearance) { _, _ in applyGlobalAppearanceOverride() }
.onChange(of: openInTabs) { _, _ in applyOpenInTabsPreference() }
.tint(.blue)
.preferredColorScheme(preferredAppearance)
}
.defaultSize(width: 1000, height: 600)
@ -314,6 +316,7 @@ struct NeonVisionEditorApp: App {
.onAppear { applyOpenInTabsPreference() }
.onChange(of: appearance) { _, _ in applyGlobalAppearanceOverride() }
.onChange(of: openInTabs) { _, _ in applyOpenInTabsPreference() }
.tint(.blue)
.preferredColorScheme(preferredAppearance)
}

View file

@ -350,19 +350,15 @@ final class AppUpdateManager: ObservableObject {
}
func installUpdateNow() async {
guard ReleaseRuntimePolicy.isUpdaterEnabledForCurrentDistribution else {
installMessage = "Updater is disabled for this distribution channel."
return
}
guard !Self.isDevelopmentRuntime else {
installMessage = "Install now is disabled while running from Xcode/DerivedData. Use a direct-distribution app build."
if let reason = installNowDisabledReason {
installMessage = reason
return
}
await attemptAutoInstall(interactive: true)
}
var installNowSupported: Bool {
ReleaseRuntimePolicy.isUpdaterEnabledForCurrentDistribution && !Self.isDevelopmentRuntime
installNowDisabledReason == nil
}
var installNowDisabledReason: String? {
@ -372,6 +368,20 @@ final class AppUpdateManager: ObservableObject {
guard !Self.isDevelopmentRuntime else {
return "Install is unavailable in Xcode/DerivedData runs."
}
#if os(macOS)
guard let release = latestRelease else {
return "No update metadata loaded yet."
}
guard release.downloadURL != nil, release.assetName != nil else {
return "This release does not provide a supported ZIP asset for automatic install."
}
let destinationDir = Bundle.main.bundleURL
.standardizedFileURL
.deletingLastPathComponent()
guard FileManager.default.isWritableFile(atPath: destinationDir.path) else {
return "No permission to write to \(destinationDir.path). Move the app to a writable location."
}
#endif
return nil
}

View file

@ -6,7 +6,7 @@
"components" : {
"alpha" : "1.000",
"blue" : "1.000",
"green" : "0.486",
"green" : "0.478",
"red" : "0.000"
}
},
@ -25,7 +25,7 @@
"alpha" : "1.000",
"blue" : "1.000",
"green" : "0.620",
"red" : "0.220"
"red" : "0.000"
}
},
"idiom" : "universal"

View file

@ -229,10 +229,10 @@ extension ContentView {
Button(action: {
toggleAutoCompletion()
}) {
Image(systemName: "text.badge.plus")
Image(systemName: "bolt.horizontal.circle")
.symbolVariant(isAutoCompletionEnabled ? .fill : .none)
}
.help("Code Completion")
.help(isAutoCompletionEnabled ? "Disable Code Completion" : "Enable Code Completion")
.accessibilityLabel("Code Completion")
}
@ -634,10 +634,10 @@ extension ContentView {
Button(action: {
toggleAutoCompletion()
}) {
Image(systemName: "text.badge.plus")
Image(systemName: "bolt.horizontal.circle")
.symbolVariant(isAutoCompletionEnabled ? .fill : .none)
}
.help("Code Completion")
.help(isAutoCompletionEnabled ? "Disable Code Completion" : "Enable Code Completion")
.accessibilityLabel("Code Completion")
Button(action: {

View file

@ -32,12 +32,6 @@ extension String {
//Manages the editor area, toolbar, popovers, and bridges to the view model for file I/O and metrics.
struct ContentView: View {
private static let completionSignposter = OSSignposter(subsystem: "h3p.Neon-Vision-Editor", category: "InlineCompletion")
private enum PerformanceTier: Int {
case normal = 0
case light = 1
case strong = 2
case maximum = 3
}
private struct CompletionCacheEntry {
let suggestion: String
@ -49,11 +43,8 @@ struct ContentView: View {
@EnvironmentObject private var supportPurchaseManager: SupportPurchaseManager
@EnvironmentObject var appUpdateManager: AppUpdateManager
@Environment(\.colorScheme) var colorScheme
@Environment(\.accessibilityReduceTransparency) private var reduceTransparency
#if os(iOS)
@Environment(\.horizontalSizeClass) var horizontalSizeClass
@SceneStorage("SceneSidebarVisible") private var sceneSidebarVisible: Bool = false
@SceneStorage("SceneProjectSidebarVisible") private var sceneProjectSidebarVisible: Bool = false
#endif
#if os(macOS)
@Environment(\.openWindow) var openWindow
@ -76,11 +67,6 @@ struct ContentView: View {
@AppStorage("SettingsShowScopeGuides") var showScopeGuides: Bool = false
@AppStorage("SettingsHighlightScopeBackground") var highlightScopeBackground: Bool = false
@AppStorage("SettingsLineWrapEnabled") var settingsLineWrapEnabled: Bool = false
@AppStorage("SettingsLiquidGlassEnabled") var liquidGlassEnabled: Bool = true
@AppStorage("SettingsForceLargeFileMode") var forceLargeFileMode: Bool = false
@AppStorage("SettingsShowBottomActionBarIOS") var showBottomActionBarIOS: Bool = false
@AppStorage("SettingsShowKeyboardAccessoryBarIOS") var showKeyboardAccessoryBarIOS: Bool = false
@AppStorage("SettingsEnableDiagnostics") var diagnosticsEnabled: Bool = false
// Removed showHorizontalRuler and showVerticalRuler AppStorage properties
@AppStorage("SettingsIndentStyle") var indentStyle: String = "spaces"
@AppStorage("SettingsIndentWidth") var indentWidth: Int = 4
@ -106,16 +92,14 @@ struct ContentView: View {
@State var geminiAPIToken: String = ""
@State var anthropicAPIToken: String = ""
// Debounce handle for inline completion
@State var lastCompletionWorkItem: DispatchWorkItem?
// Debounce/cancellation handles for inline completion
@State private var completionDebounceTask: Task<Void, Never>?
@State private var completionTask: Task<Void, Never>?
@State private var inFlightCompletionKey: String?
@State private var lastCompletionTriggerSignature: String = ""
@State private var isApplyingCompletion: Bool = false
@State private var completionCache: [String: CompletionCacheEntry] = [:]
@State private var pendingHighlightRefresh: DispatchWorkItem?
@AppStorage("EnableTranslucentWindow") var enableTranslucentWindow: Bool = false
@AppStorage("SettingsTranslucencyStrength") private var translucencyStrength: String = "medium"
@State private var isLowPowerModeEnabled: Bool = ProcessInfo.processInfo.isLowPowerModeEnabled
@State var showFindReplace: Bool = false
@State var showSettingsSheet: Bool = false
@ -150,17 +134,11 @@ struct ContentView: View {
@State var droppedFileLoadProgress: Double = 0
@State var droppedFileLoadLabel: String = ""
@State var largeFileModeEnabled: Bool = false
@State private var performanceTier: PerformanceTier = .normal
@State private var lastHighlightRefreshAt: Date = .distantPast
@State var projectTreeRefreshTask: Task<Void, Never>? = nil
@State var projectTreeRefreshGeneration: Int = 0
@AppStorage("HasSeenWelcomeTourV1") var hasSeenWelcomeTourV1: Bool = false
@AppStorage("WelcomeTourSeenRelease") var welcomeTourSeenRelease: String = ""
@State var showWelcomeTour: Bool = false
#if os(macOS)
@State private var hostWindowNumber: Int? = nil
#else
@State private var iosToolbarCompactness: CGFloat = 0
#endif
@State private var showLanguageSetupPrompt: Bool = false
@State private var languagePromptSelection: String = "plain"
@ -176,134 +154,6 @@ struct ContentView: View {
var activeProviderName: String { lastProviderUsed }
var shouldUseLiquidGlass: Bool {
#if os(iOS)
liquidGlassEnabled && enableTranslucentWindow && !reduceTransparency && !isLowPowerModeEnabled
#else
liquidGlassEnabled && enableTranslucentWindow && !reduceTransparency
#endif
}
var primaryGlassMaterial: Material {
colorScheme == .dark ? .thinMaterial : .ultraThinMaterial
}
var chromeFallbackColor: Color {
#if os(iOS)
Color(.secondarySystemBackground).opacity(0.92)
#else
Color.clear
#endif
}
var toolbarFallbackColor: Color {
#if os(iOS)
let isRegularWidth = horizontalSizeClass == .regular
if isRegularWidth && colorScheme == .light {
// Slightly cooler fallback for better contrast on bright iPad backgrounds.
return Color(red: 0.84, green: 0.90, blue: 0.98).opacity(0.96)
}
return chromeFallbackColor
#else
return chromeFallbackColor
#endif
}
var toolbarDensityScale: CGFloat {
#if os(iOS)
1.0 - (iosToolbarCompactness * 0.06)
#else
1.0
#endif
}
var toolbarDensityOpacity: Double {
#if os(iOS)
1.0 - Double(iosToolbarCompactness * 0.16)
#else
1.0
#endif
}
#if os(macOS)
private enum MacTranslucencySurface {
case editor
case canvas
case toolbar
case tabStrip
}
private var normalizedTranslucencyStrength: String {
switch translucencyStrength.lowercased() {
case "light", "strong":
return translucencyStrength.lowercased()
default:
return "medium"
}
}
private func macTranslucencyOpacity(for surface: MacTranslucencySurface, colorScheme: ColorScheme) -> Double {
let isDark = colorScheme == .dark
switch normalizedTranslucencyStrength {
case "light":
switch surface {
case .editor: return isDark ? 0.84 : 0.90
case .canvas: return isDark ? 0.80 : 0.88
case .toolbar: return isDark ? 0.64 : 0.80
case .tabStrip: return isDark ? 0.76 : 0.86
}
case "strong":
switch surface {
case .editor: return isDark ? 0.70 : 0.80
case .canvas: return isDark ? 0.64 : 0.76
case .toolbar: return isDark ? 0.46 : 0.66
case .tabStrip: return isDark ? 0.58 : 0.72
}
default:
switch surface {
case .editor: return isDark ? 0.78 : 0.86
case .canvas: return isDark ? 0.74 : 0.84
case .toolbar: return isDark ? 0.56 : 0.74
case .tabStrip: return isDark ? 0.68 : 0.80
}
}
}
#endif
private enum SecondaryGlassLayer {
case none
case bottomBar
case statusBar
case brainDumpCanvas
case progressOverlay
}
private var secondaryGlassLayer: SecondaryGlassLayer {
guard shouldUseLiquidGlass else { return .none }
if viewModel.isBrainDumpMode { return .brainDumpCanvas }
#if os(iOS)
if showBottomActionBarIOS { return .bottomBar }
#endif
if droppedFileLoadInProgress { return .progressOverlay }
return .statusBar
}
var shouldUseStatusGlassLayer: Bool {
secondaryGlassLayer == .statusBar
}
var shouldUseBottomBarGlassLayer: Bool {
secondaryGlassLayer == .bottomBar
}
var shouldUseProgressOverlayGlassLayer: Bool {
secondaryGlassLayer == .progressOverlay
}
var shouldUseBrainDumpGlassLayer: Bool {
secondaryGlassLayer == .brainDumpCanvas
}
var selectedModel: AIModel {
get { AIModel(rawValue: selectedModelRaw) ?? .appleIntelligence }
set { selectedModelRaw = newValue.rawValue }
@ -409,7 +259,6 @@ struct ContentView: View {
@MainActor
private func performInlineCompletion(for textView: NSTextView) {
completionTask?.cancel()
inFlightCompletionKey = nil
completionTask = Task(priority: .utility) {
await performInlineCompletionAsync(for: textView)
}
@ -483,16 +332,6 @@ struct ContentView: View {
return
}
if inFlightCompletionKey == cacheKey {
return
}
inFlightCompletionKey = cacheKey
defer {
if inFlightCompletionKey == cacheKey {
inFlightCompletionKey = nil
}
}
let modelInterval = Self.completionSignposter.beginInterval("model_completion")
let suggestion = await generateModelCompletion(prefix: contextPrefix, language: currentLanguage)
Self.completionSignposter.endInterval("model_completion", modelInterval)
@ -570,7 +409,7 @@ struct ContentView: View {
}
private func shouldThrottleHeavyEditorFeatures(in nsText: NSString? = nil) -> Bool {
if performanceTier.rawValue >= PerformanceTier.light.rawValue { return true }
if largeFileModeEnabled { return true }
let length = nsText?.length ?? (currentContentBinding.wrappedValue as NSString).length
return length >= 120_000
}
@ -581,8 +420,7 @@ struct ContentView: View {
guard selection.length == 0 else { return false }
let location = selection.location
guard location > 0, location <= nsText.length else { return false }
if performanceTier.rawValue >= PerformanceTier.strong.rawValue { return false }
if shouldThrottleHeavyEditorFeatures(in: nsText) && nsText.length >= 180_000 { return false }
if shouldThrottleHeavyEditorFeatures(in: nsText) { return false }
let prevChar = nsText.substring(with: NSRange(location: location - 1, length: 1))
let triggerChars: Set<String> = [".", "(", ")", "{", "}", "[", "]", ":", ",", "\n", "\t", " "]
@ -590,9 +428,6 @@ struct ContentView: View {
let wordChars = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "_"))
if prevChar.rangeOfCharacter(from: wordChars) == nil { return false }
if let prefixLength = identifierLengthBeforeCaret(in: nsText, caretLocation: location), prefixLength < 3 {
return false
}
if location >= nsText.length { return true }
let nextChar = nsText.substring(with: NSRange(location: location, length: 1))
@ -602,24 +437,27 @@ struct ContentView: View {
private func completionDebounceInterval(for textView: NSTextView) -> TimeInterval {
let docLength = (textView.string as NSString).length
if performanceTier == .maximum { return 1.3 }
if performanceTier == .strong || docLength >= 120_000 { return 1.05 }
if performanceTier == .light || docLength >= 50_000 { return 0.8 }
return 0.55
if docLength >= 80_000 { return 0.9 }
if docLength >= 25_000 { return 0.7 }
return 0.45
}
private func identifierLengthBeforeCaret(in text: NSString, caretLocation: Int) -> Int? {
guard caretLocation > 0, caretLocation <= text.length else { return nil }
let wordChars = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "_"))
var index = caretLocation - 1
var length = 0
while index >= 0 {
let current = text.substring(with: NSRange(location: index, length: 1))
if current.rangeOfCharacter(from: wordChars) == nil { break }
length += 1
index -= 1
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 = ""
}
return length > 0 ? length : nil
// Keep signature cheap while specific enough to skip duplicate notifications.
return "\(location)|\(prevChar)|\(nextChar)|\(nsText.length)"
}
#endif
@ -1215,26 +1053,10 @@ struct ContentView: View {
}
}
func updateLargeFileMode(for text: String) {
let bytes = text.utf8.count
let computedTier: PerformanceTier
if forceLargeFileMode {
computedTier = .maximum
} else if bytes >= 2_000_000 {
computedTier = .maximum
} else if bytes >= 1_000_000 {
computedTier = .strong
} else if bytes >= 250_000 {
computedTier = .light
} else {
computedTier = .normal
}
if performanceTier != computedTier {
performanceTier = computedTier
}
let effectiveLargeMode = computedTier.rawValue >= PerformanceTier.strong.rawValue
if largeFileModeEnabled != effectiveLargeMode {
largeFileModeEnabled = effectiveLargeMode
private func updateLargeFileMode(for text: String) {
let isLarge = text.utf8.count >= 2_000_000
if largeFileModeEnabled != isLarge {
largeFileModeEnabled = isLarge
scheduleHighlightRefresh()
}
}
@ -1353,16 +1175,22 @@ struct ContentView: View {
return
}
guard shouldScheduleCompletion(for: changedTextView) else { return }
lastCompletionWorkItem?.cancel()
let signature = completionTriggerSignature(for: changedTextView)
guard !signature.isEmpty else { return }
if signature == lastCompletionTriggerSignature {
return
}
lastCompletionTriggerSignature = signature
completionDebounceTask?.cancel()
completionTask?.cancel()
let debounce = completionDebounceInterval(for: changedTextView)
let work = DispatchWorkItem {
Task { @MainActor in
performInlineCompletion(for: changedTextView)
}
completionDebounceTask = Task { @MainActor [weak changedTextView] in
let delay = UInt64((debounce * 1_000_000_000).rounded())
try? await Task.sleep(nanoseconds: delay)
guard !Task.isCancelled, let changedTextView else { return }
lastCompletionTriggerSignature = ""
performInlineCompletion(for: changedTextView)
}
lastCompletionWorkItem = work
DispatchQueue.main.asyncAfter(deadline: .now() + debounce, execute: work)
}
#else
view
@ -1380,7 +1208,7 @@ struct ContentView: View {
editorView
}
.navigationSplitViewColumnWidth(min: 200, ideal: 250, max: 600)
.background(shouldUseLiquidGlass ? AnyShapeStyle(primaryGlassMaterial) : AnyShapeStyle(Color.clear))
.background(enableTranslucentWindow ? AnyShapeStyle(.ultraThinMaterial) : AnyShapeStyle(Color.clear))
} else {
editorView
}
@ -1396,7 +1224,7 @@ struct ContentView: View {
editorView
}
.navigationSplitViewColumnWidth(min: 200, ideal: 250, max: 600)
.background(shouldUseLiquidGlass ? AnyShapeStyle(primaryGlassMaterial) : AnyShapeStyle(Color.clear))
.background(enableTranslucentWindow ? AnyShapeStyle(.ultraThinMaterial) : AnyShapeStyle(Color.clear))
} else {
editorView
}
@ -1406,182 +1234,131 @@ struct ContentView: View {
#endif
}
private var primaryContent: some View {
AnyView(platformLayout)
.alert("AI Error", isPresented: showGrokError) {
Button("OK") { }
} message: {
Text(grokErrorMessage.wrappedValue)
}
.alert(
"Whitespace Scalars",
isPresented: Binding(
get: { whitespaceInspectorMessage != nil },
set: { if !$0 { whitespaceInspectorMessage = nil } }
)
) {
Button("OK", role: .cancel) { }
} message: {
Text(whitespaceInspectorMessage ?? "")
}
.navigationTitle("Neon Vision Editor")
.onAppear {
if UserDefaults.standard.object(forKey: "SettingsAutoIndent") == nil {
autoIndentEnabled = true
}
// Always start with completion disabled on app launch/open.
isAutoCompletionEnabled = false
UserDefaults.standard.set(false, forKey: "SettingsCompletionEnabled")
// Keep whitespace marker rendering disabled by default and after migrations.
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
}
}
.onReceive(NotificationCenter.default.publisher(for: .whitespaceScalarInspectionResult)) { notif in
guard matchesCurrentWindow(notif) else { return }
if let msg = notif.userInfo?[EditorCommandUserInfo.inspectionMessage] as? String {
whitespaceInspectorMessage = msg
}
}
.onChange(of: viewModel.isLineWrapEnabled) { _, enabled in
if settingsLineWrapEnabled != enabled {
settingsLineWrapEnabled = enabled
}
}
.onChange(of: forceLargeFileMode) { _, _ in
updateLargeFileMode(for: currentContentBinding.wrappedValue)
}
#if os(iOS)
.onChange(of: viewModel.showSidebar) { _, enabled in
sceneSidebarVisible = enabled
}
.onChange(of: showProjectStructureSidebar) { _, enabled in
sceneProjectSidebarVisible = enabled
}
#endif
.onChange(of: appUpdateManager.automaticPromptToken) { _, _ in
if appUpdateManager.consumeAutomaticPromptIfNeeded() {
showUpdaterDialog(checkNow: false)
}
}
.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()
}
#if os(iOS)
.onReceive(NotificationCenter.default.publisher(for: Notification.Name.NSProcessInfoPowerStateDidChange)) { _ in
isLowPowerModeEnabled = ProcessInfo.processInfo.isLowPowerModeEnabled
}
.onReceive(NotificationCenter.default.publisher(for: .editorScrollCompactnessDidChange)) { notif in
guard let value = notif.userInfo?["compactness"] as? CGFloat else { return }
let clamped = min(1, max(0, value))
if abs(clamped - iosToolbarCompactness) >= 0.01 {
iosToolbarCompactness = clamped
}
}
#endif
}
private var lifecycleContent: some View {
primaryContent
.modifier(ModalPresentationModifier(contentView: self))
.onAppear {
#if os(iOS)
viewModel.showSidebar = sceneSidebarVisible
showProjectStructureSidebar = sceneProjectSidebarVisible
#else
// Start with sidebar collapsed by default
viewModel.showSidebar = false
showProjectStructureSidebar = false
#endif
DispatchQueue.main.async {
applyStartupBehaviorIfNeeded()
}
// Restore Brain Dump mode from defaults
if UserDefaults.standard.object(forKey: "BrainDumpModeEnabled") != nil {
viewModel.isBrainDumpMode = UserDefaults.standard.bool(forKey: "BrainDumpModeEnabled")
}
applyWindowTranslucency(enableTranslucentWindow)
if !hasSeenWelcomeTourV1 || welcomeTourSeenRelease != WelcomeTourView.releaseID {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
showWelcomeTour = true
}
}
}
.onDisappear {
projectTreeRefreshTask?.cancel()
}
}
// Layout: NavigationSplitView with optional sidebar and the primary code editor.
var body: some View {
lifecycleContent
#if os(macOS)
.background(
WindowAccessor { window in
updateWindowRegistration(window)
}
.frame(width: 0, height: 0)
AnyView(platformLayout)
.alert("AI Error", isPresented: showGrokError) {
Button("OK") { }
} message: {
Text(grokErrorMessage.wrappedValue)
}
.alert(
"Whitespace Scalars",
isPresented: Binding(
get: { whitespaceInspectorMessage != nil },
set: { if !$0 { whitespaceInspectorMessage = nil } }
)
.onDisappear {
lastCompletionWorkItem?.cancel()
completionTask?.cancel()
inFlightCompletionKey = nil
pendingHighlightRefresh?.cancel()
completionCache.removeAll(keepingCapacity: false)
if let number = hostWindowNumber {
WindowViewModelRegistry.shared.unregister(windowNumber: number)
) {
Button("OK", role: .cancel) { }
} message: {
Text(whitespaceInspectorMessage ?? "")
}
.navigationTitle("Neon Vision Editor")
.onAppear {
if UserDefaults.standard.object(forKey: "SettingsAutoIndent") == nil {
autoIndentEnabled = true
}
// Always start with completion disabled on app launch/open.
isAutoCompletionEnabled = false
UserDefaults.standard.set(false, forKey: "SettingsCompletionEnabled")
// Keep whitespace marker rendering disabled by default and after migrations.
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
}
}
.onReceive(NotificationCenter.default.publisher(for: .whitespaceScalarInspectionResult)) { notif in
guard matchesCurrentWindow(notif) else { return }
if let msg = notif.userInfo?[EditorCommandUserInfo.inspectionMessage] as? String {
whitespaceInspectorMessage = msg
}
}
.onChange(of: viewModel.isLineWrapEnabled) { _, enabled in
if settingsLineWrapEnabled != enabled {
settingsLineWrapEnabled = enabled
}
}
.onChange(of: appUpdateManager.automaticPromptToken) { _, _ in
if appUpdateManager.consumeAutomaticPromptIfNeeded() {
showUpdaterDialog(checkNow: false)
}
}
.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 {
// Start with sidebar collapsed by default
viewModel.showSidebar = false
showProjectStructureSidebar = false
applyStartupBehaviorIfNeeded()
// Restore Brain Dump mode from defaults
if UserDefaults.standard.object(forKey: "BrainDumpModeEnabled") != nil {
viewModel.isBrainDumpMode = UserDefaults.standard.bool(forKey: "BrainDumpModeEnabled")
}
applyWindowTranslucency(enableTranslucentWindow)
if !hasSeenWelcomeTourV1 || welcomeTourSeenRelease != WelcomeTourView.releaseID {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
showWelcomeTour = true
}
}
}
#if os(macOS)
.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)
}
}
#endif
}
private func scheduleHighlightRefresh(delay: TimeInterval = 0.05) {
pendingHighlightRefresh?.cancel()
let adjustedDelay: TimeInterval = {
if performanceTier == .maximum { return max(delay, 0.2) }
if performanceTier == .strong { return max(delay, 0.14) }
return delay
}()
let work = DispatchWorkItem {
let now = Date()
if now.timeIntervalSince(self.lastHighlightRefreshAt) < 0.05 {
return
}
self.lastHighlightRefreshAt = now
highlightRefreshToken &+= 1
}
pendingHighlightRefresh = work
DispatchQueue.main.asyncAfter(deadline: .now() + adjustedDelay, execute: work)
DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: work)
}
#if !os(macOS)
private func shouldThrottleHeavyEditorFeatures(in nsText: NSString? = nil) -> Bool {
if performanceTier.rawValue >= PerformanceTier.light.rawValue { return true }
if largeFileModeEnabled { return true }
let length = nsText?.length ?? (currentContentBinding.wrappedValue as NSString).length
return length >= 120_000
}
@ -1621,6 +1398,7 @@ struct ContentView: View {
supportsTranslucency: false
)
.environmentObject(contentView.supportPurchaseManager)
.tint(.blue)
#if os(iOS)
.presentationDetents([.large])
.presentationDragIndicator(.visible)
@ -1859,17 +1637,6 @@ struct ContentView: View {
#endif
}
func recordDiagnostic(_ message: String) {
guard diagnosticsEnabled else { return }
let timestamp = ISO8601DateFormatter().string(from: Date())
var logs = UserDefaults.standard.stringArray(forKey: "SettingsDiagnosticsLog") ?? []
logs.append("[\(timestamp)] \(message)")
if logs.count > 250 {
logs.removeFirst(logs.count - 250)
}
UserDefaults.standard.set(logs, forKey: "SettingsDiagnosticsLog")
}
private func applyLanguageSelection(language: String, insertTemplate: Bool) {
let contentIsEmpty = currentContent.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
if let tab = viewModel.selectedTab {
@ -2258,7 +2025,6 @@ struct ContentView: View {
indentWidth: indentWidth,
autoIndentEnabled: autoIndentEnabled,
autoCloseBracketsEnabled: autoCloseBracketsEnabled,
showKeyboardAccessoryBar: showKeyboardAccessoryBarIOS && showBottomActionBarIOS,
highlightRefreshToken: highlightRefreshToken
)
.id(currentLanguage)
@ -2268,31 +2034,15 @@ struct ContentView: View {
.padding(.vertical, viewModel.isBrainDumpMode ? 40 : 0)
.background(
Group {
#if os(macOS)
if enableTranslucentWindow {
Color(nsColor: .windowBackgroundColor)
.opacity(macTranslucencyOpacity(for: .editor, colorScheme: colorScheme))
} else if shouldUseLiquidGlass {
Color.clear.background(primaryGlassMaterial)
Color.clear.background(.ultraThinMaterial)
} else {
Color.clear
}
#else
if shouldUseLiquidGlass {
Color.clear.background(primaryGlassMaterial)
} else {
Color.clear
}
#endif
}
)
if !viewModel.isBrainDumpMode {
#if os(iOS)
if showBottomActionBarIOS {
iosBottomActionBar
}
#endif
wordCountView
}
}
@ -2306,36 +2056,24 @@ struct ContentView: View {
Divider()
ProjectStructureSidebarView(
rootFolderURL: projectRootFolderURL,
nodes: $projectTreeNodes,
nodes: projectTreeNodes,
selectedFileURL: viewModel.selectedTab?.fileURL,
translucentBackgroundEnabled: enableTranslucentWindow && !shouldUseLiquidGlass,
translucentBackgroundEnabled: enableTranslucentWindow,
onOpenFile: { openFileFromToolbar() },
onOpenFolder: { openProjectFolder() },
onOpenProjectFile: { openProjectFile(url: $0) },
onRefreshTree: { refreshProjectTree() },
onLoadChildren: { loadProjectTreeChildren(for: $0) }
onRefreshTree: { refreshProjectTree() }
)
.frame(minWidth: 220, idealWidth: 260, maxWidth: 340)
}
}
.background(
Group {
#if os(macOS)
if enableTranslucentWindow {
Color(nsColor: .windowBackgroundColor)
.opacity(macTranslucencyOpacity(for: .canvas, colorScheme: colorScheme))
} else if shouldUseBrainDumpGlassLayer {
Color.clear.background(primaryGlassMaterial)
if viewModel.isBrainDumpMode && enableTranslucentWindow {
Color.clear.background(.ultraThinMaterial)
} else {
Color.clear
}
#else
if shouldUseBrainDumpGlassLayer {
Color.clear.background(primaryGlassMaterial)
} else {
Color.clear
}
#endif
}
)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
@ -2357,45 +2095,31 @@ struct ContentView: View {
}
.overlay(alignment: Alignment.topTrailing) {
if droppedFileLoadInProgress {
GlassSurface(
enabled: shouldUseProgressOverlayGlassLayer,
material: primaryGlassMaterial,
fallbackColor: chromeFallbackColor,
shape: .capsule
) {
HStack(spacing: 8) {
if droppedFileProgressDeterminate {
ProgressView(value: droppedFileLoadProgress)
.progressViewStyle(.linear)
.frame(width: 120)
} else {
ProgressView()
.frame(width: 16)
}
Text(droppedFileProgressDeterminate ? "\(droppedFileLoadLabel) \(importProgressPercentText)" : "\(droppedFileLoadLabel) Loading…")
.font(.system(size: 11, weight: .medium))
.lineLimit(1)
HStack(spacing: 8) {
if droppedFileProgressDeterminate {
ProgressView(value: droppedFileLoadProgress)
.progressViewStyle(.linear)
.frame(width: 120)
} else {
ProgressView()
.frame(width: 16)
}
.padding(.horizontal, 10)
.padding(.vertical, 7)
Text(droppedFileProgressDeterminate ? "\(droppedFileLoadLabel) \(importProgressPercentText)" : "\(droppedFileLoadLabel) Loading…")
.font(.system(size: 11, weight: .medium))
.lineLimit(1)
}
.padding(.horizontal, 10)
.padding(.vertical, 7)
.background(.ultraThinMaterial, in: Capsule(style: .continuous))
.padding(.top, viewModel.isBrainDumpMode ? 12 : 50)
.padding(.trailing, 12)
}
}
#if os(macOS)
.toolbarBackground(
enableTranslucentWindow
? AnyShapeStyle(Color(nsColor: .windowBackgroundColor).opacity(macTranslucencyOpacity(for: .toolbar, colorScheme: colorScheme)))
: AnyShapeStyle(Color(nsColor: .windowBackgroundColor)),
for: ToolbarPlacement.windowToolbar
)
.toolbarBackgroundVisibility(.visible, for: ToolbarPlacement.windowToolbar)
.toolbarBackground(AnyShapeStyle(Color(nsColor: .windowBackgroundColor)), for: ToolbarPlacement.windowToolbar)
.toolbarBackgroundVisibility(enableTranslucentWindow ? .hidden : .visible, for: ToolbarPlacement.windowToolbar)
#else
.toolbarBackground(
shouldUseLiquidGlass ? AnyShapeStyle(primaryGlassMaterial) : AnyShapeStyle(Color(.systemBackground)),
for: ToolbarPlacement.navigationBar
)
.toolbarBackground(enableTranslucentWindow ? AnyShapeStyle(.ultraThinMaterial) : AnyShapeStyle(Color(.systemBackground)), for: ToolbarPlacement.navigationBar)
#endif
}
@ -2421,8 +2145,8 @@ struct ContentView: View {
.padding(.leading, 12)
}
if performanceTier != .normal {
Text(performanceStatusLabel)
if largeFileModeEnabled {
Text("Large File Mode")
.font(.system(size: 11, weight: .semibold))
.foregroundColor(.secondary)
.padding(.horizontal, 8)
@ -2441,46 +2165,9 @@ struct ContentView: View {
.padding(.bottom, 8)
.padding(.trailing, 16)
}
.background(shouldUseStatusGlassLayer ? AnyShapeStyle(primaryGlassMaterial) : AnyShapeStyle(Color.clear))
.background(enableTranslucentWindow ? AnyShapeStyle(.ultraThinMaterial) : AnyShapeStyle(Color.clear))
}
#if os(iOS)
@ViewBuilder
private var iosBottomActionBar: some View {
GlassSurface(
enabled: shouldUseBottomBarGlassLayer,
material: primaryGlassMaterial,
fallbackColor: chromeFallbackColor,
shape: .rounded(0)
) {
HStack {
Spacer(minLength: 0)
HStack(spacing: 8) {
Button(action: { openFileFromToolbar() }) {
Label("Open", systemImage: "folder")
}
Button(action: { saveCurrentTabFromToolbar() }) {
Label("Save", systemImage: "square.and.arrow.down")
}
.disabled(viewModel.selectedTab == nil)
Button(action: { showFindReplace = true }) {
Label("Find", systemImage: "magnifyingglass")
}
Button(action: { toggleAutoCompletion() }) {
Label("Complete", systemImage: "text.badge.plus")
}
}
Spacer(minLength: 0)
}
.font(.system(size: 12, weight: .medium))
.buttonStyle(.bordered)
.padding(.horizontal, 10)
.padding(.vertical, 8)
.frame(maxWidth: .infinity)
}
}
#endif
@ViewBuilder
var tabBarView: some View {
ScrollView(.horizontal, showsIndicators: false) {
@ -2517,13 +2204,9 @@ struct ContentView: View {
.padding(.vertical, 6)
}
#if os(macOS)
.background(
enableTranslucentWindow
? AnyShapeStyle(Color(nsColor: .windowBackgroundColor).opacity(macTranslucencyOpacity(for: .tabStrip, colorScheme: colorScheme)))
: AnyShapeStyle(Color(nsColor: .windowBackgroundColor))
)
.background(enableTranslucentWindow ? AnyShapeStyle(.ultraThinMaterial) : AnyShapeStyle(Color(nsColor: .windowBackgroundColor)))
#else
.background(shouldUseLiquidGlass ? AnyShapeStyle(Color.secondary.opacity(0.08)) : AnyShapeStyle(Color(.systemBackground)))
.background(enableTranslucentWindow ? AnyShapeStyle(.ultraThinMaterial) : AnyShapeStyle(Color(.systemBackground)))
#endif
}
@ -2536,16 +2219,6 @@ struct ContentView: View {
#endif
}
private var performanceStatusLabel: String {
if forceLargeFileMode { return "Performance Mode" }
switch performanceTier {
case .normal: return ""
case .light: return "Light Performance"
case .strong: return "Large File Mode"
case .maximum: return "Max Performance"
}
}
private var importProgressPercentText: String {
let clamped = min(max(droppedFileLoadProgress, 0), 1)
if clamped > 0, clamped < 0.01 { return "1%" }

View file

@ -7,7 +7,6 @@ private let syntaxHighlightSignposter = OSSignposter(subsystem: "h3p.Neon-Vision
///MARK: - Paste Notifications
extension Notification.Name {
static let pastedFileURL = Notification.Name("pastedFileURL")
static let editorScrollCompactnessDidChange = Notification.Name("editorScrollCompactnessDidChange")
}
///MARK: - Scope Match Models
@ -1306,7 +1305,6 @@ struct CustomTextEditor: NSViewRepresentable {
let indentWidth: Int
let autoIndentEnabled: Bool
let autoCloseBracketsEnabled: Bool
let showKeyboardAccessoryBar: Bool
let highlightRefreshToken: Int
private var fontName: String {
@ -1845,6 +1843,27 @@ struct CustomTextEditor: NSViewRepresentable {
return NSRange(location: startLine, length: max(0, endLine - startLine))
}
private func preferredHighlightRange(
in textView: NSTextView,
text: NSString,
explicitRange: NSRange?,
immediate: Bool
) -> NSRange {
let fullRange = NSRange(location: 0, length: text.length)
if let explicitRange {
return explicitRange
}
// For very large buffers, prioritize visible content while typing.
guard !immediate, text.length >= 100_000 else { return fullRange }
guard let layoutManager = textView.layoutManager,
let textContainer = textView.textContainer else { return fullRange }
layoutManager.ensureLayout(for: textContainer)
let visibleGlyphRange = layoutManager.glyphRange(forBoundingRect: textView.visibleRect, in: textContainer)
let visibleCharacterRange = layoutManager.characterRange(forGlyphRange: visibleGlyphRange, actualGlyphRange: nil)
guard visibleCharacterRange.length > 0 else { return fullRange }
return expandedHighlightRange(around: visibleCharacterRange, in: text, maxUTF16Padding: 12_000)
}
func rehighlight(token: Int, generation: Int, immediate: Bool = false, targetRange: NSRange? = nil) {
if !Thread.isMainThread {
DispatchQueue.main.async { [weak self] in
@ -1876,6 +1895,14 @@ struct CustomTextEditor: NSViewRepresentable {
type: theme.syntax.type
)
let patterns = getSyntaxPatterns(for: language, colors: colors)
let nsText = textSnapshot as NSString
let fullRange = NSRange(location: 0, length: nsText.length)
let applyRange = preferredHighlightRange(
in: textView,
text: nsText,
explicitRange: targetRange,
immediate: immediate
)
// Cancel any in-flight work
pendingHighlight?.cancel()
@ -1883,9 +1910,6 @@ struct CustomTextEditor: NSViewRepresentable {
let work = DispatchWorkItem { [weak self] in
let interval = syntaxHighlightSignposter.beginInterval("rehighlight_macos")
// Compute matches off the main thread
let nsText = textSnapshot as NSString
let fullRange = NSRange(location: 0, length: nsText.length)
let applyRange = targetRange ?? fullRange
var coloredRanges: [(NSRange, Color)] = []
for (pattern, color) in patterns {
guard let regex = cachedSyntaxRegex(pattern: pattern, options: [.anchorsMatchLines]) else { continue }
@ -1984,9 +2008,6 @@ struct CustomTextEditor: NSViewRepresentable {
self.lastHighlightToken = token
self.lastSelectionLocation = selectedLocation
self.lastTranslucencyEnabled = self.parent.translucentBackgroundEnabled
// Re-apply visibility preference after recoloring.
self.parent.applyInvisibleCharacterPreference(tv)
}
}
@ -1995,7 +2016,15 @@ struct CustomTextEditor: NSViewRepresentable {
if immediate {
highlightQueue.async(execute: work)
} else {
highlightQueue.asyncAfter(deadline: .now() + 0.12, execute: work)
let delay: TimeInterval
if targetRange != nil {
delay = 0.08
} else if textSnapshot.utf16.count >= 120_000 {
delay = 0.22
} else {
delay = 0.12
}
highlightQueue.asyncAfter(deadline: .now() + delay, execute: work)
}
}
@ -2283,7 +2312,6 @@ struct CustomTextEditor: UIViewRepresentable {
let indentWidth: Int
let autoIndentEnabled: Bool
let autoCloseBracketsEnabled: Bool
let showKeyboardAccessoryBar: Bool
let highlightRefreshToken: Int
private var fontName: String {
@ -2310,95 +2338,6 @@ struct CustomTextEditor: UIViewRepresentable {
return UIFont.monospacedSystemFont(ofSize: targetSize, weight: .regular)
}
#if os(iOS)
private static let keyboardAccessoryTag = 824731
private func makeKeyboardAccessoryView(for textView: UITextView) -> UIView {
let container = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: 44))
container.tag = Self.keyboardAccessoryTag
container.backgroundColor = UIColor.secondarySystemBackground
let stack = UIStackView()
stack.translatesAutoresizingMaskIntoConstraints = false
stack.axis = .horizontal
stack.alignment = .fill
stack.distribution = .fillEqually
stack.spacing = 6
func makeButton(_ title: String, action: @escaping () -> Void) -> UIButton {
let button = UIButton(type: .system)
button.setTitle(title, for: .normal)
button.titleLabel?.font = UIFont.systemFont(ofSize: 14, weight: .semibold)
button.backgroundColor = UIColor.tertiarySystemFill
button.layer.cornerRadius = 8
button.addAction(UIAction { _ in action() }, for: .touchUpInside)
return button
}
let buttons: [UIButton] = [
makeButton("Tab") { insertSnippet("\t", in: textView) },
makeButton("{ }") { insertSnippet("{}", in: textView, caretOffset: -1) },
makeButton("( )") { insertSnippet("()", in: textView, caretOffset: -1) },
makeButton("[ ]") { insertSnippet("[]", in: textView, caretOffset: -1) },
makeButton("->") { insertSnippet(" -> ", in: textView) },
makeButton(">>") { indentSelection(in: textView) },
makeButton("<<") { unindentSelection(in: textView) }
]
buttons.forEach { stack.addArrangedSubview($0) }
container.addSubview(stack)
NSLayoutConstraint.activate([
stack.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 8),
stack.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -8),
stack.topAnchor.constraint(equalTo: container.topAnchor, constant: 6),
stack.bottomAnchor.constraint(equalTo: container.bottomAnchor, constant: -6)
])
return container
}
private func insertSnippet(_ snippet: String, in textView: UITextView, caretOffset: Int = 0) {
let selected = textView.selectedRange
if let textRange = textView.selectedTextRange {
textView.replace(textRange, withText: snippet)
}
let newLocation = max(0, min((textView.text as NSString).length, selected.location + snippet.utf16.count + caretOffset))
textView.selectedRange = NSRange(location: newLocation, length: 0)
(textView.delegate as? Coordinator)?.textViewDidChange(textView)
}
private func indentSelection(in textView: UITextView) {
let nsText = textView.text as NSString? ?? ""
let selected = textView.selectedRange
let lineRange = nsText.lineRange(for: selected)
let chunk = nsText.substring(with: lineRange)
let indented = chunk
.components(separatedBy: "\n")
.map { " " + $0 }
.joined(separator: "\n")
textView.textStorage.replaceCharacters(in: lineRange, with: indented)
textView.selectedRange = NSRange(location: lineRange.location + 4, length: selected.length)
(textView.delegate as? Coordinator)?.textViewDidChange(textView)
}
private func unindentSelection(in textView: UITextView) {
let nsText = textView.text as NSString? ?? ""
let selected = textView.selectedRange
let lineRange = nsText.lineRange(for: selected)
let chunk = nsText.substring(with: lineRange)
let unindented = chunk
.components(separatedBy: "\n")
.map { line -> String in
if line.hasPrefix(" ") { return String(line.dropFirst(4)) }
if line.hasPrefix("\t") { return String(line.dropFirst(1)) }
return line
}
.joined(separator: "\n")
textView.textStorage.replaceCharacters(in: lineRange, with: unindented)
textView.selectedRange = NSRange(location: max(0, lineRange.location), length: selected.length)
(textView.delegate as? Coordinator)?.textViewDidChange(textView)
}
#endif
func makeUIView(context: Context) -> LineNumberedTextViewContainer {
let container = LineNumberedTextViewContainer()
let textView = container.textView
@ -2426,7 +2365,6 @@ struct CustomTextEditor: UIViewRepresentable {
textView.smartDashesType = .no
textView.smartQuotesType = .no
textView.smartInsertDeleteType = .no
textView.inputAccessoryView = showKeyboardAccessoryBar ? makeKeyboardAccessoryView(for: textView) : nil
textView.backgroundColor = translucentBackgroundEnabled ? .clear : .systemBackground
textView.textContainer.lineBreakMode = (isLineWrapEnabled && !isLargeFileMode) ? .byWordWrapping : .byClipping
textView.textContainer.widthTracksTextView = isLineWrapEnabled && !isLargeFileMode
@ -2469,13 +2407,6 @@ struct CustomTextEditor: UIViewRepresentable {
let baseColor = UIColor(theme.text)
textView.tintColor = UIColor(theme.cursor)
textView.backgroundColor = translucentBackgroundEnabled ? .clear : UIColor(theme.background)
let hasSnippetAccessory = textView.inputAccessoryView?.tag == Self.keyboardAccessoryTag
if showKeyboardAccessoryBar != hasSnippetAccessory {
textView.inputAccessoryView = showKeyboardAccessoryBar ? makeKeyboardAccessoryView(for: textView) : nil
if textView.isFirstResponder {
textView.reloadInputViews()
}
}
textView.textContainer.lineBreakMode = (isLineWrapEnabled && !isLargeFileMode) ? .byWordWrapping : .byClipping
textView.textContainer.widthTracksTextView = isLineWrapEnabled && !isLargeFileMode
textView.typingAttributes[.foregroundColor] = baseColor
@ -2508,7 +2439,6 @@ struct CustomTextEditor: UIViewRepresentable {
private var lastTranslucencyEnabled: Bool?
private var isApplyingHighlight = false
private var highlightGeneration: Int = 0
private var lastPostedScrollCompactness: CGFloat = -1
init(_ parent: CustomTextEditor) {
self.parent = parent
@ -2566,11 +2496,7 @@ struct CustomTextEditor: UIViewRepresentable {
pendingHighlight?.cancel()
highlightGeneration &+= 1
let generation = highlightGeneration
let targetRange: NSRange? = {
let nsLength = (text as NSString).length
guard nsLength >= 100_000 else { return nil }
return expandedVisibleRange(in: textView, textLength: nsLength)
}()
let applyRange = preferredHighlightRange(textView: textView, text: text as NSString, immediate: immediate)
let work = DispatchWorkItem { [weak self] in
self?.rehighlight(
text: text,
@ -2578,29 +2504,48 @@ struct CustomTextEditor: UIViewRepresentable {
colorScheme: scheme,
token: token,
generation: generation,
targetRange: targetRange
applyRange: applyRange
)
}
pendingHighlight = work
if immediate || lastHighlightedText.isEmpty || lastHighlightToken != token {
highlightQueue.async(execute: work)
} else {
let delay: TimeInterval = text.utf16.count >= 80_000 ? 0.2 : 0.1
let delay: TimeInterval
if text.utf16.count >= 120_000 {
delay = 0.24
} else if text.utf16.count >= 80_000 {
delay = 0.18
} else {
delay = 0.1
}
highlightQueue.asyncAfter(deadline: .now() + delay, execute: work)
}
}
private func expandedVisibleRange(in textView: UITextView, textLength: Int) -> NSRange {
let visibleRect = CGRect(origin: textView.contentOffset, size: textView.bounds.size).insetBy(dx: 0, dy: -120)
private func expandedRange(around range: NSRange, in text: NSString, maxUTF16Padding: Int = 8000) -> NSRange {
let start = max(0, range.location - maxUTF16Padding)
let end = min(text.length, NSMaxRange(range) + maxUTF16Padding)
let startLine = text.lineRange(for: NSRange(location: start, length: 0)).location
let endAnchor = max(startLine, min(text.length - 1, max(0, end - 1)))
let endLine = NSMaxRange(text.lineRange(for: NSRange(location: endAnchor, length: 0)))
return NSRange(location: startLine, length: max(0, endLine - startLine))
}
private func preferredHighlightRange(
textView: UITextView,
text: NSString,
immediate: Bool
) -> NSRange {
let fullRange = NSRange(location: 0, length: text.length)
// iOS rehighlight builds attributed text for the whole buffer; for very large files
// keep syntax matching focused on visible content while typing.
guard !immediate, text.length >= 100_000 else { return fullRange }
let visibleRect = CGRect(origin: textView.contentOffset, size: textView.bounds.size).insetBy(dx: 0, dy: -80)
let glyphRange = textView.layoutManager.glyphRange(forBoundingRect: visibleRect, in: textView.textContainer)
var charRange = textView.layoutManager.characterRange(forGlyphRange: glyphRange, actualGlyphRange: nil)
if charRange.location == NSNotFound {
charRange = NSRange(location: 0, length: min(4000, textLength))
}
let pad = 4000
let start = max(0, charRange.location - pad)
let end = min(textLength, NSMaxRange(charRange) + pad)
return NSRange(location: start, length: max(0, end - start))
let charRange = textView.layoutManager.characterRange(forGlyphRange: glyphRange, actualGlyphRange: nil)
guard charRange.length > 0 else { return fullRange }
return expandedRange(around: charRange, in: text, maxUTF16Padding: 12_000)
}
private func rehighlight(
@ -2609,13 +2554,12 @@ struct CustomTextEditor: UIViewRepresentable {
colorScheme: ColorScheme,
token: Int,
generation: Int,
targetRange: NSRange? = nil
applyRange: NSRange
) {
let interval = syntaxHighlightSignposter.beginInterval("rehighlight_ios")
defer { syntaxHighlightSignposter.endInterval("rehighlight_ios", interval) }
let nsText = text as NSString
let fullRange = NSRange(location: 0, length: nsText.length)
let applyRange = targetRange ?? fullRange
let theme = currentEditorTheme(colorScheme: colorScheme)
let baseColor = UIColor(theme.text)
let baseFont: UIFont
@ -2627,6 +2571,14 @@ struct CustomTextEditor: UIViewRepresentable {
baseFont = UIFont.monospacedSystemFont(ofSize: parent.fontSize, weight: .regular)
}
let attributed = NSMutableAttributedString(
string: text,
attributes: [
.foregroundColor: baseColor,
.font: baseFont
]
)
let colors = SyntaxColors(
keyword: theme.syntax.keyword,
string: theme.syntax.string,
@ -2644,13 +2596,12 @@ struct CustomTextEditor: UIViewRepresentable {
)
let patterns = getSyntaxPatterns(for: language, colors: colors)
var coloredRanges: [(NSRange, UIColor)] = []
for (pattern, color) in patterns {
guard let regex = cachedSyntaxRegex(pattern: pattern, options: [.anchorsMatchLines]) else { continue }
let matches = regex.matches(in: text, range: applyRange)
let uiColor = UIColor(color)
for match in matches {
coloredRanges.append((match.range, uiColor))
attributed.addAttribute(.foregroundColor, value: uiColor, range: match.range)
}
}
@ -2660,15 +2611,7 @@ struct CustomTextEditor: UIViewRepresentable {
guard textView.text == text else { return }
let selectedRange = textView.selectedRange
self.isApplyingHighlight = true
textView.textStorage.beginEditing()
textView.textStorage.removeAttribute(.foregroundColor, range: applyRange)
textView.textStorage.removeAttribute(.backgroundColor, range: applyRange)
textView.textStorage.removeAttribute(.underlineStyle, range: applyRange)
textView.textStorage.addAttribute(.foregroundColor, value: baseColor, range: applyRange)
textView.textStorage.addAttribute(.font, value: baseFont, range: applyRange)
for (range, color) in coloredRanges {
textView.textStorage.addAttribute(.foregroundColor, value: color, range: range)
}
textView.attributedText = attributed
let wantsBracketTokens = self.parent.highlightMatchingBrackets
let wantsScopeBackground = self.parent.highlightScopeBackground
let wantsScopeGuides = self.parent.showScopeGuides && !self.parent.isLineWrapEnabled && self.parent.language.lowercased() != "swift"
@ -2711,11 +2654,8 @@ struct CustomTextEditor: UIViewRepresentable {
if self.parent.highlightCurrentLine {
let ns = text as NSString
let lineRange = ns.lineRange(for: selectedRange)
if targetRange == nil || NSIntersectionRange(lineRange, applyRange).length > 0 {
textView.textStorage.addAttribute(.backgroundColor, value: UIColor.secondarySystemFill, range: lineRange)
}
textView.textStorage.addAttribute(.backgroundColor, value: UIColor.secondarySystemFill, range: lineRange)
}
textView.textStorage.endEditing()
textView.selectedRange = selectedRange
textView.typingAttributes = [
.foregroundColor: baseColor,
@ -2743,9 +2683,6 @@ struct CustomTextEditor: UIViewRepresentable {
func textViewDidChangeSelection(_ textView: UITextView) {
guard !isApplyingHighlight else { return }
if textView.selectedRange.length == 0 {
textView.scrollRangeToVisible(textView.selectedRange)
}
scheduleHighlightIfNeeded(currentText: textView.text, immediate: true)
}
@ -2800,16 +2737,6 @@ struct CustomTextEditor: UIViewRepresentable {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
syncLineNumberScroll()
let y = max(0, scrollView.contentOffset.y + scrollView.adjustedContentInset.top)
let compactness = min(1, y / 120)
if abs(compactness - lastPostedScrollCompactness) >= 0.02 {
lastPostedScrollCompactness = compactness
NotificationCenter.default.post(
name: .editorScrollCompactnessDidChange,
object: nil,
userInfo: ["compactness": compactness]
)
}
}
func syncLineNumberScroll() {

File diff suppressed because it is too large Load diff

View file

@ -5,6 +5,27 @@ import UniformTypeIdentifiers
import AppKit
#endif
enum NeonUIStyle {
static let accentBlue = Color(red: 0.17, green: 0.49, blue: 0.98)
static let accentBlueSoft = Color(red: 0.44, green: 0.72, blue: 0.99)
static func surfaceFill(for scheme: ColorScheme) -> LinearGradient {
let top = scheme == .dark
? Color(red: 0.09, green: 0.14, blue: 0.23).opacity(0.82)
: Color(red: 0.94, green: 0.97, blue: 1.00).opacity(0.94)
let bottom = scheme == .dark
? Color(red: 0.06, green: 0.10, blue: 0.18).opacity(0.88)
: Color(red: 0.88, green: 0.94, blue: 1.00).opacity(0.96)
return LinearGradient(colors: [top, bottom], startPoint: .topLeading, endPoint: .bottomTrailing)
}
static func surfaceStroke(for scheme: ColorScheme) -> Color {
scheme == .dark
? accentBlueSoft.opacity(0.34)
: accentBlue.opacity(0.22)
}
}
struct PlainTextDocument: FileDocument {
static var readableContentTypes: [UTType] { [.plainText, .text, .sourceCode] }

View file

@ -4,9 +4,13 @@ import Foundation
struct SidebarView: View {
let content: String
let language: String
@Environment(\.colorScheme) private var colorScheme
@State private var tocItems: [String] = ["No content available"]
@State private var tocRefreshTask: Task<Void, Never>?
var body: some View {
List {
ForEach(generateTableOfContents(), id: \.self) { item in
ForEach(tocItems, id: \.self) { item in
Button {
jump(to: item)
} label: {
@ -23,6 +27,26 @@ struct SidebarView: View {
.scrollContentBackground(.hidden)
.background(Color.clear)
.frame(maxWidth: .infinity, alignment: .leading)
.background(
RoundedRectangle(cornerRadius: 14, style: .continuous)
.fill(NeonUIStyle.surfaceFill(for: colorScheme))
.overlay(
RoundedRectangle(cornerRadius: 14, style: .continuous)
.stroke(NeonUIStyle.surfaceStroke(for: colorScheme), lineWidth: 1)
)
)
.onAppear {
scheduleTOCRefresh()
}
.onChange(of: content) { _, _ in
scheduleTOCRefresh()
}
.onChange(of: language) { _, _ in
scheduleTOCRefresh()
}
.onDisappear {
tocRefreshTask?.cancel()
}
}
private func jump(to item: String) {
@ -39,8 +63,22 @@ struct SidebarView: View {
}
}
private func scheduleTOCRefresh() {
tocRefreshTask?.cancel()
let snapshotContent = content
let snapshotLanguage = language
tocRefreshTask = Task(priority: .utility) {
try? await Task.sleep(nanoseconds: 120_000_000)
guard !Task.isCancelled else { return }
let generated = SidebarView.generateTableOfContents(content: snapshotContent, language: snapshotLanguage)
await MainActor.run {
tocItems = generated
}
}
}
// Naive line-scanning TOC: looks for language-specific declarations or headers.
func generateTableOfContents() -> [String] {
static func generateTableOfContents(content: String, language: String) -> [String] {
guard !content.isEmpty else { return ["No content available"] }
if (content as NSString).length >= 400_000 {
return ["Large file detected: TOC disabled for performance"]
@ -195,26 +233,15 @@ struct SidebarView: View {
}
struct ProjectStructureSidebarView: View {
let rootFolderURL: URL?
@Binding var nodes: [ProjectTreeNode]
let nodes: [ProjectTreeNode]
let selectedFileURL: URL?
let translucentBackgroundEnabled: Bool
let onOpenFile: () -> Void
let onOpenFolder: () -> Void
let onOpenProjectFile: (URL) -> Void
let onRefreshTree: () -> Void
let onLoadChildren: (URL) -> [ProjectTreeNode]
@Environment(\.accessibilityReduceTransparency) private var reduceTransparency
@AppStorage("SettingsLiquidGlassEnabled") private var liquidGlassEnabled: Bool = true
@State private var isLowPowerModeEnabled: Bool = ProcessInfo.processInfo.isLowPowerModeEnabled
@State private var expandedDirectories: Set<String> = []
private var shouldUseSidebarGlass: Bool {
#if os(iOS)
translucentBackgroundEnabled && liquidGlassEnabled && !reduceTransparency && !isLowPowerModeEnabled
#else
translucentBackgroundEnabled && liquidGlassEnabled && !reduceTransparency
#endif
}
@Environment(\.colorScheme) private var colorScheme
var body: some View {
VStack(alignment: .leading, spacing: 8) {
@ -267,14 +294,16 @@ struct ProjectStructureSidebarView: View {
}
.listStyle(.sidebar)
.scrollContentBackground(.hidden)
.background(shouldUseSidebarGlass ? AnyShapeStyle(.ultraThinMaterial) : AnyShapeStyle(Color.clear))
.background(translucentBackgroundEnabled ? AnyShapeStyle(.ultraThinMaterial) : AnyShapeStyle(Color.clear))
}
.background(shouldUseSidebarGlass ? AnyShapeStyle(.ultraThinMaterial) : AnyShapeStyle(Color.clear))
#if os(iOS)
.onReceive(NotificationCenter.default.publisher(for: Notification.Name.NSProcessInfoPowerStateDidChange)) { _ in
isLowPowerModeEnabled = ProcessInfo.processInfo.isLowPowerModeEnabled
}
#endif
.background(
RoundedRectangle(cornerRadius: 14, style: .continuous)
.fill(NeonUIStyle.surfaceFill(for: colorScheme))
.overlay(
RoundedRectangle(cornerRadius: 14, style: .continuous)
.stroke(NeonUIStyle.surfaceStroke(for: colorScheme), lineWidth: 1)
)
)
}
private func projectNodeView(_ node: ProjectTreeNode, level: Int) -> AnyView {
@ -285,10 +314,6 @@ struct ProjectStructureSidebarView: View {
set: { isExpanded in
if isExpanded {
expandedDirectories.insert(node.id)
if !node.isChildrenLoaded {
let children = onLoadChildren(node.url)
updateChildren(for: node.id, children: children)
}
} else {
expandedDirectories.remove(node.id)
}
@ -327,29 +352,11 @@ struct ProjectStructureSidebarView: View {
)
}
}
private func updateChildren(for nodeID: String, children: [ProjectTreeNode]) {
func patch(nodes: inout [ProjectTreeNode]) -> Bool {
for idx in nodes.indices {
if nodes[idx].id == nodeID {
nodes[idx].children = children
nodes[idx].isChildrenLoaded = true
return true
}
if patch(nodes: &nodes[idx].children) {
return true
}
}
return false
}
_ = patch(nodes: &nodes)
}
}
struct ProjectTreeNode: Identifiable {
let url: URL
let isDirectory: Bool
var children: [ProjectTreeNode]
var isChildrenLoaded: Bool
var id: String { url.path }
}

View file

@ -437,11 +437,11 @@ private func paletteForThemeName(_ name: String, defaults: UserDefaults) -> Them
let cursor = colorFromHex(defaults.string(forKey: "SettingsThemeCursorColor") ?? "#4EA4FF", fallback: .blue)
let selection = colorFromHex(defaults.string(forKey: "SettingsThemeSelectionColor") ?? "#2A3340", fallback: .gray)
let keyword = colorFromHex(defaults.string(forKey: "SettingsThemeKeywordColor") ?? "#F5D90A", fallback: .yellow)
let string = colorFromHex(defaults.string(forKey: "SettingsThemeStringColor") ?? "#66B2FF", fallback: .blue)
let string = colorFromHex(defaults.string(forKey: "SettingsThemeStringColor") ?? "#4EA4FF", fallback: .blue)
let number = colorFromHex(defaults.string(forKey: "SettingsThemeNumberColor") ?? "#FFB86C", fallback: .orange)
let comment = colorFromHex(defaults.string(forKey: "SettingsThemeCommentColor") ?? "#7F8C98", fallback: .gray)
let type = colorFromHex(defaults.string(forKey: "SettingsThemeTypeColor") ?? "#32D269", fallback: .green)
let builtin = colorFromHex(defaults.string(forKey: "SettingsThemeBuiltinColor") ?? "#4EA4FF", fallback: .blue)
let builtin = colorFromHex(defaults.string(forKey: "SettingsThemeBuiltinColor") ?? "#EC7887", fallback: .red)
return ThemePalette(
text: text,
background: background,