diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index 62cf29b93f..4a711cc831 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -370,6 +370,7 @@ 85773E1E2A3E0A1F00C5D926 /* SettingsSearchResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85773E1D2A3E0A1F00C5D926 /* SettingsSearchResult.swift */; }; 85CD0C5F2A10CC3200E531FD /* URL+isImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85CD0C5E2A10CC3200E531FD /* URL+isImage.swift */; }; 85E4122A2A46C8CA00183F2B /* LocationsSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85E412292A46C8CA00183F2B /* LocationsSettings.swift */; }; + 9D36E1BF2B5E7D7500443C41 /* GitBranchesGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D36E1BE2B5E7D7500443C41 /* GitBranchesGroup.swift */; }; B6041F4D29D7A4E9000F3454 /* SettingsPageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6041F4C29D7A4E9000F3454 /* SettingsPageView.swift */; }; B6041F5229D7D6D6000F3454 /* SettingsWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6041F5129D7D6D6000F3454 /* SettingsWindow.swift */; }; B607181D2B0C5BE3009CDAB4 /* GitClient+Stash.swift in Sources */ = {isa = PBXBuildFile; fileRef = B607181C2B0C5BE3009CDAB4 /* GitClient+Stash.swift */; }; @@ -885,6 +886,7 @@ 85773E1D2A3E0A1F00C5D926 /* SettingsSearchResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSearchResult.swift; sourceTree = ""; }; 85CD0C5E2A10CC3200E531FD /* URL+isImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+isImage.swift"; sourceTree = ""; }; 85E412292A46C8CA00183F2B /* LocationsSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationsSettings.swift; sourceTree = ""; }; + 9D36E1BE2B5E7D7500443C41 /* GitBranchesGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GitBranchesGroup.swift; sourceTree = ""; }; B6041F4C29D7A4E9000F3454 /* SettingsPageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsPageView.swift; sourceTree = ""; }; B6041F5129D7D6D6000F3454 /* SettingsWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsWindow.swift; sourceTree = ""; }; B607181C2B0C5BE3009CDAB4 /* GitClient+Stash.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GitClient+Stash.swift"; sourceTree = ""; }; @@ -1951,6 +1953,7 @@ 04BA7C0A2AE2A2D100584E1C /* GitBranch.swift */, B65B10F12B07D34F002852CF /* GitRemote.swift */, B607181F2B0C6CE7009CDAB4 /* GitStashEntry.swift */, + 9D36E1BE2B5E7D7500443C41 /* GitBranchesGroup.swift */, ); path = Models; sourceTree = ""; @@ -3251,6 +3254,7 @@ B6F0517D29D9E4B100D72287 /* TerminalSettingsView.swift in Sources */, 587B9E8C29301D8F00AC7927 /* GitHubOpenness.swift in Sources */, 5894E59729FEF7740077E59C /* CEWorkspaceFile+Recursion.swift in Sources */, + 9D36E1BF2B5E7D7500443C41 /* GitBranchesGroup.swift in Sources */, 587B9E8229301D8F00AC7927 /* GitHubPreviewHeader.swift in Sources */, 611191FC2B08CCB800D4459B /* SearchIndexer+AsyncController.swift in Sources */, 58F2EB02292FB2B0004A9BDE /* Loopable.swift in Sources */, diff --git a/CodeEdit/Features/CodeEditUI/Views/ToolbarBranchPicker.swift b/CodeEdit/Features/CodeEditUI/Views/ToolbarBranchPicker.swift index fde39952ce..ea505fe15e 100644 --- a/CodeEdit/Features/CodeEditUI/Views/ToolbarBranchPicker.swift +++ b/CodeEdit/Features/CodeEditUI/Views/ToolbarBranchPicker.swift @@ -33,7 +33,7 @@ struct ToolbarBranchPicker: View { var body: some View { HStack(alignment: .center, spacing: 5) { if currentBranch != nil { - Image.checkout + Image.branch .font(.title3) .imageScale(.large) .foregroundColor(controlActive == .inactive ? inactiveColor : .primary) @@ -50,33 +50,24 @@ struct ToolbarBranchPicker: View { .frame(height: 16) .help(title) if let currentBranch { - ZStack(alignment: .trailing) { - Text(currentBranch.name) - .padding(.trailing) - if isHovering { - Image(systemName: "chevron.down") + Menu(content: { + if let sourceControlManager = workspaceFileManager?.sourceControlManager { + PopoverView(sourceControlManager: sourceControlManager) } - } - .font(.subheadline) - .foregroundColor(controlActive == .inactive ? inactiveColor : .secondary) - .frame(height: 11) + }, label: { + Text(currentBranch.name) + .font(.subheadline) + .foregroundColor(controlActive == .inactive ? inactiveColor : .secondary) + .frame(height: 11) + }) + .buttonStyle(.borderless) + .padding(.leading, -3) } } } - .contentShape(Rectangle()) - .onTapGesture { - if currentBranch != nil { - displayPopover.toggle() - } - } .onHover { active in isHovering = active } - .popover(isPresented: $displayPopover, arrowEdge: .bottom) { - if let sourceControlManager = workspaceFileManager?.sourceControlManager { - PopoverView(sourceControlManager: sourceControlManager) - } - } .onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)) { (_) in Task { await sourceControlManager?.refreshCurrentBranch() @@ -112,7 +103,7 @@ struct ToolbarBranchPicker: View { var body: some View { VStack(alignment: .leading) { if let currentBranch = sourceControlManager.currentBranch { - VStack(alignment: .leading, spacing: 0) { + Section { headerLabel("Current Branch") BranchCell(sourceControlManager: sourceControlManager, branch: currentBranch, active: true) } @@ -120,11 +111,46 @@ struct ToolbarBranchPicker: View { let branches = sourceControlManager.branches .filter({ $0.isLocal && $0 != sourceControlManager.currentBranch }) + let branchesGroups = branches.reduce(into: [String: GitBranchesGroup]()) { result, branch in + guard let branchPrefix = branch.name.components(separatedBy: "/").first else { + return + } + + result[ + branchPrefix, + default: GitBranchesGroup(name: branchPrefix, branches: []) + ].branches.append(branch) + } + if !branches.isEmpty { - VStack(alignment: .leading, spacing: 0) { + Section { headerLabel("Branches") - ForEach(branches, id: \.self) { branch in - BranchCell(sourceControlManager: sourceControlManager, branch: branch) + ForEach(branchesGroups.keys.sorted(), id: \.self) { branchGroupPrefix in + if let group = branchesGroups[branchGroupPrefix] { + if !group.shouldNest { + BranchCell( + sourceControlManager: sourceControlManager, + branch: group.branches.first! + ) + } else { + Menu(content: { + ForEach(group.branches, id: \.self) { branch in + BranchCell( + sourceControlManager: sourceControlManager, + branch: branch, + title: String( + branch.name.suffix(branch.name.count - branchGroupPrefix.count - 1) + ) + ) + } + }, label: { + HStack { + Image(systemName: "folder") + Text(group.name) + } + }) + } + } } } } @@ -150,43 +176,37 @@ struct ToolbarBranchPicker: View { /// A Button Cell that represents a branch in the branch picker struct BranchCell: View { let sourceControlManager: SourceControlManager - var branch: GitBranch - var active: Bool = false + let branch: GitBranch + let active: Bool + let title: String? + + init( + sourceControlManager: SourceControlManager, + branch: GitBranch, + active: Bool = false, + title: String? = nil + ) { + self.sourceControlManager = sourceControlManager + self.branch = branch + self.active = active + self.title = title + } @Environment(\.dismiss) private var dismiss - @State private var isHovering: Bool = false - var body: some View { Button { switchBranch() } label: { HStack { - Label { - Text(branch.name) - .frame(maxWidth: .infinity, alignment: .leading) - } icon: { - Image.checkout - .imageScale(.large) - } - .foregroundColor(isHovering ? .white : .secondary) if active { Image(systemName: "checkmark.circle.fill") - .foregroundColor(isHovering ? .white : .green) + } else { + Image.branch } + Text(self.title ?? branch.name) } - .contentShape(Rectangle()) - } - .buttonStyle(.plain) - .padding(.horizontal) - .padding(.vertical, 10) - .background( - EffectView.selectionBackground(isHovering) - ) - .clipShape(RoundedRectangle(cornerRadius: 4)) - .onHover { active in - isHovering = active } } diff --git a/CodeEdit/Features/Git/Client/Models/GitBranchesGroup.swift b/CodeEdit/Features/Git/Client/Models/GitBranchesGroup.swift new file mode 100644 index 0000000000..6c05097f6b --- /dev/null +++ b/CodeEdit/Features/Git/Client/Models/GitBranchesGroup.swift @@ -0,0 +1,16 @@ +// +// GitBranchesGroup.swift +// CodeEdit +// +// Created by Federico Zivolo on 22/01/24. +// + +import Foundation + +struct GitBranchesGroup: Hashable { + let name: String + var branches: [GitBranch] + var shouldNest: Bool { + branches.first?.name.hasPrefix(name + "/") ?? false + } +}