Stabilize translucency surfaces and finalize v0.5.2 release notes

This commit is contained in:
h3p 2026-03-09 17:47:50 +01:00
parent d4914e6d97
commit 6a9758a187
32 changed files with 431 additions and 96 deletions

View file

@ -18,6 +18,10 @@ The format follows *Keep a Changelog*. Versions use semantic versioning with pre
### Fixed
- Fixed missing diagnostics reset workflow by adding a dedicated `Clear Diagnostics` action that also clears file-open timing snapshots.
- Fixed macOS editor-window top-bar jumping when toggling the toolbar translucency control by keeping chrome flags stable.
- Fixed CSV/TSV mode header transparency so the mode bar now uses a solid standard window background.
- Fixed settings-window translucency consistency on macOS so title/tab and content regions render as one unified surface.
- Fixed cross-platform updater diagnostics compilation by adding a non-macOS bundle-version reader fallback.
## [Unreleased]

View file

@ -361,7 +361,7 @@
CODE_SIGNING_ALLOWED = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 451;
CURRENT_PROJECT_VERSION = 452;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = CS727NF72U;
ENABLE_APP_SANDBOX = YES;
@ -444,7 +444,7 @@
CODE_SIGNING_ALLOWED = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 451;
CURRENT_PROJECT_VERSION = 452;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = CS727NF72U;
ENABLE_APP_SANDBOX = YES;

View file

@ -6,6 +6,7 @@ import FoundationModels
#if false
// This enum is defined in ContentView.swift; this is here to avoid redefinition errors.
public enum AIModel {
case appleIntelligence
case grok
@ -15,6 +16,8 @@ public enum AIModel {
}
#endif
/// MARK: - Types
public protocol AIClient {
func streamSuggestions(prompt: String) -> AsyncStream<String>
}

View file

@ -4,6 +4,10 @@ import SwiftUI
import FoundationModels
#endif
/// MARK: - Types
struct NeonVisionMacAppCommands: Commands {
let activeEditorViewModel: () -> EditorViewModel
let hasActiveEditorWindow: () -> Bool

View file

@ -10,6 +10,10 @@ import UIKit
#endif
#if os(macOS)
/// MARK: - Types
final class AppDelegate: NSObject, NSApplicationDelegate {
weak var viewModel: EditorViewModel? {
didSet {

View file

@ -3,6 +3,10 @@ import Observation
@MainActor
@Observable
/// MARK: - Types
final class AIActivityLog {
enum Level: String, CaseIterable {
case info = "INFO"

View file

@ -13,6 +13,10 @@ import AppKit
import UIKit
#endif
/// MARK: - Types
enum AppUpdateCheckInterval: String, CaseIterable, Identifiable {
case hourly = "hourly"
case daily = "daily"
@ -1094,6 +1098,21 @@ final class AppUpdateManager: ObservableObject {
}
#endif
#if !os(macOS)
private nonisolated static func readBundleShortVersionString(of appBundleURL: URL) -> String? {
let infoPlistURL = appBundleURL.appendingPathComponent("Info.plist")
guard
let data = try? Data(contentsOf: infoPlistURL),
let plist = try? PropertyListSerialization.propertyList(from: data, options: [], format: nil) as? [String: Any],
let version = plist["CFBundleShortVersionString"] as? String
else {
return nil
}
let trimmed = version.trimmingCharacters(in: .whitespacesAndNewlines)
return trimmed.isEmpty ? nil : trimmed
}
#endif
private func openURL(_ url: URL) {
#if canImport(AppKit)
NSWorkspace.shared.open(url)

View file

@ -3,6 +3,10 @@ import Foundation
import FoundationModels
@Generable(description: "Plain generated text")
/// MARK: - Types
public struct GeneratedText { public var text: String }
public enum AppleFM {

View file

@ -2,6 +2,10 @@ import Foundation
import OSLog
@MainActor
/// MARK: - Types
final class EditorPerformanceMonitor {
struct FileOpenEvent: Codable, Identifiable {
let id: UUID

View file

@ -1,5 +1,9 @@
import Foundation
/// MARK: - Types
public struct LanguageDetector {
public static let shared = LanguageDetector()
private init() {}

View file

@ -1,6 +1,10 @@
import Foundation
import SwiftUI
/// MARK: - Types
enum ReleaseRuntimePolicy {
static var isUpdaterEnabledForCurrentDistribution: Bool {
#if os(macOS)

View file

@ -2,6 +2,10 @@ import Foundation
import OSLog
@MainActor
/// MARK: - Types
final class RuntimeReliabilityMonitor {
static let shared = RuntimeReliabilityMonitor()

View file

@ -1,6 +1,10 @@
import SwiftUI
import Foundation
/// MARK: - Types
private enum SyntaxRegexCache {
static var storage: [String: NSRegularExpression] = [:]
static let lock = NSLock()

View file

@ -5,6 +5,10 @@
import Foundation
// Supported AI providers for suggestions. Extend as needed.
/// MARK: - Types
public enum AIModel: String, CaseIterable, Identifiable {
case appleIntelligence
case grok

View file

@ -1,5 +1,9 @@
import SwiftUI
/// MARK: - Types
struct AIActivityLogView: View {
@State private var log = AIActivityLog.shared

View file

@ -1,5 +1,9 @@
import SwiftUI
/// MARK: - Types
struct AppUpdaterDialog: View {
@EnvironmentObject private var appUpdateManager: AppUpdateManager
@Environment(\.accessibilityReduceTransparency) private var reduceTransparency

View file

@ -6,6 +6,10 @@ import AppKit
import UIKit
#endif
/// MARK: - Types
extension ContentView {
private struct ProjectEditorOverrides: Decodable {
let indentWidth: Int?
@ -629,9 +633,13 @@ extension ContentView {
func applyWindowTranslucency(_ enabled: Bool) {
#if os(macOS)
for window in NSApp.windows {
// Apply only to editor windows registered by ContentView instances.
guard WindowViewModelRegistry.shared.viewModel(for: window.windowNumber) != nil else {
continue
}
window.isOpaque = !enabled
window.backgroundColor = enabled ? .clear : NSColor.windowBackgroundColor
// Keep window chrome layout stable across both modes to avoid frame/titlebar jumps.
// Keep chrome flags constant; toggling these causes visible top-bar jumps.
window.titlebarAppearsTransparent = true
window.toolbarStyle = .unified
window.styleMask.insert(.fullSizeContentView)
@ -760,7 +768,16 @@ extension ContentView {
return []
}
let sorted = urls.sorted { $0.lastPathComponent.localizedCaseInsensitiveCompare($1.lastPathComponent) == .orderedAscending }
let sorted = urls.sorted { lhs, rhs in
let lhsValues = try? lhs.resourceValues(forKeys: [.isDirectoryKey])
let rhsValues = try? rhs.resourceValues(forKeys: [.isDirectoryKey])
let lhsIsDirectory = lhsValues?.isDirectory == true
let rhsIsDirectory = rhsValues?.isDirectory == true
if lhsIsDirectory != rhsIsDirectory {
return lhsIsDirectory && !rhsIsDirectory
}
return lhs.lastPathComponent.localizedCaseInsensitiveCompare(rhs.lastPathComponent) == .orderedAscending
}
var nodes: [ProjectTreeNode] = []
for url in sorted {
if Task.isCancelled { break }

View file

@ -5,6 +5,10 @@ import AppKit
import UIKit
#endif
/// MARK: - Types
extension ContentView {
private var compactActiveProviderName: String {
activeProviderName.components(separatedBy: " (").first ?? activeProviderName

View file

@ -347,6 +347,9 @@ struct ContentView: View {
@State private var recoverySnapshotIdentifier: String = UUID().uuidString
@State private var lastCaretLocation: Int = 0
@State private var sessionCaretByFileURL: [String: Int] = [:]
#if os(macOS)
@State private var isProjectSidebarResizeHandleHovered: Bool = false
#endif
private let quickSwitcherRecentsDefaultsKey = "QuickSwitcherRecentItemsV1"
#if USE_FOUNDATION_MODELS && canImport(FoundationModels)
@ -3463,19 +3466,51 @@ struct ContentView: View {
}
return ZStack {
Color.clear
// Match the same surface as the editor area so the splitter doesn't look like a foreign strip.
Rectangle()
.fill(Color.secondary.opacity(0.32))
.fill(projectSidebarHandleSurfaceStyle)
Rectangle()
.fill(Color.secondary.opacity(0.22))
.frame(width: 1)
}
.frame(width: 10)
.contentShape(Rectangle())
.gesture(drag)
#if os(macOS)
.onHover { hovering in
guard hovering != isProjectSidebarResizeHandleHovered else { return }
isProjectSidebarResizeHandleHovered = hovering
if hovering {
NSCursor.resizeLeftRight.push()
} else {
NSCursor.pop()
}
}
.onDisappear {
if isProjectSidebarResizeHandleHovered {
isProjectSidebarResizeHandleHovered = false
NSCursor.pop()
}
}
#endif
.accessibilityElement()
.accessibilityLabel("Resize Project Sidebar")
.accessibilityHint("Drag left or right to adjust project sidebar width")
}
private var projectSidebarHandleSurfaceStyle: AnyShapeStyle {
if enableTranslucentWindow {
return editorSurfaceBackgroundStyle
}
#if os(iOS)
return useIOSUnifiedSolidSurfaces
? AnyShapeStyle(iOSNonTranslucentSurfaceColor)
: AnyShapeStyle(Color.clear)
#else
return AnyShapeStyle(Color.clear)
#endif
}
private var projectStructureSidebarBody: some View {
ProjectStructureSidebarView(
rootFolderURL: projectRootFolderURL,
@ -3524,6 +3559,15 @@ struct ContentView: View {
}
.padding(.horizontal, 10)
.padding(.vertical, 8)
.background(delimitedHeaderBackgroundColor)
}
private var delimitedHeaderBackgroundColor: Color {
#if os(macOS)
Color(nsColor: .windowBackgroundColor)
#else
Color(.systemBackground)
#endif
}
private var delimitedTableView: some View {

View file

@ -1,5 +1,9 @@
import SwiftUI
/// MARK: - Types
enum GlassShapeKind {
case capsule
case circle

View file

@ -2,6 +2,10 @@ import SwiftUI
#if canImport(UIKit)
import UIKit
/// MARK: - Types
struct IPadKeyboardShortcutBridge: UIViewRepresentable {
let onNewTab: () -> Void
let onOpenFile: () -> Void

View file

@ -1,6 +1,10 @@
#if os(macOS)
import AppKit
/// MARK: - Types
private struct RulerObserverToken: @unchecked Sendable {
let raw: NSObjectProtocol
}

View file

@ -2,6 +2,10 @@ import SwiftUI
import WebKit
#if os(macOS)
/// MARK: - Types
struct MarkdownPreviewWebView: NSViewRepresentable {
let html: String

View file

@ -9,6 +9,10 @@ import CoreText
import UIKit
#endif
/// MARK: - Types
struct NeonSettingsView: View {
private static var cachedEditorFonts: [String] = []
let supportsOpenInTabs: Bool
@ -254,6 +258,7 @@ struct NeonSettingsView: View {
var body: some View {
settingsTabs
#if os(macOS)
.background(settingsWindowBackground)
.frame(
minWidth: macSettingsWindowSize.min.width,
idealWidth: macSettingsWindowSize.ideal.width,
@ -264,14 +269,19 @@ struct NeonSettingsView: View {
SettingsWindowConfigurator(
minSize: macSettingsWindowSize.min,
idealSize: macSettingsWindowSize.ideal,
translucentEnabled: supportsTranslucency && translucentWindow
translucentEnabled: supportsTranslucency && translucentWindow,
translucencyModeRaw: macTranslucencyModeRaw
)
)
#endif
.preferredColorScheme(preferredColorSchemeOverride)
.onAppear {
settingsActiveTab = "general"
moreSectionTab = "support"
if settingsActiveTab.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
settingsActiveTab = "general"
}
if moreSectionTab.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
moreSectionTab = "support"
}
selectedTheme = canonicalThemeName(selectedTheme)
migrateLegacyPinkSettingsIfNeeded()
loadAvailableEditorFontsIfNeeded()
@ -2039,16 +2049,23 @@ struct NeonSettingsView: View {
@ViewBuilder
private var settingsContainerBackground: some View {
#if os(macOS)
if supportsTranslucency && translucentWindow {
Color.clear.background(.ultraThinMaterial)
} else {
Color(nsColor: .windowBackgroundColor)
}
Color.clear
#else
Color.clear.background(.ultraThinMaterial)
#endif
}
#if os(macOS)
@ViewBuilder
private var settingsWindowBackground: some View {
if supportsTranslucency && translucentWindow {
Color.clear
} else {
Color(nsColor: .windowBackgroundColor)
}
}
#endif
private func settingsEffectiveMaxWidth(base: CGFloat) -> CGFloat {
#if os(iOS)
if useTwoColumnSettingsLayout { return max(base, 780) }
@ -2186,24 +2203,8 @@ struct NeonSettingsView: View {
}
private var macSettingsWindowSize: (min: NSSize, ideal: NSSize) {
switch settingsActiveTab {
case "themes":
return (NSSize(width: 740, height: 900), NSSize(width: 840, height: 980))
case "editor":
return (NSSize(width: 640, height: 820), NSSize(width: 720, height: 900))
case "templates":
return (NSSize(width: 600, height: 760), NSSize(width: 680, height: 840))
case "general":
return (NSSize(width: 600, height: 760), NSSize(width: 680, height: 840))
case "ai":
return (NSSize(width: 640, height: 780), NSSize(width: 720, height: 860))
case "updates":
return (NSSize(width: 580, height: 720), NSSize(width: 660, height: 780))
case "support":
return (NSSize(width: 580, height: 720), NSSize(width: 660, height: 780))
default:
return (NSSize(width: 600, height: 760), NSSize(width: 680, height: 840))
}
// Keep a stable window envelope across tabs to avoid toolbar-tab jump/overflow relayout.
(NSSize(width: 740, height: 900), NSSize(width: 840, height: 980))
}
#endif
@ -2323,13 +2324,13 @@ struct SettingsWindowConfigurator: NSViewRepresentable {
let minSize: NSSize
let idealSize: NSSize
let translucentEnabled: Bool
let translucencyModeRaw: String
final class Coordinator {
var didInitialApply = false
var pendingApply: DispatchWorkItem?
var lastMinSize: NSSize?
var lastIdealSize: NSSize?
var lastTranslucentEnabled: Bool?
var lastTranslucencyModeRaw: String?
var didConfigureWindowChrome = false
}
@ -2350,13 +2351,11 @@ struct SettingsWindowConfigurator: NSViewRepresentable {
}
private func scheduleApply(to window: NSWindow?, coordinator: Coordinator) {
if coordinator.didInitialApply,
coordinator.lastMinSize == minSize,
coordinator.lastIdealSize == idealSize,
coordinator.lastTranslucentEnabled == translucentEnabled {
coordinator.pendingApply?.cancel()
if !coordinator.didInitialApply, let window {
apply(to: window, coordinator: coordinator)
return
}
coordinator.pendingApply?.cancel()
let work = DispatchWorkItem {
apply(to: window, coordinator: coordinator)
}
@ -2367,59 +2366,90 @@ struct SettingsWindowConfigurator: NSViewRepresentable {
private func apply(to window: NSWindow?, coordinator: Coordinator) {
guard let window else { return }
let isFirstApply = !coordinator.didInitialApply
let translucencyChanged = coordinator.lastTranslucentEnabled != translucentEnabled
coordinator.lastMinSize = minSize
coordinator.lastIdealSize = idealSize
coordinator.lastTranslucentEnabled = translucentEnabled
coordinator.lastTranslucencyModeRaw = translucencyModeRaw
window.minSize = minSize
// Always enforce native macOS Settings toolbar chrome; other window updaters may have changed it.
window.toolbarStyle = .preference
window.titleVisibility = .hidden
window.title = ""
if isFirstApply {
let targetWidth = max(minSize.width, idealSize.width)
let targetHeight = max(minSize.height, idealSize.height)
if abs(targetWidth - window.frame.size.width) > 1 || abs(targetHeight - window.frame.size.height) > 1 {
// Apply initial geometry once; avoid frame churn during tab/content updates.
var frame = window.frame
let oldHeight = frame.size.height
frame.size = NSSize(width: targetWidth, height: targetHeight)
frame.origin.y += oldHeight - targetHeight
window.setFrame(frame, display: true, animate: false)
}
centerSettingsWindow(window)
}
if !coordinator.didConfigureWindowChrome {
// Match native macOS Settings layout: centered preference tabs and hidden title text.
window.toolbarStyle = .preference
window.titleVisibility = .hidden
window.title = ""
// Keep settings chrome stable for the lifetime of this window.
window.isOpaque = false
window.titlebarAppearsTransparent = true
window.styleMask.insert(.fullSizeContentView)
if #available(macOS 13.0, *) {
window.titlebarSeparatorStyle = .none
}
coordinator.didConfigureWindowChrome = true
}
let targetWidth: CGFloat
let targetHeight: CGFloat
if coordinator.didInitialApply {
// Respect manual window size changes while enforcing per-tab minimums.
targetWidth = max(minSize.width, window.frame.size.width)
targetHeight = max(minSize.height, window.frame.size.height)
} else {
targetWidth = max(minSize.width, idealSize.width)
targetHeight = max(minSize.height, idealSize.height)
}
if abs(targetWidth - window.frame.size.width) > 1 || abs(targetHeight - window.frame.size.height) > 1 {
// Keep the top edge visually stable while adapting size per tab.
var frame = window.frame
let oldHeight = frame.size.height
frame.size = NSSize(width: targetWidth, height: targetHeight)
frame.origin.y += oldHeight - targetHeight
window.setFrame(frame, display: true, animate: false)
}
// Keep settings-window translucency in sync without relying on editor view events.
if translucencyChanged || isFirstApply {
window.isOpaque = !translucentEnabled
window.backgroundColor = translucentEnabled ? .clear : NSColor.windowBackgroundColor
window.titlebarAppearsTransparent = translucentEnabled
if translucentEnabled {
window.styleMask.insert(.fullSizeContentView)
} else {
window.styleMask.remove(.fullSizeContentView)
}
if #available(macOS 13.0, *) {
window.titlebarSeparatorStyle = translucentEnabled ? .none : .automatic
}
}
// Keep a non-clear background to avoid fully transparent titlebar artifacts.
window.backgroundColor = translucencyEnabledColor(enabled: translucentEnabled, window: window)
// Some macOS states restore the title from the selected settings tab.
// Force an empty, hidden title for native Settings appearance.
window.title = ""
window.titleVisibility = .hidden
window.representedURL = nil
coordinator.didInitialApply = true
}
private func translucencyEnabledColor(enabled: Bool, window: NSWindow) -> NSColor {
guard enabled else { return NSColor.windowBackgroundColor }
let isDark = window.effectiveAppearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua
let whiteLevel: CGFloat
switch translucencyModeRaw {
case "subtle":
whiteLevel = isDark ? 0.18 : 0.90
case "vibrant":
whiteLevel = isDark ? 0.12 : 0.82
default:
whiteLevel = isDark ? 0.15 : 0.86
}
// Keep settings tint almost opaque to avoid "more transparent" appearance.
return NSColor(calibratedWhite: whiteLevel, alpha: 0.98)
}
private func centerSettingsWindow(_ settingsWindow: NSWindow) {
let referenceWindow = preferredReferenceWindow(excluding: settingsWindow)
let size = settingsWindow.frame.size
let referenceFrame = referenceWindow?.frame ?? settingsWindow.frame
var origin = NSPoint(
x: round(referenceFrame.midX - size.width / 2),
y: round(referenceFrame.midY - size.height / 2)
)
if let visibleFrame = referenceWindow?.screen?.visibleFrame ?? settingsWindow.screen?.visibleFrame {
origin.x = min(max(origin.x, visibleFrame.minX), visibleFrame.maxX - size.width)
origin.y = min(max(origin.y, visibleFrame.minY), visibleFrame.maxY - size.height)
}
settingsWindow.setFrameOrigin(origin)
}
private func preferredReferenceWindow(excluding settingsWindow: NSWindow) -> NSWindow? {
if let key = NSApp.keyWindow, key !== settingsWindow, key.isVisible {
return key
}
if let main = NSApp.mainWindow, main !== settingsWindow, main.isVisible {
return main
}
return NSApp.windows.first(where: { window in
window !== settingsWindow && window.isVisible && window.level == .normal
})
}
}
#endif

View file

@ -5,6 +5,10 @@ import UniformTypeIdentifiers
import AppKit
#endif
/// MARK: - Types
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)

View file

@ -3,6 +3,10 @@ import SwiftUI
import UniformTypeIdentifiers
import UIKit
/// MARK: - Types
struct ProjectFolderPicker: UIViewControllerRepresentable {
let onPick: (URL) -> Void
let onCancel: () -> Void

View file

@ -2,6 +2,10 @@ import SwiftUI
import Foundation
#if os(macOS)
/// MARK: - Types
private enum MacTranslucencyMode: String {
case subtle
case balanced
@ -355,6 +359,18 @@ struct SidebarView: View {
}
}
struct ProjectStructureSidebarView: View {
private enum SidebarDensity: String, CaseIterable, Identifiable {
case compact
case comfortable
var id: String { rawValue }
}
private struct FileIconStyle {
let symbol: String
let color: Color
}
let rootFolderURL: URL?
let nodes: [ProjectTreeNode]
let selectedFileURL: URL?
@ -370,6 +386,8 @@ struct ProjectStructureSidebarView: View {
#if os(macOS)
@AppStorage("SettingsMacTranslucencyMode") private var macTranslucencyModeRaw: String = "balanced"
#endif
@AppStorage("SettingsProjectSidebarDensity") private var sidebarDensityRaw: String = SidebarDensity.compact.rawValue
@AppStorage("SettingsProjectSidebarAutoCollapseDeep") private var autoCollapseDeepFolders: Bool = true
var body: some View {
VStack(alignment: .leading, spacing: 0) {
@ -405,6 +423,12 @@ struct ProjectStructureSidebarView: View {
)
}
Divider()
Picker("Density", selection: $sidebarDensityRaw) {
Text("Compact").tag(SidebarDensity.compact.rawValue)
Text("Comfortable").tag(SidebarDensity.comfortable.rawValue)
}
Toggle("Auto-collapse Deep Folders", isOn: $autoCollapseDeepFolders)
Divider()
Button("Expand All") {
expandAllDirectories()
}
@ -419,20 +443,20 @@ struct ProjectStructureSidebarView: View {
.accessibilityLabel("Expand or collapse all folders")
.accessibilityHint("Expands or collapses all folders in the project tree")
}
.padding(.horizontal, 10)
.padding(.top, 10)
.padding(.bottom, 8)
.padding(.horizontal, headerHorizontalPadding)
.padding(.top, headerTopPadding)
.padding(.bottom, headerBottomPadding)
#if os(macOS)
.background(sidebarHeaderFill)
#endif
if let rootFolderURL {
Text(rootFolderURL.path)
.font(.caption2)
.font(.system(size: isCompactDensity ? 11 : 12))
.foregroundStyle(.secondary)
.lineLimit(2)
.lineLimit(isCompactDensity ? 1 : 2)
.textSelection(.enabled)
.padding(.horizontal, 10)
.padding(.horizontal, headerHorizontalPadding)
}
List {
@ -560,18 +584,21 @@ struct ProjectStructureSidebarView: View {
}
private func expandAllDirectories() {
expandedDirectories = allDirectoryNodeIDs(in: nodes)
expandedDirectories = allDirectoryNodeIDs(in: nodes, level: 0)
}
private func collapseAllDirectories() {
expandedDirectories.removeAll()
}
private func allDirectoryNodeIDs(in treeNodes: [ProjectTreeNode]) -> Set<String> {
private func allDirectoryNodeIDs(in treeNodes: [ProjectTreeNode], level: Int) -> Set<String> {
var result: Set<String> = []
for node in treeNodes where node.isDirectory {
result.insert(node.id)
result.formUnion(allDirectoryNodeIDs(in: node.children))
let shouldInclude = !autoCollapseDeepFolders || level < 2
if shouldInclude {
result.insert(node.id)
}
result.formUnion(allDirectoryNodeIDs(in: node.children, level: level + 1))
}
return result
}
@ -593,21 +620,30 @@ struct ProjectStructureSidebarView: View {
projectNodeView(child, level: level + 1)
}
} label: {
Label(node.url.lastPathComponent, systemImage: "folder")
.lineLimit(1)
HStack(spacing: 8) {
Image(systemName: "folder")
.foregroundStyle(.blue)
.symbolRenderingMode(.hierarchical)
Text(node.url.lastPathComponent)
.lineLimit(1)
}
.padding(.vertical, rowVerticalPadding)
}
.padding(.leading, CGFloat(level) * 10)
.padding(.leading, CGFloat(level) * levelIndent)
.listRowInsets(rowInsets)
.listRowBackground(Color.clear)
.listRowSeparator(.hidden)
)
} else {
let style = fileIconStyle(for: node.url)
return AnyView(
Button {
onOpenProjectFile(node.url)
} label: {
HStack(spacing: 8) {
Image(systemName: "doc.text")
.foregroundColor(.secondary)
Image(systemName: style.symbol)
.foregroundStyle(style.color)
.symbolRenderingMode(.hierarchical)
Text(node.url.lastPathComponent)
.lineLimit(1)
Spacer()
@ -616,14 +652,100 @@ struct ProjectStructureSidebarView: View {
.foregroundColor(.accentColor)
}
}
.padding(.vertical, rowVerticalPadding)
}
.buttonStyle(.plain)
.padding(.leading, CGFloat(level) * 10)
.padding(.leading, CGFloat(level) * levelIndent)
.listRowInsets(rowInsets)
.listRowBackground(Color.clear)
.listRowSeparator(.hidden)
)
}
}
private var sidebarDensity: SidebarDensity {
SidebarDensity(rawValue: sidebarDensityRaw) ?? .compact
}
private var isCompactDensity: Bool { sidebarDensity == .compact }
private var levelIndent: CGFloat {
isCompactDensity ? 8 : 12
}
private var rowVerticalPadding: CGFloat {
isCompactDensity ? 1 : 4
}
private var headerHorizontalPadding: CGFloat {
isCompactDensity ? 8 : 10
}
private var headerTopPadding: CGFloat {
isCompactDensity ? 8 : 10
}
private var headerBottomPadding: CGFloat {
isCompactDensity ? 6 : 8
}
private var rowInsets: EdgeInsets {
EdgeInsets(top: 0, leading: isCompactDensity ? 4 : 6, bottom: 0, trailing: 4)
}
private func fileIconStyle(for url: URL) -> FileIconStyle {
let ext = url.pathExtension.lowercased()
let name = url.lastPathComponent.lowercased()
switch ext {
case "swift":
return .init(symbol: "swift", color: .orange)
case "js", "mjs", "cjs":
return .init(symbol: "curlybraces.square", color: .yellow)
case "ts", "tsx":
return .init(symbol: "chevron.left.forwardslash.chevron.right", color: .blue)
case "json", "jsonc", "json5":
return .init(symbol: "curlybraces", color: .green)
case "md", "markdown":
return .init(symbol: "text.alignleft", color: .teal)
case "yml", "yaml", "toml", "ini", "env":
return .init(symbol: "slider.horizontal.3", color: .mint)
case "html", "htm":
return .init(symbol: "chevron.left.slash.chevron.right", color: .orange)
case "css":
return .init(symbol: "paintbrush.pointed", color: .cyan)
case "xml", "svg":
return .init(symbol: "diamond", color: .pink)
case "sh", "bash", "zsh", "ps1":
return .init(symbol: "terminal", color: .indigo)
case "py":
return .init(symbol: "chevron.left.forwardslash.chevron.right", color: .yellow)
case "rb":
return .init(symbol: "diamond.fill", color: .red)
case "go":
return .init(symbol: "g.circle", color: .cyan)
case "rs":
return .init(symbol: "gearshape.2", color: .orange)
case "sql":
return .init(symbol: "cylinder", color: .purple)
case "csv", "tsv":
return .init(symbol: "tablecells", color: .green)
case "txt", "log":
return .init(symbol: "doc.plaintext", color: .secondary)
case "png", "jpg", "jpeg", "gif", "webp", "heic":
return .init(symbol: "photo", color: .purple)
case "pdf":
return .init(symbol: "doc.richtext", color: .red)
default:
if name.hasPrefix(".git") {
return .init(symbol: "arrow.triangle.branch", color: .orange)
}
if name.hasPrefix(".env") {
return .init(symbol: "lock.doc", color: .mint)
}
return .init(symbol: "doc.text", color: .secondary)
}
}
}
struct ProjectTreeNode: Identifiable {

View file

@ -1,6 +1,10 @@
import XCTest
@testable import Neon_Vision_Editor
/// MARK: - Tests
final class AppUpdateManagerTests: XCTestCase {
func testHostAllowlistBehavior() {
XCTAssertTrue(AppUpdateManager.isTrustedGitHubHost("github.com"))

View file

@ -1,5 +1,9 @@
import XCTest
/// MARK: - Tests
final class LanguageDetectorTests: XCTestCase {
func testPreferredLanguageForExtensions() {
let cases: [(String, String)] = [

View file

@ -2,6 +2,10 @@ import XCTest
import SwiftUI
@testable import Neon_Vision_Editor
/// MARK: - Tests
final class MarkdownSyntaxHighlightingTests: XCTestCase {
private func markdownPatterns() -> [String: Color] {
getSyntaxPatterns(

View file

@ -2,6 +2,10 @@ import XCTest
import SwiftUI
@testable import Neon_Vision_Editor
/// MARK: - Tests
final class ReleaseRuntimePolicyTests: XCTestCase {
func testSettingsTabFallsBackToGeneral() {
XCTAssertEqual(ReleaseRuntimePolicy.settingsTab(from: nil), "general")

View file

@ -5,6 +5,10 @@ import XCTest
import AppKit
@MainActor
/// MARK: - Tests
final class WindowTranslucencyTests: XCTestCase {
// Verifies that the translucency toggle updates AppKit window flags used by the toolbar/titlebar.
func testApplyWindowTranslucencyUpdatesMacWindowFlags() {