diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index 3e9c94bc4f..56a13b8d22 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -368,6 +368,14 @@ 6CFF967829BEBCF600182D6F /* MainCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CFF967729BEBCF600182D6F /* MainCommands.swift */; }; 6CFF967A29BEBD2400182D6F /* ViewCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CFF967929BEBD2400182D6F /* ViewCommands.swift */; }; 6CFF967C29BEBD5200182D6F /* WindowCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CFF967B29BEBD5200182D6F /* WindowCommands.swift */; }; + 774830612B7554400003B8F0 /* TestTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 774830602B7554400003B8F0 /* TestTask.swift */; }; + 775A8F692B63EC69009B40CC /* ActivityViewer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 775A8F682B63EC69009B40CC /* ActivityViewer.swift */; }; + 77617E5B2B6FD21500601128 /* CETaskRun.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77617E5A2B6FD21500601128 /* CETaskRun.swift */; }; + 7786FF522B7A79640021C653 /* DropdownMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7786FF512B7A79640021C653 /* DropdownMenu.swift */; }; + 7798922C2B6CCF100007510B /* CRTaskStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7798922B2B6CCF100007510B /* CRTaskStatus.swift */; }; + 7798922E2B6D29580007510B /* TaskManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7798922D2B6D29580007510B /* TaskManager.swift */; }; + 779892322B6D2C500007510B /* CETask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 779892312B6D2C4F0007510B /* CETask.swift */; }; + 779A686D2B7E109200A4BBBF /* DropdownMenuItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 779A686C2B7E109200A4BBBF /* DropdownMenuItem.swift */; }; 850C631029D6B01D00E1444C /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850C630F29D6B01D00E1444C /* SettingsView.swift */; }; 850C631229D6B03400E1444C /* SettingsPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850C631129D6B03400E1444C /* SettingsPage.swift */; }; 852C7E332A587279006BA599 /* SearchableSettingsPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 852C7E322A587279006BA599 /* SearchableSettingsPage.swift */; }; @@ -893,6 +901,14 @@ 6CFF967729BEBCF600182D6F /* MainCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainCommands.swift; sourceTree = ""; }; 6CFF967929BEBD2400182D6F /* ViewCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewCommands.swift; sourceTree = ""; }; 6CFF967B29BEBD5200182D6F /* WindowCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowCommands.swift; sourceTree = ""; }; + 774830602B7554400003B8F0 /* TestTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestTask.swift; sourceTree = ""; }; + 775A8F682B63EC69009B40CC /* ActivityViewer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityViewer.swift; sourceTree = ""; }; + 77617E5A2B6FD21500601128 /* CETaskRun.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CETaskRun.swift; sourceTree = ""; }; + 7786FF512B7A79640021C653 /* DropdownMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DropdownMenu.swift; sourceTree = ""; }; + 7798922B2B6CCF100007510B /* CRTaskStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CRTaskStatus.swift; sourceTree = ""; }; + 7798922D2B6D29580007510B /* TaskManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskManager.swift; sourceTree = ""; }; + 779892312B6D2C4F0007510B /* CETask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CETask.swift; sourceTree = ""; }; + 779A686C2B7E109200A4BBBF /* DropdownMenuItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DropdownMenuItem.swift; sourceTree = ""; }; 850C630F29D6B01D00E1444C /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; 850C631129D6B03400E1444C /* SettingsPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsPage.swift; sourceTree = ""; }; 852C7E322A587279006BA599 /* SearchableSettingsPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchableSettingsPage.swift; sourceTree = ""; }; @@ -1367,6 +1383,7 @@ B67B270029D7868000FB9301 /* Settings */, 6C147C4729A329E50089B630 /* SplitView */, 588224FF292C280D00E83CDE /* StatusBar */, + 779892262B6CC7DC0007510B /* Tasks */, 5879827E292ED0FB0085B254 /* TerminalEmulator */, 58822512292C280D00E83CDE /* UtilityArea */, 581BFB4B2926431000D251EC /* Welcome */, @@ -1739,9 +1756,12 @@ 587B9D8629300ABD00AC7927 /* Views */ = { isa = PBXGroup; children = ( + 775A8F682B63EC69009B40CC /* ActivityViewer.swift */, B62AEDA92A1FCBE5009A9F52 /* AreaTabBar.swift */, B65B10EB2B073913002852CF /* CEContentUnavailableView.swift */, B65B10FA2B08B054002852CF /* Divided.swift */, + 7786FF512B7A79640021C653 /* DropdownMenu.swift */, + 779A686C2B7E109200A4BBBF /* DropdownMenuItem.swift */, 587B9D8B29300ABD00AC7927 /* EffectView.swift */, 587B9D9029300ABD00AC7927 /* HelpButton.swift */, B67DB0F82AFDF638002DC647 /* IconButtonStyle.swift */, @@ -2424,6 +2444,42 @@ path = Text; sourceTree = ""; }; + 774830632B7554470003B8F0 /* Tasks */ = { + isa = PBXGroup; + children = ( + 774830602B7554400003B8F0 /* TestTask.swift */, + ); + path = Tasks; + sourceTree = ""; + }; + 779892262B6CC7DC0007510B /* Tasks */ = { + isa = PBXGroup; + children = ( + 779892302B6D2C180007510B /* Models */, + 7798922D2B6D29580007510B /* TaskManager.swift */, + ); + path = Tasks; + sourceTree = ""; + }; + 779892302B6D2C180007510B /* Models */ = { + isa = PBXGroup; + children = ( + 774830632B7554470003B8F0 /* Tasks */, + 779892312B6D2C4F0007510B /* CETask.swift */, + 77617E5A2B6FD21500601128 /* CETaskRun.swift */, + 7798922B2B6CCF100007510B /* CRTaskStatus.swift */, + ); + path = Models; + sourceTree = ""; + }; + 85AEE8D42A474EEC009507BC /* Models */ = { + isa = PBXGroup; + children = ( + 6C5BE51D2A3D545F002DA0FC /* FeatureFlagsSettings.swift */, + ); + path = Models; + sourceTree = ""; + }; 85CD0C5D2A10CC2500E531FD /* URL */ = { isa = PBXGroup; children = ( @@ -3245,6 +3301,7 @@ 58F2EB0D292FB2B0004A9BDE /* ThemeSettings.swift in Sources */, 85CD0C5F2A10CC3200E531FD /* URL+isImage.swift in Sources */, 587B9D9F29300ABD00AC7927 /* SegmentedControl.swift in Sources */, + 779A686D2B7E109200A4BBBF /* DropdownMenuItem.swift in Sources */, 6C7256D729A3D7D000C2D3E0 /* SplitViewControllerView.swift in Sources */, B6EA1FE529DA33DB001BF195 /* ThemeModel.swift in Sources */, B6EA200029DB7966001BF195 /* SettingsColorPicker.swift in Sources */, @@ -3279,6 +3336,8 @@ B640A99E29E2184700715F20 /* SettingsForm.swift in Sources */, B62AEDD12A27B264009A9F52 /* View+paneToolbar.swift in Sources */, 5878DAB1291D627C00DD95A3 /* EditorPathBarComponent.swift in Sources */, + 77617E5B2B6FD21500601128 /* CETaskRun.swift in Sources */, + 7798922E2B6D29580007510B /* TaskManager.swift in Sources */, B628B7B72B223BAD00F9775A /* FindModePicker.swift in Sources */, 587B9E6E29301D8F00AC7927 /* GitLabProject.swift in Sources */, 58798234292E30B90085B254 /* FeedbackIssueArea.swift in Sources */, @@ -3300,6 +3359,7 @@ B6C4F2AC2B3CC4D000B2B140 /* CommitChangedFileListItemView.swift in Sources */, 6C82D6B329BFD88700495C54 /* NavigateCommands.swift in Sources */, B66A4E4C29C9179B004573B4 /* CodeEditApp.swift in Sources */, + 775A8F692B63EC69009B40CC /* ActivityViewer.swift in Sources */, 4E7F066629602E7B00BB3C12 /* CodeEditSplitViewController.swift in Sources */, 587B9E8D29301D8F00AC7927 /* GitHubAccount.swift in Sources */, 201169E72837B5CA00F92B46 /* SourceControlManager.swift in Sources */, @@ -3312,6 +3372,7 @@ 6C578D8129CD294800DC73B2 /* ExtensionActivatorView.swift in Sources */, B6F0517D29D9E4B100D72287 /* TerminalSettingsView.swift in Sources */, 587B9E8C29301D8F00AC7927 /* GitHubOpenness.swift in Sources */, + 7786FF522B7A79640021C653 /* DropdownMenu.swift in Sources */, 5894E59729FEF7740077E59C /* CEWorkspaceFile+Recursion.swift in Sources */, 9D36E1BF2B5E7D7500443C41 /* GitBranchesGroup.swift in Sources */, 587B9E8229301D8F00AC7927 /* GitHubPreviewHeader.swift in Sources */, @@ -3396,6 +3457,7 @@ 6CE622692A2A174A0013085C /* InspectorTab.swift in Sources */, 58F2EB04292FB2B0004A9BDE /* SourceControlSettings.swift in Sources */, 58710159298EB80000951BA4 /* CEWorkspaceFileManager.swift in Sources */, + 774830612B7554400003B8F0 /* TestTask.swift in Sources */, 582213F0291834A500EFE361 /* AboutView.swift in Sources */, 6CC9E4B229B5669900C97388 /* Environment+ActiveEditor.swift in Sources */, 58822526292C280D00E83CDE /* StatusBarBreakpointButton.swift in Sources */, @@ -3463,6 +3525,7 @@ 587B9E9529301D8F00AC7927 /* BitBucketUser.swift in Sources */, 587B9E7C29301D8F00AC7927 /* GitHubRepositoryRouter.swift in Sources */, 286471AB27ED51FD0039369D /* ProjectNavigatorView.swift in Sources */, + 7798922C2B6CCF100007510B /* CRTaskStatus.swift in Sources */, B6E41C7C29DE2B110088F9F4 /* AccountsSettingsProviderRow.swift in Sources */, B62AEDB52A1FE295009A9F52 /* UtilityAreaDebugView.swift in Sources */, 6C049A372A49E2DB00D42923 /* DirectoryEventStream.swift in Sources */, @@ -3588,6 +3651,7 @@ 6C81916729B3E80700B75C92 /* ModifierKeysObserver.swift in Sources */, 613899BC2B6E709C00A5CAF6 /* URL+FuzzySearchable.swift in Sources */, 611192002B08CCD700D4459B /* SearchIndexer+Memory.swift in Sources */, + 779892322B6D2C500007510B /* CETask.swift in Sources */, 587B9E8129301D8F00AC7927 /* PublicKey.swift in Sources */, 611191FE2B08CCD200D4459B /* SearchIndexer+File.swift in Sources */, 5B241BF32B6DDBFF0016E616 /* IgnorePatternListItemView.swift in Sources */, diff --git a/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 14106a2645..bad5ec96e0 100644 --- a/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -30,7 +30,7 @@ { "identity" : "codeeditsourceeditor", "kind" : "remoteSourceControl", - "location" : "https://github.com/CodeEditApp/CodeEditSourceEditor", + "location" : "https://github.com/CodeEditApp/CodeEditSourceEditor.git", "state" : { "revision" : "7360f00bf7ec8e93b4833357bd254bef7e5c943d", "version" : "0.7.2" @@ -95,8 +95,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/mattmassicotte/MainOffender", "state" : { - "revision" : "8de872d9256ff7f9913cbc5dd560568ab164be45", - "version" : "0.2.1" + "revision" : "750730d33acec0f79edc836221937389e7455bae", + "version" : "0.2.0" } }, { diff --git a/CodeEdit/Features/CodeEditUI/Views/ActivityViewer.swift b/CodeEdit/Features/CodeEditUI/Views/ActivityViewer.swift new file mode 100644 index 0000000000..69568deefe --- /dev/null +++ b/CodeEdit/Features/CodeEditUI/Views/ActivityViewer.swift @@ -0,0 +1,198 @@ +// +// ActivityViewer.swift +// CodeEdit +// +// Created by Axel Martinez on 26/1/24. +// + +import SwiftUI +import Combine + +/// A view that shows the activity bar and the current status of any executed task +struct ActivityViewer: View { + private var workspaceFileManager: CEWorkspaceFileManager? + + @ObservedObject private var taskManager: TaskManager + @State private var status: CETaskStatus = .stopped + @State private var output: String = "" + + init( + workspaceFileManager: CEWorkspaceFileManager?, + taskManager: TaskManager + ) { + self.workspaceFileManager = workspaceFileManager + self.taskManager = taskManager + } + + var body: some View { + HStack { + HStack(spacing: 3) { + DropdownMenu( + icon: "folder.badge.gearshape", + selectedItem: workspaceFileManager?.workspaceItem.fileName(), + items: { + WorkspaceMenuItem( + workspaceFileManager: workspaceFileManager, + item: workspaceFileManager?.workspaceItem + ) + }, + options: { + OptionMenuItem(label: "Add Folder..") + OptionMenuItem(label: "Workspace Settings...") + } + ) + Image(systemName: "chevron.compact.right") + .imageScale(.medium) + DropdownMenu( + icon: "gearshape", + selectedItem: taskManager.activeTask?.name, + status: status.color, + items: { + ForEach(taskManager.getTasks(), id: \.name) { item in + TaskMenuItem(item: item, taskManager: taskManager) + } + }, + options: { + OptionMenuItem(label: "Add Task..") + OptionMenuItem(label: "Manage Tasks...") + } + ) + Spacer() + Text(output) + .foregroundColor(.primary) + if let progress = taskManager.activeTaskRun?.progress, progress > 0 { + ring(progress: progress) + .frame(width: 14) + .animation(Animation.easeOut(duration: 2), value: true) + .padding(.leading, 5) + } + } + .padding(.vertical, 5) + .padding(.horizontal, 10) + .background { + RoundedRectangle(cornerRadius: 5).opacity(0.15) + } + .frame(minWidth: 200, idealWidth: 900) + + HStack { + if let errors = taskManager.activeTaskRun?.errors, errors > 0 { + statusLabel("xmark.octagon.fill", errors.description, Color.red) + } + if let warnings = taskManager.activeTaskRun?.warnings, warnings > 0 { + statusLabel("exclamationmark.triangle.fill", warnings.description, Color.yellow) + } + } + } + .font(.subheadline) + .padding(.leading, -50) + .onReceive(taskManager.activeTaskRun?.$output.eraseToAnyPublisher() ?? + Empty().eraseToAnyPublisher()) { output in + self.output = output + } + .onReceive(taskManager.activeTaskRun?.$status.eraseToAnyPublisher() ?? + Empty().eraseToAnyPublisher()) { status in + self.status = status + } + } + + @ViewBuilder + private func ring(progress value: CGFloat) -> some View { + let color = Color(#colorLiteral(red: 0.1764705926, green: 0.4980392158, blue: 0.7568627596, alpha: 1)) + Circle() + .stroke(style: StrokeStyle(lineWidth: 3)) + .foregroundStyle(.tertiary) + .overlay { + Circle() + .trim(from: 0, to: value) + .stroke(color.gradient, style: StrokeStyle(lineWidth: 3, lineCap: .round)) + } + .rotationEffect(.degrees(-90)) + } + + @ViewBuilder + private func statusLabel(_ icon: String, _ count: String, _ color: Color) -> some View { + Label(title: { + Text(count) + .padding(.leading, -7) + }, icon: { + Image(systemName: icon) + .imageScale(.medium) + .symbolRenderingMode(.multicolor) + .foregroundColor(color) + }) + .labelStyle(.titleAndIcon) + } + + struct WorkspaceMenuItem: View { + var workspaceFileManager: CEWorkspaceFileManager? + var item: CEWorkspaceFile? + + var body: some View { + HStack { + if workspaceFileManager?.workspaceItem.fileName() == item?.name { + Image(systemName: "checkmark") + .imageScale(.medium) + .frame(width: 10) + } else { + Spacer() + .frame(width: 18) + } + Image(systemName: "folder.badge.gearshape") + .imageScale(.medium) + Text(item?.name ?? "") + Spacer() + } + .padding(.vertical, 5) + .padding(.horizontal, 10) + .dropdownMenuItem() + .onTapGesture { + } + } + } + + struct TaskMenuItem: View { + var item: any CETask + + @ObservedObject var taskManager: TaskManager + + var body: some View { + HStack { + if taskManager.activeTask?.name == item.name { + Image(systemName: "checkmark") + .imageScale(.medium) + .frame(width: 10) + } else { + Spacer() + .frame(width: 18) + } + Image(systemName: "gearshape") + .imageScale(.medium) + Text(item.name) + Spacer() + Circle() + .fill(taskManager.activeTaskRun?.status.color ?? CETaskStatus.stopped.color) + .frame(width: 5, height: 5) + } + .padding(.vertical, 5) + .padding(.horizontal, 10) + .dropdownMenuItem() + .onTapGesture { + self.taskManager.activeTask = item + } + } + } + + struct OptionMenuItem: View { + var label: String + + var body: some View { + HStack { + Text(label) + Spacer() + } + .padding(.vertical, 5) + .padding(.horizontal, 28) + .dropdownMenuItem() + } + } +} diff --git a/CodeEdit/Features/CodeEditUI/Views/DropdownMenu.swift b/CodeEdit/Features/CodeEditUI/Views/DropdownMenu.swift new file mode 100644 index 0000000000..48477b74e2 --- /dev/null +++ b/CodeEdit/Features/CodeEditUI/Views/DropdownMenu.swift @@ -0,0 +1,62 @@ +// +// DropdownMenu.swift +// CodeEdit +// +// Created by Axel Martinez on 12/2/24. +// + +import SwiftUI + +/// A view that shows a custom dropdown menu +struct DropdownMenu: View { + let icon: String + let options: Options + + var selectedItem: String? + var status: Color? + var items: Items + + @State private var isPresented: Bool = false + + init( + icon: String, + selectedItem: String? = nil, + status: Color? = nil, + @ViewBuilder items: @escaping () -> Items, + @ViewBuilder options: @escaping() -> Options + ) { + self.icon = icon + self.selectedItem = selectedItem + self.status = status + self.items = items() + self.options = options() + } + + var body: some View { + HStack(spacing: 3) { + Image(systemName: icon) + .imageScale(.medium) + Text(selectedItem ?? "") + if let status = status { + Circle() + .fill(status) + .frame(width: 5, height: 5) + } + } + .font(.caption) + .popover(isPresented: $isPresented) { + VStack(alignment: .leading, spacing: 0) { + items + .cornerRadius(5) + Divider() + .padding(.vertical, 5) + options + .cornerRadius(5) + } + .padding(5) + .frame(width: 215) + }.onTapGesture { + self.isPresented.toggle() + } + } +} diff --git a/CodeEdit/Features/CodeEditUI/Views/DropdownMenuItem.swift b/CodeEdit/Features/CodeEditUI/Views/DropdownMenuItem.swift new file mode 100644 index 0000000000..2f973dd7bd --- /dev/null +++ b/CodeEdit/Features/CodeEditUI/Views/DropdownMenuItem.swift @@ -0,0 +1,28 @@ +// +// DropdopMenuItem.swift +// CodeEdit +// +// Created by Axel Martinez on 15/2/24. +// + +import SwiftUI + +/// A view that represents a custom dropdown menu item +struct DropdownMenuItem: ViewModifier { + @State private var isHovering = false + + func body(content: Content) -> some View { + content + .background(isHovering ? Color(NSColor.systemBlue) : .clear) + .foregroundColor(isHovering ? Color(NSColor.white) : .primary) + .onHover(perform: { hovering in + self.isHovering = hovering + }) + } +} + +extension View { + func dropdownMenuItem() -> some View { + modifier(DropdownMenuItem()) + } +} diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift b/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift index 9f270f061a..c25cdd7d6e 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift @@ -141,23 +141,33 @@ final class CodeEditSplitViewController: NSSplitViewController { /// Quick fix for list tracking separator needing to be added again after closing, /// then opening the inspector with a drag. - private func insertToolbarItemIfNeeded() { - guard !( - view.window?.toolbar?.items.contains(where: { $0.itemIdentifier == .itemListTrackingSeparator }) ?? true - ) else { + func insertToolbarItemIfNeeded() { + guard !(splitViewItems.last?.isCollapsed ?? true), + let toolbar = view.window?.toolbar, + let addIndex = toolbar.items.firstIndex(where: { + $0.itemIdentifier == .addSidebarItem + }) else { return } - view.window?.toolbar?.insertItem(withItemIdentifier: .itemListTrackingSeparator, at: 4) + + toolbar.insertItem(withItemIdentifier: .itemListTrackingSeparator, at: addIndex+1) + toolbar.insertItem(withItemIdentifier: .flexibleSpace, at: addIndex+2) } /// Quick fix for list tracking separator needing to be removed after closing the inspector with a drag - private func removeToolbarItemIfNeeded() { - guard let index = view.window?.toolbar?.items.firstIndex( - where: { $0.itemIdentifier == .itemListTrackingSeparator } - ) else { + func removeToolbarItemIfNeeded() { + guard let toolbar = view.window?.toolbar, + let separatorIndex = toolbar.items.firstIndex(where: { + $0.itemIdentifier == .itemListTrackingSeparator + }), + let flexibleSpaceIndex = toolbar.items.lastIndex(where: { + $0.itemIdentifier == .flexibleSpace + }) else { return } - view.window?.toolbar?.removeItem(at: index) + + toolbar.removeItem(at: flexibleSpaceIndex) + toolbar.removeItem(at: separatorIndex) } func hideInspectorToolbarBackground() { diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift index 210d130dc9..56c0366b23 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift @@ -15,6 +15,8 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs @Published var navigatorCollapsed = false @Published var inspectorCollapsed = false + private var taskManager = TaskManager() + var observers: [NSKeyValueObservation] = [] var workspace: WorkspaceDocument? @@ -28,7 +30,9 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs init(window: NSWindow, workspace: WorkspaceDocument) { super.init(window: window) + self.workspace = workspace + setupSplitView(with: workspace) let view = CodeEditSplitView(controller: splitViewController).ignoresSafeArea() @@ -140,11 +144,15 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { [ .toggleFirstSidebarItem, + .flexibleSpace, + .stopTaskSidebarItem, + .startTaskSidebarItem, .sidebarTrackingSeparator, .branchPicker, .flexibleSpace, - .itemListTrackingSeparator, + .activityViewer, .flexibleSpace, + .addSidebarItem, .toggleLastSidebarItem ] } @@ -156,7 +164,9 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs .flexibleSpace, .itemListTrackingSeparator, .toggleLastSidebarItem, - .branchPicker + .branchPicker, + .activityViewer, + .addSidebarItem ] } @@ -170,55 +180,100 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs guard let splitViewController else { return nil } - return NSTrackingSeparatorToolbarItem( identifier: .itemListTrackingSeparator, splitView: splitViewController.splitView, dividerIndex: 1 ) case .toggleFirstSidebarItem: - let toolbarItem = NSToolbarItem(itemIdentifier: NSToolbarItem.Identifier.toggleFirstSidebarItem) - toolbarItem.label = "Navigator Sidebar" - toolbarItem.paletteLabel = " Navigator Sidebar" - toolbarItem.toolTip = "Hide or show the Navigator" - toolbarItem.isBordered = true - toolbarItem.target = self - toolbarItem.action = #selector(self.toggleFirstPanel) - toolbarItem.image = NSImage( - systemSymbolName: "sidebar.leading", - accessibilityDescription: nil - )?.withSymbolConfiguration(.init(scale: .large)) - - return toolbarItem + return toolbarItem( + identifier: itemIdentifier, + label: "Navigator Sidebar", + tooltip: "Hide or show the Navigator", + icon: "sidebar.leading", + action: #selector(self.toggleFirstPanel) + ) case .toggleLastSidebarItem: - let toolbarItem = NSToolbarItem(itemIdentifier: NSToolbarItem.Identifier.toggleLastSidebarItem) - toolbarItem.label = "Inspector Sidebar" - toolbarItem.paletteLabel = "Inspector Sidebar" - toolbarItem.toolTip = "Hide or show the Inspectors" - toolbarItem.isBordered = true - toolbarItem.target = self - toolbarItem.action = #selector(self.toggleLastPanel) - toolbarItem.image = NSImage( - systemSymbolName: "sidebar.trailing", - accessibilityDescription: nil - )?.withSymbolConfiguration(.init(scale: .large)) - - return toolbarItem + return toolbarItem( + identifier: itemIdentifier, + label: "Inspector Sidebar", + tooltip: "Hide or show the Inspectors", + icon: "sidebar.trailing", + action: #selector(self.toggleLastPanel) + ) + case .stopTaskSidebarItem: + return toolbarItem( + identifier: itemIdentifier, + label: "Stop", + tooltip: "Stop execution of task", + icon: "stop.fill", + action: nil + ) + case .startTaskSidebarItem: + return toolbarItem( + identifier: itemIdentifier, + label: "Start", + tooltip: "Start execution of task", + icon: "play.fill", + action: #selector(self.runActiveTask) + ) + case .addSidebarItem: + return toolbarItem( + identifier: itemIdentifier, + label: "Add", + tooltip: "Add", + icon: "plus", + action: nil + ) case .branchPicker: let toolbarItem = NSToolbarItem(itemIdentifier: .branchPicker) - let view = NSHostingView( + toolbarItem.view = NSHostingView( rootView: ToolbarBranchPicker( workspaceFileManager: workspace?.workspaceFileManager ) ) - toolbarItem.view = view - + return toolbarItem + case .activityViewer: + let toolbarItem = NSToolbarItem(itemIdentifier: .activityViewer) + toolbarItem.view = NSHostingView( + rootView: ActivityViewer( + workspaceFileManager: workspace?.workspaceFileManager, + taskManager: taskManager + ) + ) return toolbarItem default: return NSToolbarItem(itemIdentifier: itemIdentifier) } } + private func toolbarItem( + identifier: NSToolbarItem.Identifier, + label: String, + tooltip: String, + icon: String, + action: Selector? + ) -> NSToolbarItem { + let toolbarItem = NSToolbarItem(itemIdentifier: identifier) + toolbarItem.label = label + toolbarItem.paletteLabel = label + toolbarItem.toolTip = tooltip + toolbarItem.isBordered = true + toolbarItem.target = self + toolbarItem.action = action + toolbarItem.image = NSImage( + systemSymbolName: icon, + accessibilityDescription: nil + )?.withSymbolConfiguration(.init(scale: .large)) + + return toolbarItem + } + + @objc + private func runActiveTask() { + taskManager.executeActiveTask() + } + private func getSelectedCodeFile() -> CodeFileDocument? { workspace?.editorManager.activeEditor.selectedTab?.file.fileDocument } diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditWindowControllerExtensions.swift b/CodeEdit/Features/Documents/Controllers/CodeEditWindowControllerExtensions.swift index 3a367e6601..fb836eb8cb 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditWindowControllerExtensions.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditWindowControllerExtensions.swift @@ -20,25 +20,20 @@ extension CodeEditWindowController { @objc func toggleLastPanel() { - guard let lastSplitView = splitViewController.splitViewItems.last else { return } + guard let codeEditSplitVC = splitViewController as? CodeEditSplitViewController else { return } + guard let lastSplitView = codeEditSplitVC.splitViewItems.last else { return } - if let toolbar = window?.toolbar, - lastSplitView.isCollapsed, - !toolbar.items.map(\.itemIdentifier).contains(.itemListTrackingSeparator) { - window?.toolbar?.insertItem(withItemIdentifier: .itemListTrackingSeparator, at: 4) - } NSAnimationContext.runAnimationGroup { _ in lastSplitView.animator().isCollapsed.toggle() - } completionHandler: { [weak self] in + } completionHandler: { if lastSplitView.isCollapsed { - self?.window?.animator().toolbar?.removeItem(at: 4) + codeEditSplitVC.removeToolbarItemIfNeeded() } } - if let codeEditSplitVC = splitViewController as? CodeEditSplitViewController { - codeEditSplitVC.saveInspectorCollapsedState(isCollapsed: lastSplitView.isCollapsed) - codeEditSplitVC.hideInspectorToolbarBackground() - } + codeEditSplitVC.insertToolbarItemIfNeeded() + codeEditSplitVC.saveInspectorCollapsedState(isCollapsed: lastSplitView.isCollapsed) + codeEditSplitVC.hideInspectorToolbarBackground() } /// These are example items that added as commands to command palette @@ -116,6 +111,10 @@ extension CodeEditWindowController { extension NSToolbarItem.Identifier { static let toggleFirstSidebarItem: NSToolbarItem.Identifier = NSToolbarItem.Identifier("ToggleFirstSidebarItem") static let toggleLastSidebarItem: NSToolbarItem.Identifier = NSToolbarItem.Identifier("ToggleLastSidebarItem") + static let addSidebarItem: NSToolbarItem.Identifier = NSToolbarItem.Identifier("AddSidebarItem") + static let stopTaskSidebarItem: NSToolbarItem.Identifier = NSToolbarItem.Identifier("StopTaskSidebarItem") + static let startTaskSidebarItem: NSToolbarItem.Identifier = NSToolbarItem.Identifier("StartTaskSidebarItem") static let itemListTrackingSeparator = NSToolbarItem.Identifier("ItemListTrackingSeparator") static let branchPicker: NSToolbarItem.Identifier = NSToolbarItem.Identifier("BranchPicker") + static let activityViewer: NSToolbarItem.Identifier = NSToolbarItem.Identifier("ActivityViewer") } diff --git a/CodeEdit/Features/Tasks/Models/CETask.swift b/CodeEdit/Features/Tasks/Models/CETask.swift new file mode 100644 index 0000000000..ac86af2b40 --- /dev/null +++ b/CodeEdit/Features/Tasks/Models/CETask.swift @@ -0,0 +1,15 @@ +// +// CETask.swift +// CodeEdit +// +// Created by Axel Martinez on 2/2/24. +// + +import Foundation + +/// Represents a CodeEdit task that can be executed. +protocol CETask: Hashable { + var name: String { get } + + func execute(_ activeTaskRun: CETaskRun) async throws +} diff --git a/CodeEdit/Features/Tasks/Models/CETaskRun.swift b/CodeEdit/Features/Tasks/Models/CETaskRun.swift new file mode 100644 index 0000000000..d794b6078f --- /dev/null +++ b/CodeEdit/Features/Tasks/Models/CETaskRun.swift @@ -0,0 +1,66 @@ +// +// CETaskRun.swift +// CodeEdit +// +// Created by Axel Martinez on 4/2/24. +// + +import Foundation + +/// Stores the state of a task once it's executed +class CETaskRun: ObservableObject { + /// The name of the associated task. + let task: any CETask + + /// The current progress of the task. + @Published private(set) var output: String = "" + + /// The status of the task. + @Published private(set) var status: CETaskStatus = .stopped + + /// The process percent of the task. + @Published private(set) var progress: CGFloat = 0 + + //TODO: replace these counts with the actual errors/warnings + /// The errors generated by a task run if any. + @Published private(set) var errors: Int = 0 + + /// The warnings generated by a task if any. + @Published private(set) var warnings: Int = 0 + + init(task: any CETask) { + self.task = task + } + + func start() async throws { + await MainActor.run { + self.status = .running + } + + try await task.execute(self) + + await MainActor.run { + if errors > 0 { + self.status = .failed + } else { + self.status = .finished + } + } + } + + /// Updates the progress and output values on the main thread` + func updateProgress(_ output: String, progress: Double) async { + await MainActor.run { + self.progress = progress + self.output = output + } + } + + /// Updates the erros and warning counts on the main thread` + func updateErrorsAndWarnings(errors: Int, warnings: Int) async { + await MainActor.run { + self.errors = errors + self.warnings = warnings + } + } +} diff --git a/CodeEdit/Features/Tasks/Models/CRTaskStatus.swift b/CodeEdit/Features/Tasks/Models/CRTaskStatus.swift new file mode 100644 index 0000000000..f5e422f4e7 --- /dev/null +++ b/CodeEdit/Features/Tasks/Models/CRTaskStatus.swift @@ -0,0 +1,25 @@ +// +// TaskStatus.swift +// CodeEdit +// +// Created by Axel Martinez on 2/2/24. +// + +import SwiftUI + +/// Enum to represent a task's status +enum CETaskStatus { + case running + case stopped + case failed + case finished + + var color: Color { + switch self { + case .running: return Color.orange + case .stopped: return Color.gray + case .failed: return Color.red + case .finished: return Color.green + } + } +} diff --git a/CodeEdit/Features/Tasks/Models/Tasks/TestTask.swift b/CodeEdit/Features/Tasks/Models/Tasks/TestTask.swift new file mode 100644 index 0000000000..363e6b385a --- /dev/null +++ b/CodeEdit/Features/Tasks/Models/Tasks/TestTask.swift @@ -0,0 +1,43 @@ +// +// FakeTask.swift +// CodeEdit +// +// Created by Axel Martinez on 8/2/24. +// + +import Foundation + +/// Fake task for testing +class TestTask: ObservableObject, CETask { + @Published var name: String + + init(name: String) { + self.name = name + } + + func hash(into hasher: inout Hasher) { + hasher.combine(name) + } + + static func == (lhs: TestTask, rhs: TestTask) -> Bool { + lhs.name == rhs.name + } + + func execute(_ taskRun: CETaskRun) async throws { + let seconds = 4.0 + + await taskRun.updateProgress("Executing task 1 of 2", progress: 0.33) + + try await Task.sleep(nanoseconds: UInt64(seconds * Double(NSEC_PER_SEC))) + + await taskRun.updateProgress("Executing task 2 of 2", progress: 0.66) + + try await Task.sleep(nanoseconds: UInt64(seconds * Double(NSEC_PER_SEC))) + + await taskRun.updateProgress("Finished all tasks", progress: 1) + + if name == "auth" { + await taskRun.updateErrorsAndWarnings(errors: 2, warnings: 6) + } + } +} diff --git a/CodeEdit/Features/Tasks/TaskManager.swift b/CodeEdit/Features/Tasks/TaskManager.swift new file mode 100644 index 0000000000..936ad9bd44 --- /dev/null +++ b/CodeEdit/Features/Tasks/TaskManager.swift @@ -0,0 +1,42 @@ +// +// TaskManager.swift +// CodeEdit +// +// Created by Axel Martinez on 2/2/24. +// + +import Foundation + +/// This class handles the execution of tasks +final class TaskManager: ObservableObject { + @Published var activeTask: (any CETask)? + @Published var activeTaskRun: CETaskRun? + + init() { + self.activeTask = getTasks().first + } + + /// Gets the current available tasks + func getTasks() -> [any CETask] { + // TODO: Replace with actual tasks + return [ + TestTask(name: "dev"), + TestTask(name: "backend"), + TestTask(name: "auth"), + TestTask(name: "test") + ] + } + + /// Executes the active task + func executeActiveTask() { + guard let activeTask = activeTask else { return } + + activeTaskRun = CETaskRun(task: activeTask) + + guard let run = activeTaskRun else { return } + + Task { + try await run.start() + } + } +}