import SwiftUI // MARK: - File Tree View struct FileTreeView: View { @ObservedObject var workspaceService: WorkspaceService @ObservedObject var viewModel: EditorViewModel @State private var contextMenuNode: FileNode? @State private var newItemName: String = "" @State private var isCreatingFile: Bool = false @State private var isCreatingFolder: Bool = false @State private var renamingNode: FileNode? var body: some View { VStack(spacing: 0) { treeHeader Divider() if workspaceService.isLoading { loadingView } else if workspaceService.fileTree.isEmpty { emptyView } else { fileList } } .background(viewModel.currentTheme.sidebarBackground) } // MARK: - Header private var treeHeader: some View { HStack(spacing: 6) { Image(systemName: "folder.fill") .foregroundColor(.blue) .font(.caption) Text(workspaceService.currentWorkspace?.name.uppercased() ?? "EXPLORER") .font(.system(size: 10, weight: .bold)) .foregroundColor(.gray) .lineLimit(1) Spacer() Button(action: { isCreatingFile = true }) { Image(systemName: "doc.badge.plus") .font(.caption) .foregroundColor(.gray) } .buttonStyle(.plain) .help("New File") Button(action: { isCreatingFolder = true }) { Image(systemName: "folder.badge.plus") .font(.caption) .foregroundColor(.gray) } .buttonStyle(.plain) .help("New Folder") Button(action: workspaceService.refreshFileTree) { Image(systemName: "arrow.clockwise") .font(.caption) .foregroundColor(.gray) } .buttonStyle(.plain) .help("Refresh") } .padding(.horizontal, 10) .padding(.vertical, 6) } // MARK: - File List private var fileList: some View { ScrollView { LazyVStack(alignment: .leading, spacing: 0) { // New item input if isCreatingFile || isCreatingFolder { newItemRow } ForEach(workspaceService.fileTree) { node in fileNodeView(node) } } .padding(.vertical, 2) } } // MARK: - Node View (recursive) @ViewBuilder private func fileNodeView(_ node: FileNode) -> some View { fileNodeRow(node) if node.isDirectory && node.isExpanded, let children = node.children { ForEach(children) { child in AnyView(fileNodeView(child)) } } } private func fileNodeRow(_ node: FileNode) -> some View { let isSelected = viewModel.activeTab?.url == node.url let indent = CGFloat(node.depth) * 16 + 8 return HStack(spacing: 4) { if node.isDirectory { Image(systemName: node.isExpanded ? "chevron.down" : "chevron.right") .font(.system(size: 8, weight: .bold)) .foregroundColor(.gray) .frame(width: 10) } else { Spacer().frame(width: 10) } Image(systemName: node.iconName) .font(.system(size: 11)) .foregroundColor(colorForName(node.iconColor)) .frame(width: 14) if renamingNode?.id == node.id { TextField("", text: $newItemName) .font(.system(size: 11, design: .monospaced)) .textFieldStyle(.plain) .onSubmit { commitRename(node) } .onAppear { newItemName = node.name } } else { Text(node.name) .font(.system(size: 11)) .foregroundColor(isSelected ? .white : .primary.opacity(0.85)) .lineLimit(1) } Spacer() if let gitStatus = node.gitStatus { Text(gitStatus.rawValue) .font(.system(size: 9, weight: .bold, design: .monospaced)) .foregroundColor(colorForName(gitStatus.color)) } } .padding(.leading, indent) .padding(.trailing, 8) .padding(.vertical, 3) .background(isSelected ? VSC.listActive : Color.clear) .contentShape(Rectangle()) .onTapGesture { if node.isDirectory { workspaceService.toggleExpansion(node) } else { viewModel.openFile(at: node.url) } } .contextMenu { nodeContextMenu(node) } } // MARK: - Context Menu @ViewBuilder private func nodeContextMenu(_ node: FileNode) -> some View { if node.isDirectory { Button("New File...") { contextMenuNode = node isCreatingFile = true } Button("New Folder...") { contextMenuNode = node isCreatingFolder = true } Divider() } Button("Rename...") { renamingNode = node newItemName = node.name } Button("Delete") { _ = workspaceService.deleteItem(node) workspaceService.refreshFileTree() } Divider() Button("Reveal in Finder") { NSWorkspace.shared.selectFile(node.url.path, inFileViewerRootedAtPath: node.url.deletingLastPathComponent().path) } Button("Copy Path") { NSPasteboard.general.clearContents() NSPasteboard.general.setString(node.url.path, forType: .string) } if !node.isDirectory { Divider() Button("Open in New Tab") { viewModel.openFile(at: node.url) } } } // MARK: - New Item Row private var newItemRow: some View { HStack(spacing: 4) { Image(systemName: isCreatingFolder ? "folder.badge.plus" : "doc.badge.plus") .font(.system(size: 11)) .foregroundColor(isCreatingFolder ? .blue : .orange) TextField(isCreatingFolder ? "New folder name..." : "New file name...", text: $newItemName) .font(.system(size: 11, design: .monospaced)) .textFieldStyle(.plain) .onSubmit { commitNewItem() } Button(action: { isCreatingFile = false; isCreatingFolder = false; newItemName = "" }) { Image(systemName: "xmark.circle.fill") .font(.caption) .foregroundColor(.gray) } .buttonStyle(.plain) } .padding(.horizontal, 28) .padding(.vertical, 3) .background(Color.accentColor.opacity(0.1)) } // MARK: - Actions private func commitNewItem() { guard !newItemName.isEmpty else { return } if isCreatingFolder { if workspaceService.createFolder(name: newItemName, in: contextMenuNode) != nil { viewModel.notify(.success("Created folder: \(newItemName)")) } else { viewModel.notify(.error("Failed to create folder: \(newItemName)")) } } else { if let fileURL = workspaceService.createFile(name: newItemName, in: contextMenuNode) { viewModel.notify(.success("Created file: \(newItemName)")) workspaceService.refreshFileTree() viewModel.openFile(at: fileURL) } else { viewModel.notify(.error("Failed to create file: \(newItemName)")) } } isCreatingFile = false isCreatingFolder = false newItemName = "" contextMenuNode = nil workspaceService.refreshFileTree() } private func commitRename(_ node: FileNode) { guard !newItemName.isEmpty, newItemName != node.name else { renamingNode = nil return } _ = workspaceService.renameItem(node, to: newItemName) renamingNode = nil newItemName = "" workspaceService.refreshFileTree() } // MARK: - Helpers private var loadingView: some View { VStack { Spacer() ProgressView() .scaleEffect(0.8) Text("Loading...") .font(.caption) .foregroundColor(.gray) Spacer() } .frame(maxWidth: .infinity) } private var emptyView: some View { VStack(spacing: 8) { Spacer() Image(systemName: "folder.badge.questionmark") .font(.largeTitle) .foregroundColor(.gray.opacity(0.3)) Text("No workspace open") .font(.caption) .foregroundColor(.gray) Button("Open Folder...") { workspaceService.openWorkspaceFromDialog() } .controlSize(.small) Spacer() } .frame(maxWidth: .infinity) } private func colorForName(_ name: String) -> Color { switch name { case "orange": return .orange case "blue": return .blue case "purple": return .purple case "cyan": return .cyan case "teal": return .teal case "yellow": return .yellow case "green": return .green case "red": return .red case "gray": return .gray default: return .primary } } } // MARK: - Git Sidebar View struct GitSidebarView: View { @ObservedObject var gitService: GitService @ObservedObject var viewModel: EditorViewModel @State private var commitMessage: String = "" @State private var showingLog: Bool = false @State private var logEntries: [GitLogEntry] = [] var body: some View { VStack(spacing: 0) { gitHeader Divider() if !gitService.isGitRepo { noRepoView } else { gitContent } } .background(viewModel.currentTheme.sidebarBackground) } // MARK: - Header private var gitHeader: some View { HStack(spacing: 6) { Image(systemName: "point.3.filled.connected.trianglepath.dotted") .foregroundColor(.orange) .font(.caption) Text("SOURCE CONTROL") .font(.system(size: 10, weight: .bold)) .foregroundColor(.gray) Spacer() if gitService.isRefreshing { ProgressView() .scaleEffect(0.5) .frame(width: 14, height: 14) } Button(action: { Task { await gitService.refresh() } }) { Image(systemName: "arrow.clockwise") .font(.caption) .foregroundColor(.gray) } .buttonStyle(.plain) } .padding(.horizontal, 10) .padding(.vertical, 6) } // MARK: - Git Content private var gitContent: some View { ScrollView { VStack(alignment: .leading, spacing: 12) { // Branch info branchSection // Commit box commitSection // Changed files if !gitService.status.files.isEmpty { changesSection } // Log toggle logSection } .padding(10) } } private var branchSection: some View { HStack(spacing: 6) { Image(systemName: "arrow.triangle.branch") .font(.caption) .foregroundColor(.green) Text(gitService.status.branch) .font(.system(size: 12, weight: .medium, design: .monospaced)) .foregroundColor(.white) if gitService.status.ahead > 0 { HStack(spacing: 1) { Image(systemName: "arrow.up") .font(.system(size: 8)) Text("\(gitService.status.ahead)") .font(.system(size: 9)) } .foregroundColor(.green) } if gitService.status.behind > 0 { HStack(spacing: 1) { Image(systemName: "arrow.down") .font(.system(size: 8)) Text("\(gitService.status.behind)") .font(.system(size: 9)) } .foregroundColor(.orange) } } } private var commitSection: some View { VStack(spacing: 6) { TextField("Commit message...", text: $commitMessage, axis: .vertical) .font(.system(size: 11)) .textFieldStyle(.roundedBorder) .lineLimit(3) HStack(spacing: 6) { Button(action: { Task { await gitService.stageAll() _ = await gitService.commit(message: commitMessage) commitMessage = "" } }) { HStack(spacing: 3) { Image(systemName: "checkmark") .font(.system(size: 9)) Text("Commit All") .font(.system(size: 10, weight: .medium)) } } .controlSize(.small) .disabled(commitMessage.isEmpty) Button(action: { Task { await gitService.stageAll() } }) { HStack(spacing: 3) { Image(systemName: "plus") .font(.system(size: 9)) Text("Stage All") .font(.system(size: 10)) } } .controlSize(.small) } } } private var changesSection: some View { VStack(alignment: .leading, spacing: 4) { HStack { Text("CHANGES (\(gitService.status.files.count))") .font(.system(size: 9, weight: .bold)) .foregroundColor(.gray) Spacer() } ForEach(gitService.status.files) { file in HStack(spacing: 6) { Text(file.status.rawValue) .font(.system(size: 9, weight: .bold, design: .monospaced)) .foregroundColor(colorForStatus(file.status)) .frame(width: 14) if file.staged { Image(systemName: "circle.fill") .font(.system(size: 4)) .foregroundColor(.green) } Text(file.path) .font(.system(size: 11, design: .monospaced)) .foregroundColor(.white.opacity(0.85)) .lineLimit(1) .truncationMode(.middle) Spacer() } .padding(.vertical, 1) .contextMenu { Button("Stage") { Task { await gitService.stageFile(file.path) } } Button("Unstage") { Task { await gitService.unstageFile(file.path) } } Button("Discard Changes") { Task { await gitService.discardChanges(file.path) } } } } } } private var logSection: some View { VStack(alignment: .leading, spacing: 4) { Button(action: { showingLog.toggle() if showingLog { Task { logEntries = await gitService.log(count: 10) } } }) { HStack { Image(systemName: showingLog ? "chevron.down" : "chevron.right") .font(.system(size: 8)) Text("HISTORY") .font(.system(size: 9, weight: .bold)) } .foregroundColor(.gray) } .buttonStyle(.plain) if showingLog { ForEach(logEntries) { entry in HStack(spacing: 6) { Text(entry.hash) .font(.system(size: 9, design: .monospaced)) .foregroundColor(.yellow.opacity(0.7)) Text(entry.message) .font(.system(size: 10)) .foregroundColor(.white.opacity(0.8)) .lineLimit(1) } .padding(.vertical, 1) } } } } // MARK: - Helpers private var noRepoView: some View { VStack(spacing: 8) { Spacer() Image(systemName: "point.3.filled.connected.trianglepath.dotted") .font(.largeTitle) .foregroundColor(.gray.opacity(0.3)) Text("Not a Git repository") .font(.caption) .foregroundColor(.gray) Spacer() } .frame(maxWidth: .infinity) } private func colorForStatus(_ status: GitFileStatus) -> Color { switch status { case .modified: return .orange case .added: return .green case .deleted: return .red case .renamed: return .blue case .untracked: return .gray case .conflicted: return .red } } }