2026-02-06 18:59:53 +00:00
import SwiftUI
import Foundation
2026-02-19 14:29:53 +00:00
#if os ( macOS )
2026-03-09 16:47:50 +00:00
// / MARK: - T y p e s
2026-02-19 14:29:53 +00:00
private enum MacTranslucencyMode : String {
case subtle
case balanced
case vibrant
var material : Material {
switch self {
case . subtle , . balanced :
return . thickMaterial
case . vibrant :
return . regularMaterial
}
}
var opacity : Double {
switch self {
case . subtle : return 0.98
case . balanced : return 0.93
case . vibrant : return 0.90
}
}
}
#endif
2026-02-06 18:59:53 +00:00
struct SidebarView : View {
2026-02-20 10:34:22 +00:00
private struct TOCItem : Identifiable , Hashable {
let id : String
let title : String
let line : Int ?
}
2026-02-06 18:59:53 +00:00
let content : String
let language : String
2026-02-19 14:29:53 +00:00
let translucentBackgroundEnabled : Bool
2026-02-19 08:09:35 +00:00
@ Environment ( \ . colorScheme ) private var colorScheme
2026-02-19 14:29:53 +00:00
#if os ( macOS )
@ AppStorage ( " SettingsMacTranslucencyMode " ) private var macTranslucencyModeRaw : String = " balanced "
#endif
2026-02-20 10:34:22 +00:00
@ State private var tocItems : [ TOCItem ] = [
TOCItem ( id : " empty " , title : " No content available " , line : nil )
]
2026-02-19 08:09:35 +00:00
@ State private var tocRefreshTask : Task < Void , Never > ?
2026-02-06 18:59:53 +00:00
var body : some View {
List {
2026-02-20 10:34:22 +00:00
ForEach ( tocItems ) { item in
2026-02-06 18:59:53 +00:00
Button {
jump ( to : item )
} label : {
2026-02-20 10:34:22 +00:00
Text ( item . title )
2026-02-06 18:59:53 +00:00
. font ( . system ( size : 13 ) )
. foregroundColor ( . primary )
2026-02-19 14:29:53 +00:00
. frame ( maxWidth : . infinity , alignment : . leading )
. padding ( . vertical , 8 )
. padding ( . horizontal , 12 )
. background (
RoundedRectangle ( cornerRadius : 12 , style : . continuous )
. fill ( sidebarRowFill )
)
2026-02-06 18:59:53 +00:00
}
. buttonStyle ( . plain )
2026-02-20 10:34:22 +00:00
. disabled ( item . line = = nil )
2026-02-19 14:29:53 +00:00
. listRowBackground ( Color . clear )
. listRowSeparator ( . hidden )
2026-02-06 18:59:53 +00:00
}
}
2026-02-19 14:29:53 +00:00
. listStyle ( platformListStyle )
2026-02-06 18:59:53 +00:00
. scrollContentBackground ( . hidden )
. background ( Color . clear )
2026-02-19 14:29:53 +00:00
. frame ( maxWidth : . infinity , maxHeight : . infinity , alignment : . topLeading )
. padding ( sidebarOuterPaddingInsets )
2026-02-19 08:09:35 +00:00
. background (
2026-02-19 14:29:53 +00:00
RoundedRectangle ( cornerRadius : sidebarCornerRadius , style : . continuous )
. fill ( sidebarSurfaceFill )
2026-02-19 08:09:35 +00:00
. overlay (
2026-02-19 14:29:53 +00:00
RoundedRectangle ( cornerRadius : sidebarCornerRadius , style : . continuous )
. stroke ( sidebarSurfaceStroke , lineWidth : 1 )
2026-02-19 08:09:35 +00:00
)
)
2026-02-19 14:29:53 +00:00
. clipShape ( RoundedRectangle ( cornerRadius : sidebarCornerRadius , style : . continuous ) )
2026-02-19 08:09:35 +00:00
. onAppear {
scheduleTOCRefresh ( )
}
. onChange ( of : content ) { _ , _ in
scheduleTOCRefresh ( )
}
. onChange ( of : language ) { _ , _ in
scheduleTOCRefresh ( )
}
. onDisappear {
tocRefreshTask ? . cancel ( )
}
2026-02-06 18:59:53 +00:00
}
2026-02-19 14:29:53 +00:00
private var sidebarSurfaceFill : AnyShapeStyle {
if translucentBackgroundEnabled {
#if os ( macOS )
let mode = MacTranslucencyMode ( rawValue : macTranslucencyModeRaw ) ? ? . balanced
return AnyShapeStyle ( mode . material . opacity ( mode . opacity ) )
#else
return AnyShapeStyle ( . ultraThinMaterial )
#endif
}
#if os ( macOS )
2026-03-26 18:19:45 +00:00
return AnyShapeStyle ( currentEditorTheme ( colorScheme : colorScheme ) . background )
2026-02-19 14:29:53 +00:00
#else
2026-03-26 18:19:45 +00:00
return AnyShapeStyle ( currentEditorTheme ( colorScheme : colorScheme ) . background )
2026-02-19 14:29:53 +00:00
#endif
}
private var sidebarSurfaceStroke : Color {
colorScheme = = . dark
? Color . white . opacity ( 0.12 )
: Color . black . opacity ( 0.08 )
}
private var platformListStyle : some ListStyle {
#if os ( iOS )
PlainListStyle ( )
#else
SidebarListStyle ( )
#endif
}
private var sidebarRowFill : Color {
#if os ( macOS )
Color . secondary . opacity ( 0.10 )
#else
2026-02-20 01:16:58 +00:00
colorScheme = = . dark
? Color . white . opacity ( 0.06 )
: Color ( red : 0.80 , green : 0.88 , blue : 1.0 ) . opacity ( 0.55 )
2026-02-19 14:29:53 +00:00
#endif
}
private var sidebarOuterPaddingInsets : EdgeInsets {
#if os ( iOS )
EdgeInsets ( top : 0 , leading : 10 , bottom : 10 , trailing : 10 )
#else
EdgeInsets ( )
#endif
}
private var sidebarCornerRadius : CGFloat {
#if os ( macOS )
0
#else
14
#endif
}
2026-02-20 10:34:22 +00:00
private func jump ( to item : TOCItem ) {
guard let lineOneBased = item . line , lineOneBased > 0 else { return }
DispatchQueue . main . async {
NotificationCenter . default . post ( name : . moveCursorToLine , object : lineOneBased )
2026-02-06 18:59:53 +00:00
}
}
2026-02-19 08:09:35 +00:00
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
}
}
}
2026-02-06 18:59:53 +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 .
2026-02-20 10:34:22 +00:00
private static func generateTableOfContents ( content : String , language : String ) -> [ TOCItem ] {
guard ! content . isEmpty else {
return [ TOCItem ( id : " empty " , title : " No content available " , line : nil ) ]
}
2026-02-08 11:57:41 +00:00
if ( content as NSString ) . length >= 400_000 {
2026-02-20 10:34:22 +00:00
return [ TOCItem ( id : " large " , title : " Large file detected: TOC disabled for performance " , line : nil ) ]
2026-02-08 11:57:41 +00:00
}
2026-02-06 18:59:53 +00:00
let lines = content . components ( separatedBy : . newlines )
2026-02-20 10:34:22 +00:00
var toc : [ TOCItem ] = [ ]
2026-02-06 18:59:53 +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 " ) {
2026-02-20 10:34:22 +00:00
return TOCItem ( id : " swift- \( index ) " , title : " \( trimmed ) (Line \( index + 1 ) ) " , line : index + 1 )
2026-02-06 18:59:53 +00:00
}
return nil
}
case " python " :
toc = lines . enumerated ( ) . compactMap { index , line in
let trimmed = line . trimmingCharacters ( in : . whitespaces )
if trimmed . hasPrefix ( " def " ) || trimmed . hasPrefix ( " class " ) {
2026-02-20 10:34:22 +00:00
return TOCItem ( id : " python- \( index ) " , title : " \( trimmed ) (Line \( index + 1 ) ) " , line : index + 1 )
2026-02-06 18:59:53 +00:00
}
return nil
}
case " javascript " :
toc = lines . enumerated ( ) . compactMap { index , line in
let trimmed = line . trimmingCharacters ( in : . whitespaces )
if trimmed . hasPrefix ( " function " ) || trimmed . hasPrefix ( " class " ) {
2026-02-20 10:34:22 +00:00
return TOCItem ( id : " js- \( index ) " , title : " \( trimmed ) (Line \( index + 1 ) ) " , line : index + 1 )
2026-02-06 18:59:53 +00:00
}
return nil
}
case " java " :
toc = lines . enumerated ( ) . compactMap { index , line in
let t = line . trimmingCharacters ( in : . whitespaces )
if t . hasPrefix ( " class " ) || ( t . contains ( " void " ) || ( t . contains ( " public " ) && t . contains ( " ( " ) && t . contains ( " ) " ) ) ) {
2026-02-20 10:34:22 +00:00
return TOCItem ( id : " java- \( index ) " , title : " \( t ) (Line \( index + 1 ) ) " , line : index + 1 )
2026-02-06 18:59:53 +00:00
}
return nil
}
case " kotlin " :
toc = lines . enumerated ( ) . compactMap { index , line in
let t = line . trimmingCharacters ( in : . whitespaces )
if t . hasPrefix ( " class " ) || t . hasPrefix ( " object " ) || t . hasPrefix ( " fun " ) {
2026-02-20 10:34:22 +00:00
return TOCItem ( id : " kotlin- \( index ) " , title : " \( t ) (Line \( index + 1 ) ) " , line : index + 1 )
2026-02-06 18:59:53 +00:00
}
return nil
}
case " go " :
toc = lines . enumerated ( ) . compactMap { index , line in
let t = line . trimmingCharacters ( in : . whitespaces )
if t . hasPrefix ( " func " ) || t . hasPrefix ( " type " ) {
2026-02-20 10:34:22 +00:00
return TOCItem ( id : " go- \( index ) " , title : " \( t ) (Line \( index + 1 ) ) " , line : index + 1 )
2026-02-06 18:59:53 +00:00
}
return nil
}
case " ruby " :
toc = lines . enumerated ( ) . compactMap { index , line in
let t = line . trimmingCharacters ( in : . whitespaces )
if t . hasPrefix ( " def " ) || t . hasPrefix ( " class " ) || t . hasPrefix ( " module " ) {
2026-02-20 10:34:22 +00:00
return TOCItem ( id : " ruby- \( index ) " , title : " \( t ) (Line \( index + 1 ) ) " , line : index + 1 )
2026-02-06 18:59:53 +00:00
}
return nil
}
case " rust " :
toc = lines . enumerated ( ) . compactMap { index , line in
let t = line . trimmingCharacters ( in : . whitespaces )
if t . hasPrefix ( " fn " ) || t . hasPrefix ( " struct " ) || t . hasPrefix ( " enum " ) || t . hasPrefix ( " impl " ) {
2026-02-20 10:34:22 +00:00
return TOCItem ( id : " rust- \( index ) " , title : " \( t ) (Line \( index + 1 ) ) " , line : index + 1 )
2026-02-06 18:59:53 +00:00
}
return nil
}
case " typescript " :
toc = lines . enumerated ( ) . compactMap { index , line in
let t = line . trimmingCharacters ( in : . whitespaces )
if t . hasPrefix ( " function " ) || t . hasPrefix ( " class " ) || t . hasPrefix ( " interface " ) || t . hasPrefix ( " type " ) {
2026-02-20 10:34:22 +00:00
return TOCItem ( id : " ts- \( index ) " , title : " \( t ) (Line \( index + 1 ) ) " , line : index + 1 )
2026-02-06 18:59:53 +00:00
}
return nil
}
2026-02-07 23:20:47 +00:00
case " php " :
toc = lines . enumerated ( ) . compactMap { index , line in
let t = line . trimmingCharacters ( in : . whitespaces )
if t . hasPrefix ( " function " ) || t . hasPrefix ( " class " ) || t . hasPrefix ( " interface " ) || t . hasPrefix ( " trait " ) {
2026-02-20 10:34:22 +00:00
return TOCItem ( id : " php- \( index ) " , title : " \( t ) (Line \( index + 1 ) ) " , line : index + 1 )
2026-02-07 23:20:47 +00:00
}
return nil
}
2026-02-06 18:59:53 +00:00
case " objective-c " :
toc = lines . enumerated ( ) . compactMap { index , line in
let t = line . trimmingCharacters ( in : . whitespaces )
if t . hasPrefix ( " @interface " ) || t . hasPrefix ( " @implementation " ) || t . contains ( " ) \n { " ) {
2026-02-20 10:34:22 +00:00
return TOCItem ( id : " objc- \( index ) " , title : " \( t ) (Line \( index + 1 ) ) " , line : index + 1 )
2026-02-06 18:59:53 +00:00
}
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 ( " { " ) ) {
2026-02-20 10:34:22 +00:00
return TOCItem ( id : " c- \( index ) " , title : " \( trimmed ) (Line \( index + 1 ) ) " , line : index + 1 )
2026-02-06 18:59:53 +00:00
}
return nil
}
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 {
2026-02-20 10:34:22 +00:00
return TOCItem ( id : " sh- \( index ) " , title : " \( trimmed ) (Line \( index + 1 ) ) " , line : index + 1 )
2026-02-06 18:59:53 +00:00
}
return nil
}
case " powershell " :
toc = lines . enumerated ( ) . compactMap { index , line in
let trimmed = line . trimmingCharacters ( in : . whitespaces )
if trimmed . range ( of : # " ^function \ s+[A-Za-z_][A-Za-z0-9_ \ -]* \ s* \ { " # , options : . regularExpression ) != nil ||
trimmed . hasPrefix ( " param( " ) {
2026-02-20 10:34:22 +00:00
return TOCItem ( id : " ps- \( index ) " , title : " \( trimmed ) (Line \( index + 1 ) ) " , line : index + 1 )
2026-02-06 18:59:53 +00:00
}
return nil
}
2026-02-07 23:20:47 +00:00
case " html " , " css " , " json " , " markdown " , " csv " :
2026-02-06 18:59:53 +00:00
toc = lines . enumerated ( ) . compactMap { index , line in
let trimmed = line . trimmingCharacters ( in : . whitespaces )
if ! trimmed . isEmpty && ( trimmed . hasPrefix ( " # " ) || trimmed . hasPrefix ( " <h " ) ) {
2026-02-20 10:34:22 +00:00
return TOCItem ( id : " markup- \( index ) " , title : " \( trimmed ) (Line \( index + 1 ) ) " , line : index + 1 )
2026-02-06 18:59:53 +00:00
}
return nil
}
case " csharp " :
toc = lines . enumerated ( ) . compactMap { index , line in
let t = line . trimmingCharacters ( in : . whitespaces )
if t . hasPrefix ( " class " ) || t . hasPrefix ( " interface " ) || t . hasPrefix ( " enum " ) || t . contains ( " static void Main( " ) || ( t . contains ( " void " ) && t . contains ( " ( " ) && t . contains ( " ) " ) && t . contains ( " { " ) ) {
2026-02-20 10:34:22 +00:00
return TOCItem ( id : " cs- \( index ) " , title : " \( t ) (Line \( index + 1 ) ) " , line : index + 1 )
2026-02-06 18:59:53 +00:00
}
return nil
}
default :
// F o r u n k n o w n o r s t a n d a r d / p l a i n , s h o w f i r s t n o n - e m p t y l i n e s a s h e a d i n g s
toc = lines . enumerated ( ) . compactMap { index , line in
let t = line . trimmingCharacters ( in : . whitespaces )
2026-02-20 10:34:22 +00:00
if ! t . isEmpty && t . count < 120 {
return TOCItem ( id : " default- \( index ) " , title : " \( t ) (Line \( index + 1 ) ) " , line : index + 1 )
}
2026-02-06 18:59:53 +00:00
return nil
}
}
2026-02-20 10:34:22 +00:00
return toc . isEmpty
? [ TOCItem ( id : " none " , title : " No headers found " , line : nil ) ]
: toc
2026-02-06 18:59:53 +00:00
}
}
struct ProjectStructureSidebarView : View {
2026-03-09 16:47:50 +00:00
private enum SidebarDensity : String , CaseIterable , Identifiable {
case compact
case comfortable
var id : String { rawValue }
}
private struct FileIconStyle {
let symbol : String
let color : Color
}
2026-02-06 18:59:53 +00:00
let rootFolderURL : URL ?
2026-02-19 08:09:35 +00:00
let nodes : [ ProjectTreeNode ]
2026-02-06 18:59:53 +00:00
let selectedFileURL : URL ?
2026-03-08 14:31:01 +00:00
let showSupportedFilesOnly : Bool
2026-02-06 18:59:53 +00:00
let translucentBackgroundEnabled : Bool
2026-03-29 13:57:01 +00:00
let boundaryEdge : HorizontalEdge ?
2026-02-06 18:59:53 +00:00
let onOpenFile : ( ) -> Void
let onOpenFolder : ( ) -> Void
2026-03-08 14:31:01 +00:00
let onToggleSupportedFilesOnly : ( Bool ) -> Void
2026-02-06 18:59:53 +00:00
let onOpenProjectFile : ( URL ) -> Void
let onRefreshTree : ( ) -> Void
2026-04-16 10:37:03 +00:00
let onCreateProjectFile : ( URL ? ) -> Void
let onCreateProjectFolder : ( URL ? ) -> Void
let onRenameProjectItem : ( URL ) -> Void
let onDuplicateProjectItem : ( URL ) -> Void
let onDeleteProjectItem : ( URL ) -> Void
let revealURL : URL ?
2026-02-06 18:59:53 +00:00
@ State private var expandedDirectories : Set < String > = [ ]
2026-04-16 10:37:03 +00:00
@ State private var hoveredNodeID : String ? = nil
2026-02-19 08:09:35 +00:00
@ Environment ( \ . colorScheme ) private var colorScheme
2026-02-19 14:29:53 +00:00
#if os ( macOS )
@ AppStorage ( " SettingsMacTranslucencyMode " ) private var macTranslucencyModeRaw : String = " balanced "
#endif
2026-03-09 16:47:50 +00:00
@ AppStorage ( " SettingsProjectSidebarDensity " ) private var sidebarDensityRaw : String = SidebarDensity . compact . rawValue
@ AppStorage ( " SettingsProjectSidebarAutoCollapseDeep " ) private var autoCollapseDeepFolders : Bool = true
2026-02-18 22:56:46 +00:00
2026-02-06 18:59:53 +00:00
var body : some View {
2026-02-19 14:29:53 +00:00
VStack ( alignment : . leading , spacing : 0 ) {
2026-03-30 17:07:02 +00:00
if showsSidebarActionsRow {
2026-04-16 10:37:03 +00:00
VStack ( alignment : . leading , spacing : isCompactDensity ? 8 : 10 ) {
2026-03-30 17:07:02 +00:00
if showsInlineSidebarTitle {
2026-04-16 10:37:03 +00:00
Text ( NSLocalizedString ( " Project Structure " , comment : " Project structure sidebar title " ) )
2026-03-30 17:07:02 +00:00
. font ( . system ( size : isCompactDensity ? 19 : 20 , weight : . semibold ) )
2026-04-16 10:37:03 +00:00
. lineLimit ( 1 )
. truncationMode ( . tail )
. layoutPriority ( 1 )
2026-03-30 17:07:02 +00:00
}
2026-04-16 10:37:03 +00:00
HStack ( spacing : isCompactDensity ? 10 : 12 ) {
Button ( action : onOpenFolder ) {
Image ( systemName : " folder " )
}
. buttonStyle ( . borderless )
. help ( NSLocalizedString ( " Open Folder… " , comment : " Project sidebar open folder action " ) )
. accessibilityLabel ( NSLocalizedString ( " Open folder " , comment : " Project sidebar open folder accessibility label " ) )
. accessibilityHint ( NSLocalizedString ( " Select a project folder to show in the sidebar " , comment : " Project sidebar open folder accessibility hint " ) )
2026-03-29 13:57:01 +00:00
2026-04-16 10:37:03 +00:00
Button ( action : onOpenFile ) {
Image ( systemName : " doc " )
2026-03-29 13:57:01 +00:00
}
2026-04-16 10:37:03 +00:00
. buttonStyle ( . borderless )
. help ( NSLocalizedString ( " Open File… " , comment : " Project sidebar open file action " ) )
. accessibilityLabel ( NSLocalizedString ( " Open file " , comment : " Project sidebar open file accessibility label " ) )
. accessibilityHint ( NSLocalizedString ( " Opens a file from disk " , comment : " Project sidebar open file accessibility hint " ) )
Menu {
Button {
onCreateProjectFile ( nil )
} label : {
Label ( NSLocalizedString ( " New File " , comment : " Project sidebar create file action " ) , systemImage : " doc.badge.plus " )
}
Button {
onCreateProjectFolder ( nil )
} label : {
Label ( NSLocalizedString ( " New Folder " , comment : " Project sidebar create folder action " ) , systemImage : " folder.badge.plus " )
}
} label : {
Image ( systemName : " plus " )
2026-03-29 13:57:01 +00:00
}
2026-04-16 10:37:03 +00:00
. buttonStyle ( . borderless )
. help ( NSLocalizedString ( " Create in Project Root " , comment : " Project sidebar create action " ) )
. accessibilityLabel ( NSLocalizedString ( " Create project item " , comment : " Project sidebar create accessibility label " ) )
. accessibilityHint ( NSLocalizedString ( " Creates a new file or folder in the project root " , comment : " Project sidebar create accessibility hint " ) )
Button ( action : onRefreshTree ) {
Image ( systemName : " arrow.clockwise " )
2026-03-29 13:57:01 +00:00
}
2026-04-16 10:37:03 +00:00
. buttonStyle ( . borderless )
. help ( NSLocalizedString ( " Refresh Folder Tree " , comment : " Project sidebar refresh tree action " ) )
. accessibilityLabel ( NSLocalizedString ( " Refresh project tree " , comment : " Project sidebar refresh accessibility label " ) )
. accessibilityHint ( NSLocalizedString ( " Reloads files and folders from disk " , comment : " Project sidebar refresh accessibility hint " ) )
Menu {
Button {
onToggleSupportedFilesOnly ( ! showSupportedFilesOnly )
} label : {
Label (
NSLocalizedString ( " Show Supported Files Only " , comment : " Project sidebar supported files filter label " ) ,
systemImage : showSupportedFilesOnly ? " checkmark.circle.fill " : " circle "
)
}
Divider ( )
Picker ( NSLocalizedString ( " Density " , comment : " Project sidebar density picker label " ) , selection : $ sidebarDensityRaw ) {
Text ( NSLocalizedString ( " Compact " , comment : " Project sidebar compact density " ) ) . tag ( SidebarDensity . compact . rawValue )
Text ( NSLocalizedString ( " Comfortable " , comment : " Project sidebar comfortable density " ) ) . tag ( SidebarDensity . comfortable . rawValue )
}
Toggle ( NSLocalizedString ( " Auto-collapse Deep Folders " , comment : " Project sidebar auto-collapse deep folders toggle " ) , isOn : $ autoCollapseDeepFolders )
Divider ( )
Button ( NSLocalizedString ( " Expand All " , comment : " Project sidebar expand all action " ) ) {
expandAllDirectories ( )
}
Button ( NSLocalizedString ( " Collapse All " , comment : " Project sidebar collapse all action " ) ) {
collapseAllDirectories ( )
}
} label : {
Image ( systemName : " arrow.up.arrow.down.circle " )
2026-03-29 13:57:01 +00:00
}
2026-04-16 10:37:03 +00:00
. buttonStyle ( . borderless )
. help ( NSLocalizedString ( " Expand or Collapse All " , comment : " Project sidebar expand/collapse help " ) )
. accessibilityLabel ( NSLocalizedString ( " Expand or collapse all folders " , comment : " Project sidebar expand/collapse accessibility label " ) )
. accessibilityHint ( NSLocalizedString ( " Expands or collapses all folders in the project tree " , comment : " Project sidebar expand/collapse accessibility hint " ) )
Spacer ( minLength : 0 )
2026-03-08 14:31:01 +00:00
}
}
2026-03-29 13:57:01 +00:00
. padding ( . horizontal , headerHorizontalPadding )
. padding ( . top , headerTopPadding )
. padding ( . bottom , headerBottomPadding )
2026-02-19 14:29:53 +00:00
#if os ( macOS )
2026-03-29 13:57:01 +00:00
. background ( sidebarHeaderFill )
2026-02-19 14:29:53 +00:00
#endif
2026-03-29 13:57:01 +00:00
}
2026-02-06 18:59:53 +00:00
if let rootFolderURL {
Text ( rootFolderURL . path )
2026-03-09 16:47:50 +00:00
. font ( . system ( size : isCompactDensity ? 11 : 12 ) )
2026-02-06 18:59:53 +00:00
. foregroundStyle ( . secondary )
2026-03-09 16:47:50 +00:00
. lineLimit ( isCompactDensity ? 1 : 2 )
2026-02-06 18:59:53 +00:00
. textSelection ( . enabled )
2026-04-16 10:37:03 +00:00
. contextMenu {
Button {
onCreateProjectFile ( rootFolderURL )
} label : {
Label ( NSLocalizedString ( " New File " , comment : " Project sidebar create file action " ) , systemImage : " doc.badge.plus " )
}
Button {
onCreateProjectFolder ( rootFolderURL )
} label : {
Label ( NSLocalizedString ( " New Folder " , comment : " Project sidebar create folder action " ) , systemImage : " folder.badge.plus " )
}
}
2026-03-09 16:47:50 +00:00
. padding ( . horizontal , headerHorizontalPadding )
2026-03-30 17:07:02 +00:00
. padding ( . top , showsSidebarActionsRow ? 0 : headerTopPadding )
2026-03-29 13:57:01 +00:00
. padding ( . bottom , headerPathBottomPadding )
2026-02-06 18:59:53 +00:00
}
List {
if rootFolderURL = = nil {
2026-04-16 10:37:03 +00:00
Text ( NSLocalizedString ( " No folder selected " , comment : " Project sidebar empty state without root folder " ) )
2026-02-06 18:59:53 +00:00
. foregroundColor ( . secondary )
2026-02-19 14:29:53 +00:00
. listRowBackground ( Color . clear )
. listRowSeparator ( . hidden )
2026-02-06 18:59:53 +00:00
} else if nodes . isEmpty {
2026-04-16 10:37:03 +00:00
Text ( NSLocalizedString ( " Folder is empty " , comment : " Project sidebar empty state for selected folder " ) )
2026-02-06 18:59:53 +00:00
. foregroundColor ( . secondary )
2026-02-19 14:29:53 +00:00
. listRowBackground ( Color . clear )
. listRowSeparator ( . hidden )
2026-02-06 18:59:53 +00:00
} else {
ForEach ( nodes ) { node in
projectNodeView ( node , level : 0 )
}
}
}
2026-02-19 14:29:53 +00:00
. listStyle ( platformListStyle )
2026-02-06 18:59:53 +00:00
. scrollContentBackground ( . hidden )
2026-02-19 14:29:53 +00:00
. background ( Color . clear )
2026-04-16 10:37:03 +00:00
. contextMenu {
if let rootFolderURL {
Button {
onCreateProjectFile ( rootFolderURL )
} label : {
Label ( NSLocalizedString ( " New File " , comment : " Project sidebar create file action " ) , systemImage : " doc.badge.plus " )
}
Button {
onCreateProjectFolder ( rootFolderURL )
} label : {
Label ( NSLocalizedString ( " New Folder " , comment : " Project sidebar create folder action " ) , systemImage : " folder.badge.plus " )
}
}
}
2026-02-06 18:59:53 +00:00
}
2026-02-19 14:29:53 +00:00
. padding ( sidebarOuterPadding )
2026-03-29 13:57:01 +00:00
. background ( sidebarContainerShape . fill ( sidebarSurfaceFill ) )
. overlay ( sidebarContainerBorderOverlay )
. clipShape ( sidebarContainerShape )
2026-04-16 10:37:03 +00:00
. onAppear {
revealTargetIfNeeded ( )
}
. onChange ( of : revealPath ) { _ , _ in
revealTargetIfNeeded ( )
}
. onChange ( of : nodes . count ) { _ , _ in
revealTargetIfNeeded ( )
}
2026-02-19 14:29:53 +00:00
#if os ( macOS )
2026-03-29 13:57:01 +00:00
. overlay ( alignment : boundaryEdge = = . leading ? . leading : . trailing ) {
if boundaryEdge != nil {
Rectangle ( )
. fill ( sidebarSeparatorColor )
. frame ( width : 1 )
}
2026-02-19 14:29:53 +00:00
}
#endif
}
private var sidebarSurfaceFill : AnyShapeStyle {
if translucentBackgroundEnabled {
#if os ( macOS )
let mode = MacTranslucencyMode ( rawValue : macTranslucencyModeRaw ) ? ? . balanced
return AnyShapeStyle ( mode . material . opacity ( mode . opacity ) )
#else
return AnyShapeStyle ( . ultraThinMaterial )
#endif
}
#if os ( macOS )
2026-03-26 18:19:45 +00:00
return AnyShapeStyle ( currentEditorTheme ( colorScheme : colorScheme ) . background )
2026-02-19 14:29:53 +00:00
#else
2026-03-26 18:19:45 +00:00
return AnyShapeStyle ( currentEditorTheme ( colorScheme : colorScheme ) . background )
2026-02-19 14:29:53 +00:00
#endif
}
private var sidebarSurfaceStroke : Color {
colorScheme = = . dark
? Color . white . opacity ( 0.12 )
: Color . black . opacity ( 0.08 )
}
private var sidebarHeaderFill : AnyShapeStyle {
translucentBackgroundEnabled ? sidebarSurfaceFill : AnyShapeStyle ( Color . clear )
}
private var sidebarSeparatorColor : Color {
#if os ( macOS )
2026-03-29 13:57:01 +00:00
colorScheme = = . dark ? Color . white . opacity ( 0.14 ) : Color . black . opacity ( 0.10 )
2026-02-19 14:29:53 +00:00
#else
Color . black . opacity ( 0.1 )
#endif
}
private var sidebarCornerRadius : CGFloat {
#if os ( macOS )
2026-03-29 13:57:01 +00:00
18
2026-02-19 14:29:53 +00:00
#else
14
#endif
}
2026-03-29 13:57:01 +00:00
private var sidebarContainerShape : AnyShape {
#if os ( macOS )
AnyShape ( Rectangle ( ) )
#elseif os ( iOS )
if UIDevice . current . userInterfaceIdiom = = . pad {
AnyShape ( Rectangle ( ) )
} else {
AnyShape (
UnevenRoundedRectangle (
topLeadingRadius : 0 ,
bottomLeadingRadius : sidebarCornerRadius ,
bottomTrailingRadius : sidebarCornerRadius ,
topTrailingRadius : 0 ,
style : . continuous
)
)
}
#else
AnyShape (
UnevenRoundedRectangle (
topLeadingRadius : 0 ,
bottomLeadingRadius : sidebarCornerRadius ,
bottomTrailingRadius : sidebarCornerRadius ,
topTrailingRadius : 0 ,
style : . continuous
)
)
#endif
}
@ ViewBuilder
private var sidebarContainerBorderOverlay : some View {
#if os ( macOS )
EmptyView ( )
#elseif os ( iOS )
if UIDevice . current . userInterfaceIdiom != . pad {
sidebarContainerShape . stroke ( sidebarSurfaceStroke , lineWidth : 1 )
}
#else
sidebarContainerShape . stroke ( sidebarSurfaceStroke , lineWidth : 1 )
#endif
}
2026-02-19 14:29:53 +00:00
private var sidebarOuterPadding : CGFloat {
#if os ( iOS )
2026-03-29 13:57:01 +00:00
UIDevice . current . userInterfaceIdiom = = . pad ? 0 : 10
2026-02-19 14:29:53 +00:00
#else
0
#endif
}
private var platformListStyle : some ListStyle {
#if os ( iOS )
PlainListStyle ( )
#else
PlainListStyle ( )
#endif
2026-02-06 18:59:53 +00:00
}
2026-03-08 14:31:01 +00:00
private func expandAllDirectories ( ) {
2026-03-09 16:47:50 +00:00
expandedDirectories = allDirectoryNodeIDs ( in : nodes , level : 0 )
2026-03-08 14:31:01 +00:00
}
private func collapseAllDirectories ( ) {
expandedDirectories . removeAll ( )
}
2026-03-09 16:47:50 +00:00
private func allDirectoryNodeIDs ( in treeNodes : [ ProjectTreeNode ] , level : Int ) -> Set < String > {
2026-03-08 14:31:01 +00:00
var result : Set < String > = [ ]
for node in treeNodes where node . isDirectory {
2026-03-09 16:47:50 +00:00
let shouldInclude = ! autoCollapseDeepFolders || level < 2
if shouldInclude {
result . insert ( node . id )
}
result . formUnion ( allDirectoryNodeIDs ( in : node . children , level : level + 1 ) )
2026-03-08 14:31:01 +00:00
}
return result
}
2026-02-06 18:59:53 +00:00
private func projectNodeView ( _ node : ProjectTreeNode , level : Int ) -> AnyView {
if node . isDirectory {
2026-04-16 10:37:03 +00:00
let isHovered = hoveredNodeID = = node . id
2026-02-06 18:59:53 +00:00
return AnyView (
DisclosureGroup ( isExpanded : Binding (
get : { expandedDirectories . contains ( node . id ) } ,
set : { isExpanded in
if isExpanded {
expandedDirectories . insert ( node . id )
} else {
expandedDirectories . remove ( node . id )
}
}
) ) {
ForEach ( node . children ) { child in
projectNodeView ( child , level : level + 1 )
}
} label : {
2026-03-29 13:57:01 +00:00
HStack ( spacing : directoryRowContentSpacing ) {
2026-03-09 16:47:50 +00:00
Image ( systemName : " folder " )
2026-03-29 13:57:01 +00:00
. foregroundStyle ( Color . accentColor )
2026-03-09 16:47:50 +00:00
. symbolRenderingMode ( . hierarchical )
Text ( node . url . lastPathComponent )
. lineLimit ( 1 )
}
2026-03-29 13:57:01 +00:00
. font ( rowFont )
2026-03-09 16:47:50 +00:00
. padding ( . vertical , rowVerticalPadding )
2026-03-29 13:57:01 +00:00
. padding ( . trailing , rowHorizontalPadding )
. padding ( . leading , directoryRowContentLeadingPadding )
. frame ( maxWidth : . infinity , alignment : . leading )
2026-04-16 10:37:03 +00:00
. background ( rowChrome ( isSelected : false , isHovered : isHovered ) )
. contentShape ( Rectangle ( ) )
. accessibilityElement ( children : . combine )
. accessibilityLabel (
Text (
String (
format : NSLocalizedString ( " Folder %@ " , comment : " Project sidebar folder accessibility label " ) ,
node . url . lastPathComponent
)
)
)
2026-02-06 18:59:53 +00:00
}
2026-03-29 13:57:01 +00:00
. padding ( . leading , directoryRowLeadingInset ( for : level ) )
2026-03-09 16:47:50 +00:00
. listRowInsets ( rowInsets )
2026-02-06 18:59:53 +00:00
. listRowBackground ( Color . clear )
2026-02-19 14:29:53 +00:00
. listRowSeparator ( . hidden )
2026-04-16 10:37:03 +00:00
. contextMenu {
Button {
onCreateProjectFile ( node . url )
} label : {
Label ( NSLocalizedString ( " New File " , comment : " Project sidebar create file action " ) , systemImage : " doc.badge.plus " )
}
Button {
onCreateProjectFolder ( node . url )
} label : {
Label ( NSLocalizedString ( " New Folder " , comment : " Project sidebar create folder action " ) , systemImage : " folder.badge.plus " )
}
Divider ( )
Button {
onRenameProjectItem ( node . url )
} label : {
Label ( NSLocalizedString ( " Rename " , comment : " Project sidebar rename action " ) , systemImage : " pencil " )
}
Button {
onDuplicateProjectItem ( node . url )
} label : {
Label ( NSLocalizedString ( " Duplicate " , comment : " Project sidebar duplicate action " ) , systemImage : " plus.square.on.square " )
}
Divider ( )
Button ( role : . destructive ) {
onDeleteProjectItem ( node . url )
} label : {
Label ( NSLocalizedString ( " Delete " , comment : " Project sidebar delete action " ) , systemImage : " trash " )
}
}
#if os ( macOS )
. onHover { hovering in
if hovering {
hoveredNodeID = node . id
} else if hoveredNodeID = = node . id {
hoveredNodeID = nil
}
}
#endif
2026-02-06 18:59:53 +00:00
)
} else {
2026-03-09 16:47:50 +00:00
let style = fileIconStyle ( for : node . url )
2026-03-29 13:57:01 +00:00
let isSelected = selectedFileURL ? . standardizedFileURL = = node . url . standardizedFileURL
2026-04-16 10:37:03 +00:00
let isHovered = hoveredNodeID = = node . id
2026-02-06 18:59:53 +00:00
return AnyView (
Button {
onOpenProjectFile ( node . url )
} label : {
HStack ( spacing : 8 ) {
2026-03-09 16:47:50 +00:00
Image ( systemName : style . symbol )
. foregroundStyle ( style . color )
. symbolRenderingMode ( . hierarchical )
2026-02-06 18:59:53 +00:00
Text ( node . url . lastPathComponent )
. lineLimit ( 1 )
Spacer ( )
2026-03-29 13:57:01 +00:00
if isSelected {
Image ( systemName : " circle.fill " )
. font ( . system ( size : 7 , weight : . semibold ) )
. foregroundColor ( . white . opacity ( colorScheme = = . dark ? 0.92 : 0.98 ) )
2026-02-06 18:59:53 +00:00
}
}
2026-03-29 13:57:01 +00:00
. font ( rowFont )
. foregroundStyle ( isSelected ? rowSelectedForegroundColor : Color . primary )
2026-03-09 16:47:50 +00:00
. padding ( . vertical , rowVerticalPadding )
2026-03-29 13:57:01 +00:00
. padding ( . horizontal , rowHorizontalPadding )
. frame ( maxWidth : . infinity , alignment : . leading )
2026-04-16 10:37:03 +00:00
. background ( rowChrome ( isSelected : isSelected , isHovered : isHovered ) )
. contentShape ( Rectangle ( ) )
2026-02-06 18:59:53 +00:00
}
. buttonStyle ( . plain )
2026-03-09 16:47:50 +00:00
. padding ( . leading , CGFloat ( level ) * levelIndent )
. listRowInsets ( rowInsets )
2026-02-06 18:59:53 +00:00
. listRowBackground ( Color . clear )
2026-02-19 14:29:53 +00:00
. listRowSeparator ( . hidden )
2026-04-16 10:37:03 +00:00
. contextMenu {
Button {
onCreateProjectFile ( node . url . deletingLastPathComponent ( ) )
} label : {
Label ( NSLocalizedString ( " New File Here " , comment : " Project sidebar create file in same directory action " ) , systemImage : " doc.badge.plus " )
}
Button {
onCreateProjectFolder ( node . url . deletingLastPathComponent ( ) )
} label : {
Label ( NSLocalizedString ( " New Folder Here " , comment : " Project sidebar create folder in same directory action " ) , systemImage : " folder.badge.plus " )
}
Divider ( )
Button {
onRenameProjectItem ( node . url )
} label : {
Label ( NSLocalizedString ( " Rename " , comment : " Project sidebar rename action " ) , systemImage : " pencil " )
}
Button {
onDuplicateProjectItem ( node . url )
} label : {
Label ( NSLocalizedString ( " Duplicate " , comment : " Project sidebar duplicate action " ) , systemImage : " plus.square.on.square " )
}
Divider ( )
Button ( role : . destructive ) {
onDeleteProjectItem ( node . url )
} label : {
Label ( NSLocalizedString ( " Delete " , comment : " Project sidebar delete action " ) , systemImage : " trash " )
}
}
. accessibilityLabel (
Text (
String (
format : NSLocalizedString ( " File %@ " , comment : " Project sidebar file accessibility label " ) ,
node . url . lastPathComponent
)
)
)
#if os ( macOS )
. onHover { hovering in
if hovering {
hoveredNodeID = node . id
} else if hoveredNodeID = = node . id {
hoveredNodeID = nil
}
}
#endif
2026-02-06 18:59:53 +00:00
)
}
}
2026-03-09 16:47:50 +00:00
private var sidebarDensity : SidebarDensity {
SidebarDensity ( rawValue : sidebarDensityRaw ) ? ? . compact
}
private var isCompactDensity : Bool { sidebarDensity = = . compact }
private var levelIndent : CGFloat {
2026-04-16 10:37:03 +00:00
isCompactDensity ? 10 : 13
2026-03-09 16:47:50 +00:00
}
private var rowVerticalPadding : CGFloat {
2026-04-16 10:37:03 +00:00
isCompactDensity ? 7 : 9
2026-03-29 13:57:01 +00:00
}
private var rowHorizontalPadding : CGFloat {
2026-04-16 10:37:03 +00:00
isCompactDensity ? 10 : 13
2026-03-29 13:57:01 +00:00
}
private var directoryRowContentSpacing : CGFloat {
#if os ( macOS )
isCompactDensity ? 4 : 5
#else
isCompactDensity ? 6 : 7
#endif
}
private var directoryRowContentLeadingPadding : CGFloat {
#if os ( macOS )
2026-04-16 10:37:03 +00:00
isCompactDensity ? 1 : 2
2026-03-29 13:57:01 +00:00
#else
2026-04-16 10:37:03 +00:00
isCompactDensity ? 4 : 5
2026-03-29 13:57:01 +00:00
#endif
2026-03-09 16:47:50 +00:00
}
private var headerHorizontalPadding : CGFloat {
2026-03-29 13:57:01 +00:00
isCompactDensity ? 16 : 18
2026-03-09 16:47:50 +00:00
}
private var headerTopPadding : CGFloat {
2026-03-29 13:57:01 +00:00
isCompactDensity ? 16 : 18
2026-03-09 16:47:50 +00:00
}
private var headerBottomPadding : CGFloat {
2026-03-29 13:57:01 +00:00
isCompactDensity ? 10 : 12
}
private var headerPathBottomPadding : CGFloat {
isCompactDensity ? 10 : 12
2026-03-09 16:47:50 +00:00
}
private var rowInsets : EdgeInsets {
2026-04-16 10:37:03 +00:00
EdgeInsets ( top : 3 , leading : isCompactDensity ? 11 : 13 , bottom : 3 , trailing : isCompactDensity ? 10 : 12 )
2026-03-29 13:57:01 +00:00
}
2026-03-30 17:07:02 +00:00
private var showsInlineSidebarTitle : Bool {
2026-03-29 13:57:01 +00:00
#if os ( iOS )
UIDevice . current . userInterfaceIdiom != . phone
#else
true
#endif
}
2026-03-30 17:07:02 +00:00
private var showsSidebarActionsRow : Bool {
true
}
2026-03-29 13:57:01 +00:00
private func directoryRowLeadingInset ( for level : Int ) -> CGFloat {
let baseInset : CGFloat
#if os ( macOS )
2026-04-16 10:37:03 +00:00
baseInset = level = = 0 ? ( isCompactDensity ? 12 : 14 ) : 0
2026-03-29 13:57:01 +00:00
#else
2026-04-16 10:37:03 +00:00
baseInset = level = = 0 ? ( isCompactDensity ? 8 : 10 ) : 0
2026-03-29 13:57:01 +00:00
#endif
return baseInset + CGFloat ( level ) * levelIndent
}
private var rowFont : Font {
. system ( size : isCompactDensity ? 13 : 14 , weight : . medium )
}
private var rowSelectedForegroundColor : Color {
colorScheme = = . dark ? . white : . primary
}
2026-04-16 10:37:03 +00:00
private func rowChrome ( isSelected : Bool , isHovered : Bool ) -> some View {
2026-03-29 13:57:01 +00:00
RoundedRectangle ( cornerRadius : isCompactDensity ? 12 : 14 , style : . continuous )
2026-04-16 10:37:03 +00:00
. fill ( rowFill ( isSelected : isSelected , isHovered : isHovered ) )
}
private func rowFill ( isSelected : Bool , isHovered : Bool ) -> Color {
if isSelected { return selectedRowFill }
if isHovered { return hoveredRowFill }
return unselectedRowFill
2026-03-29 13:57:01 +00:00
}
private var selectedRowFill : Color {
if colorScheme = = . dark {
2026-04-16 10:37:03 +00:00
return Color . accentColor . opacity ( 0.48 )
2026-03-29 13:57:01 +00:00
}
2026-04-16 10:37:03 +00:00
return Color . accentColor . opacity ( 0.22 )
}
private var hoveredRowFill : Color {
if colorScheme = = . dark {
return Color . white . opacity ( 0.08 )
}
return Color . black . opacity ( 0.07 )
2026-03-29 13:57:01 +00:00
}
private var unselectedRowFill : Color {
2026-04-16 10:37:03 +00:00
colorScheme = = . dark ? Color . white . opacity ( 0.028 ) : Color . black . opacity ( 0.024 )
}
private var revealPath : String ? {
revealURL ? . standardizedFileURL . path
}
private func revealTargetIfNeeded ( ) {
guard let revealPath else { return }
guard let pathIDs = directoryPathIDs ( for : revealPath , in : nodes ) else { return }
expandedDirectories . formUnion ( pathIDs )
}
private func directoryPathIDs ( for targetPath : String , in treeNodes : [ ProjectTreeNode ] ) -> [ String ] ? {
for node in treeNodes {
if let path = directoryPathIDs ( for : targetPath , node : node ) {
return path
}
}
return nil
}
private func directoryPathIDs ( for targetPath : String , node : ProjectTreeNode ) -> [ String ] ? {
let nodePath = node . url . standardizedFileURL . path
if nodePath = = targetPath {
return node . isDirectory ? [ node . id ] : [ ]
}
guard node . isDirectory else { return nil }
for child in node . children {
if let childPath = directoryPathIDs ( for : targetPath , node : child ) {
return [ node . id ] + childPath
}
}
return nil
2026-03-09 16:47:50 +00:00
}
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 )
2026-03-15 17:51:17 +00:00
case " tex " , " latex " , " bib " , " sty " , " cls " :
return . init ( symbol : " text.book.closed " , color : . indigo )
2026-03-09 16:47:50 +00:00
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 )
}
}
2026-02-06 18:59:53 +00:00
}
struct ProjectTreeNode : Identifiable {
let url : URL
let isDirectory : Bool
var children : [ ProjectTreeNode ]
var id : String { url . path }
}