diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index 185fb23e1..822b61719 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -382,6 +382,7 @@ 6C5B63DE29C76213005454BA /* WindowCodeFileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C5B63DD29C76213005454BA /* WindowCodeFileView.swift */; }; 6C5C891B2A3F736500A94FE1 /* FocusedValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C5C891A2A3F736500A94FE1 /* FocusedValues.swift */; }; 6C5FDF7A29E6160000BC08C0 /* AppSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C5FDF7929E6160000BC08C0 /* AppSettings.swift */; }; + 6C6362D42C3E321A0025570D /* Editor+History.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C6362D32C3E321A0025570D /* Editor+History.swift */; }; 6C66C31329D05CDC00DE9ED2 /* GRDB in Frameworks */ = {isa = PBXBuildFile; productRef = 6C66C31229D05CDC00DE9ED2 /* GRDB */; }; 6C6BD6EF29CD12E900235D17 /* ExtensionManagerWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C6BD6EE29CD12E900235D17 /* ExtensionManagerWindow.swift */; }; 6C6BD6F129CD13FA00235D17 /* ExtensionDiscovery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C6BD6F029CD13FA00235D17 /* ExtensionDiscovery.swift */; }; @@ -404,6 +405,7 @@ 6C85BB402C2105ED00EB5DEF /* CodeEditKit in Frameworks */ = {isa = PBXBuildFile; productRef = 6C85BB3F2C2105ED00EB5DEF /* CodeEditKit */; }; 6C85BB412C21061A00EB5DEF /* GitHubComment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587B9E3C29301D8F00AC7927 /* GitHubComment.swift */; }; 6C85BB442C210EFD00EB5DEF /* SwiftUIIntrospect in Frameworks */ = {isa = PBXBuildFile; productRef = 6C85BB432C210EFD00EB5DEF /* SwiftUIIntrospect */; }; + 6C85F7562C3CA638008E9836 /* EditorHistoryMenus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C85F7552C3CA638008E9836 /* EditorHistoryMenus.swift */; }; 6C91D57229B176FF0059A90D /* EditorManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C91D57129B176FF0059A90D /* EditorManager.swift */; }; 6C9619202C3F27E3009733CE /* ProjectNavigatorUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C96191B2C3F27E3009733CE /* ProjectNavigatorUITests.swift */; }; 6C9619222C3F27F1009733CE /* Query.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C9619212C3F27F1009733CE /* Query.swift */; }; @@ -1004,6 +1006,7 @@ 6C5B63DD29C76213005454BA /* WindowCodeFileView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WindowCodeFileView.swift; sourceTree = ""; }; 6C5C891A2A3F736500A94FE1 /* FocusedValues.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FocusedValues.swift; sourceTree = ""; }; 6C5FDF7929E6160000BC08C0 /* AppSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettings.swift; sourceTree = ""; }; + 6C6362D32C3E321A0025570D /* Editor+History.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Editor+History.swift"; sourceTree = ""; }; 6C6BD6EE29CD12E900235D17 /* ExtensionManagerWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionManagerWindow.swift; sourceTree = ""; }; 6C6BD6F029CD13FA00235D17 /* ExtensionDiscovery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionDiscovery.swift; sourceTree = ""; }; 6C6BD6F529CD145F00235D17 /* ExtensionInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionInfo.swift; sourceTree = ""; }; @@ -1017,6 +1020,7 @@ 6C82D6B829BFE34900495C54 /* HelpCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HelpCommands.swift; sourceTree = ""; }; 6C82D6BB29C00CD900495C54 /* FirstResponderPropertyWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirstResponderPropertyWrapper.swift; sourceTree = ""; }; 6C82D6C529C012AD00495C54 /* NSApp+openWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSApp+openWindow.swift"; sourceTree = ""; }; + 6C85F7552C3CA638008E9836 /* EditorHistoryMenus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditorHistoryMenus.swift; sourceTree = ""; }; 6C91D57129B176FF0059A90D /* EditorManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditorManager.swift; sourceTree = ""; }; 6C96191B2C3F27E3009733CE /* ProjectNavigatorUITests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProjectNavigatorUITests.swift; sourceTree = ""; }; 6C9619212C3F27F1009733CE /* Query.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Query.swift; sourceTree = ""; }; @@ -2319,6 +2323,7 @@ DE6F77862813625500D00A76 /* EditorTabBarDivider.swift */, 287776E827E34BC700D46668 /* EditorTabBarView.swift */, B6AB09A22AAABFEC0003A3A6 /* EditorTabBarLeadingAccessories.swift */, + 6C85F7552C3CA638008E9836 /* EditorHistoryMenus.swift */, B6AB09A42AAAC00F0003A3A6 /* EditorTabBarTrailingAccessories.swift */, ); path = Views; @@ -3030,6 +3035,7 @@ isa = PBXGroup; children = ( 6C147C3D29A3281D0089B630 /* Editor.swift */, + 6C6362D32C3E321A0025570D /* Editor+History.swift */, 5994B6D92BD6B408006A4C5F /* Editor+TabSwitch.swift */, 6CA1AE942B46950000378EAB /* EditorInstance.swift */, 6C147C3E29A3281D0089B630 /* EditorLayout.swift */, @@ -3998,6 +4004,7 @@ 30B088162C0D53080063A882 /* LSPCache.swift in Sources */, B6F0517929D9E3C900D72287 /* SourceControlGitView.swift in Sources */, 587B9E8329301D8F00AC7927 /* GitHubPullRequest.swift in Sources */, + 6C85F7562C3CA638008E9836 /* EditorHistoryMenus.swift in Sources */, 5878DA82291863F900DD95A3 /* AcknowledgementsView.swift in Sources */, 587B9E8529301D8F00AC7927 /* GitHubReview.swift in Sources */, 58D01C9A293167DC00C5B6B4 /* CodeEditKeychain.swift in Sources */, @@ -4033,6 +4040,7 @@ 30B087FC2C0D53080063A882 /* LanguageServer+CallHierarchy.swift in Sources */, 6CFF967C29BEBD5200182D6F /* WindowCommands.swift in Sources */, 587B9E7229301D8F00AC7927 /* GitJSONPostRouter.swift in Sources */, + 6C6362D42C3E321A0025570D /* Editor+History.swift in Sources */, 6C85BB412C21061A00EB5DEF /* GitHubComment.swift in Sources */, 5878DAB0291D627C00DD95A3 /* EditorPathBarMenu.swift in Sources */, 04BA7C242AE2E7CD00584E1C /* SourceControlNavigatorSyncView.swift in Sources */, diff --git a/CodeEdit/Features/Editor/Models/Editor+History.swift b/CodeEdit/Features/Editor/Models/Editor+History.swift new file mode 100644 index 000000000..762711f9a --- /dev/null +++ b/CodeEdit/Features/Editor/Models/Editor+History.swift @@ -0,0 +1,71 @@ +// +// Editor+History.swift +// CodeEdit +// +// Created by Khan Winter on 7/9/24. +// + +import Foundation + +/// Methods for modifying the history list on the editor. +extension Editor { + /// Add the tab to the history list. + /// - Parameter tab: The tab to add to the history. + func addToHistory(_ tab: Tab) { + if history.first != tab { + history.prepend(tab) + } + } + + /// Clear any tabs in the "future" on the history list. Resets the history offset and removes any tabs that were + /// available to navigate forwards to. + func clearFuture() { + guard historyOffset > 0 else { return } // nothing to clear, avoid an out of bounds error + history.removeFirst(historyOffset) + historyOffset = 0 + } + + /// Move backwards in the history list by one place. + func goBackInHistory() { + if canGoBackInHistory { + historyOffset += 1 + } + } + + /// Move forwards in the history list by one place. + func goForwardInHistory() { + if canGoForwardInHistory { + historyOffset -= 1 + } + } + + // TODO: move to @Observable so this works better + /// Warning: NOT published! + var canGoBackInHistory: Bool { + historyOffset != history.count - 1 && !history.isEmpty + } + + // TODO: move to @Observable so this works better + /// Warning: NOT published! + var canGoForwardInHistory: Bool { + historyOffset != 0 + } + + /// Called by the ``Editor`` class when the history offset is changed. + /// + /// This method updates the selected tab to the current tab in the history offset. + /// If the tab is not opened, it is opened without modifying the history list. + /// - Warning: Do not use except in the ``historyOffset``'s `didSet`. + func historyOffsetDidChange() { + let tab = history[historyOffset] + + if !tabs.contains(tab) { + if let temporaryTab, tabs.contains(temporaryTab) { + closeTab(file: temporaryTab.file, fromHistory: true) + } + temporaryTab = tab + openTab(file: tab.file, fromHistory: true) + } + selectedTab = tab + } +} diff --git a/CodeEdit/Features/Editor/Models/Editor.swift b/CodeEdit/Features/Editor/Models/Editor.swift index 805ec7816..be1f434f0 100644 --- a/CodeEdit/Features/Editor/Models/Editor.swift +++ b/CodeEdit/Features/Editor/Models/Editor.swift @@ -35,22 +35,16 @@ final class Editor: ObservableObject, Identifiable { } /// The current offset in the history list. + /// When set, updates the ``selectedTab`` to the tab indicated by the offset. + /// See the ``historyOffsetDidChange()`` method for more details. @Published var historyOffset: Int = 0 { didSet { - let tab = history[historyOffset] - - if !tabs.contains(tab) { - if let temporaryTab, tabs.contains(temporaryTab) { - closeTab(file: temporaryTab.file, fromHistory: true) - } - temporaryTab = tab - openTab(file: tab.file, fromHistory: true) - } - selectedTab = tab + historyOffsetDidChange() } } - /// History of tab switching. + /// Maintains the list of tabs that have been switched to. + /// - Warning: Use the ``addToHistory(_:)`` or ``clearFuture()`` methods to modify this. Do not modify directly. @Published var history: Deque = [] /// Currently selected tab. @@ -106,7 +100,11 @@ final class Editor: ObservableObject, Identifiable { /// Closes a tab in the editor. /// This will also write any changes to the file on disk and will add the tab to the tab history. - /// - Parameter item: the tab to close. + /// - Parameters: + /// - file: The tab to close + /// - fromHistory: If `true`, does not clear tabs ahead of the ``historyOffset`` + /// Used when opening tabs from the history queue where tabs ahead of the ``historyOffset`` should + /// not be removed. func closeTab(file: CEWorkspaceFile, fromHistory: Bool = false) { guard canCloseTab(file: file) else { return } @@ -114,14 +112,14 @@ final class Editor: ObservableObject, Identifiable { temporaryTab = nil } if !fromHistory { - historyOffset = 0 + clearFuture() } if file != selectedTab?.file { - history.prepend(EditorInstance(file: file)) + addToHistory(EditorInstance(file: file)) } removeTab(file) if let selectedTab { - history.prepend(selectedTab) + addToHistory(selectedTab) } // Reset change count to 0 file.fileDocument?.updateChangeCount(.changeCleared) @@ -148,16 +146,15 @@ final class Editor: ObservableObject, Identifiable { // Item is already opened in a tab. guard !tabs.contains(item) || !asTemporary else { selectedTab = item - history.prepend(item) + addToHistory(item) return } switch (temporaryTab, asTemporary) { case (.some(let tab), true): if let index = tabs.firstIndex(of: tab) { - history.removeFirst(historyOffset) - history.prepend(item) - historyOffset = 0 + clearFuture() + addToHistory(item) tabs.remove(tab) tabs.insert(item, at: index) self.selectedTab = item @@ -198,9 +195,8 @@ final class Editor: ObservableObject, Identifiable { selectedTab = item if !fromHistory { - history.removeFirst(historyOffset) - history.prepend(item) - historyOffset = 0 + clearFuture() + addToHistory(item) } do { try openFile(item: item) @@ -225,31 +221,8 @@ final class Editor: ObservableObject, Identifiable { CodeEditDocumentController.shared.addDocument(codeFile) } - func goBackInHistory() { - if canGoBackInHistory { - historyOffset += 1 - } - } - - func goForwardInHistory() { - if canGoForwardInHistory { - historyOffset -= 1 - } - } - - // TODO: move to @Observable so this works better - /// Warning: NOT published! - var canGoBackInHistory: Bool { - historyOffset != history.count-1 && !history.isEmpty - } - - // TODO: move to @Observable so this works better - /// Warning: NOT published! - var canGoForwardInHistory: Bool { - historyOffset != 0 - } - /// Check if tab can be closed + /// /// If document edited it will show dialog where user can save document before closing or cancel. private func canCloseTab(file: CEWorkspaceFile) -> Bool { guard let codeFile = file.fileDocument else { return true } diff --git a/CodeEdit/Features/Editor/TabBar/Tabs/Tab/EditorTabView.swift b/CodeEdit/Features/Editor/TabBar/Tabs/Tab/EditorTabView.swift index 7e46b7879..0b517865d 100644 --- a/CodeEdit/Features/Editor/TabBar/Tabs/Tab/EditorTabView.swift +++ b/CodeEdit/Features/Editor/TabBar/Tabs/Tab/EditorTabView.swift @@ -89,9 +89,8 @@ struct EditorTabView: View { if editor.selectedTab?.file != item { let tabItem = EditorInstance(file: item) editor.selectedTab = tabItem - editor.history.removeFirst(editor.historyOffset) - editor.history.prepend(tabItem) - editor.historyOffset = 0 + editor.clearFuture() + editor.addToHistory(tabItem) } } diff --git a/CodeEdit/Features/Editor/TabBar/Views/EditorHistoryMenus.swift b/CodeEdit/Features/Editor/TabBar/Views/EditorHistoryMenus.swift new file mode 100644 index 000000000..6f03d0fbf --- /dev/null +++ b/CodeEdit/Features/Editor/TabBar/Views/EditorHistoryMenus.swift @@ -0,0 +1,78 @@ +// +// EditorHistoryMenus.swift +// CodeEdit +// +// Created by Khan Winter on 7/8/24. +// + +import SwiftUI + +struct EditorHistoryMenus: View { + @EnvironmentObject private var editorManager: EditorManager + @EnvironmentObject private var editor: Editor + + var body: some View { + Group { + Menu { + ForEach( + Array(editor.history.dropFirst(editor.historyOffset+1).enumerated()), + id: \.offset + ) { index, tab in + Button { + editorManager.activeEditor = editor + editor.historyOffset += index + 1 + } label: { + HStack { + tab.file.icon + Text(tab.file.name) + } + } + } + } label: { + Image(systemName: "chevron.left") + .opacity(editor.historyOffset == editor.history.count - 1 || editor.history.isEmpty ? 0.5 : 1) + .frame(height: EditorTabBarView.height - 2) + .padding(.horizontal, 4) + } primaryAction: { + editorManager.activeEditor = editor + editor.goBackInHistory() + } + .disabled(editor.historyOffset == editor.history.count - 1 || editor.history.isEmpty) + .help("Navigate back") + + Menu { + ForEach( + Array(editor.history.prefix(editor.historyOffset).reversed().enumerated()), + id: \.offset + ) { index, tab in + Button { + editorManager.activeEditor = editor + editor.historyOffset -= index + 1 + } label: { + HStack { + tab.file.icon + Text(tab.file.name) + } + } + } + } label: { + Image(systemName: "chevron.right") + .opacity(editor.historyOffset == 0 ? 0.5 : 1) + .frame(height: EditorTabBarView.height - 2) + .padding(.horizontal, 4) + } primaryAction: { + editorManager.activeEditor = editor + editor.goForwardInHistory() + } + .disabled(editor.historyOffset == 0) + .help("Navigate forward") + } + .buttonStyle(.icon) + .controlSize(.small) + .font(EditorTabBarAccessoryIcon.iconFont) + } +} + +#Preview { + EditorHistoryMenus() +} diff --git a/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarLeadingAccessories.swift b/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarLeadingAccessories.swift index 07d1874fd..022ca790c 100644 --- a/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarLeadingAccessories.swift +++ b/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarLeadingAccessories.swift @@ -12,7 +12,6 @@ struct EditorTabBarLeadingAccessories: View { private var activeState @EnvironmentObject private var editorManager: EditorManager - @EnvironmentObject private var editor: Editor @State private var otherEditor: Editor? @@ -53,64 +52,7 @@ struct EditorTabBarLeadingAccessories: View { .padding(.horizontal, 4) } - Group { - Menu { - ForEach( - Array(editor.history.dropFirst(editor.historyOffset+1).enumerated()), - id: \.offset - ) { index, tab in - Button { - editorManager.activeEditor = editor - editor.historyOffset += index + 1 - } label: { - HStack { - tab.file.icon - Text(tab.file.name) - } - } - } - } label: { - Image(systemName: "chevron.left") - .opacity(editor.historyOffset == editor.history.count-1 || editor.history.isEmpty ? 0.5 : 1) - .frame(height: EditorTabBarView.height - 2) - .padding(.horizontal, 4) - } primaryAction: { - editorManager.activeEditor = editor - editor.goBackInHistory() - } - .disabled(editor.historyOffset == editor.history.count-1 || editor.history.isEmpty) - .help("Navigate back") - - Menu { - ForEach( - Array(editor.history.prefix(editor.historyOffset).reversed().enumerated()), - id: \.offset - ) { index, tab in - Button { - editorManager.activeEditor = editor - editor.historyOffset -= index + 1 - } label: { - HStack { - tab.file.icon - Text(tab.file.name) - } - } - } - } label: { - Image(systemName: "chevron.right") - .opacity(editor.historyOffset == 0 ? 0.5 : 1) - .frame(height: EditorTabBarView.height - 2) - .padding(.horizontal, 4) - } primaryAction: { - editorManager.activeEditor = editor - editor.goForwardInHistory() - } - .disabled(editor.historyOffset == 0) - .help("Navigate forward") - } - .buttonStyle(.icon) - .controlSize(.small) - .font(EditorTabBarAccessoryIcon.iconFont) + EditorHistoryMenus() } .foregroundColor(.secondary) .buttonStyle(.plain) diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift index 851f0c596..04feef4ce 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift @@ -73,8 +73,6 @@ struct ProjectNavigatorOutlineView: NSViewControllerRepresentable { for item in updatedItems { outlineView.reloadItem(item, reloadChildren: true) } - - controller?.updateSelection(itemID: workspace.editorManager.activeEditor.selectedTab?.file.id) } deinit { diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift index 2761c2382..480e81a1d 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift @@ -292,7 +292,9 @@ extension ProjectNavigatorViewController: NSOutlineViewDelegate { if !item.isFolder && shouldSendSelectionUpdate { DispatchQueue.main.async { + self.shouldSendSelectionUpdate = false self.workspace?.editorManager.activeEditor.openTab(file: item, asTemporary: true) + self.shouldSendSelectionUpdate = true } } } @@ -302,12 +304,12 @@ extension ProjectNavigatorViewController: NSOutlineViewDelegate { } func outlineViewItemDidExpand(_ notification: Notification) { - guard - let id = workspace?.editorManager.activeEditor.selectedTab?.file.id, - let item = workspace?.workspaceFileManager?.getFile(id, createIfNotFound: true) - else { return } - /// update outline selection only if the parent of selected item match with expanded item - guard item.parent === notification.userInfo?["NSObject"] as? CEWorkspaceFile else { return } + guard let id = workspace?.editorManager.activeEditor.selectedTab?.file.id, + let item = workspace?.workspaceFileManager?.getFile(id, createIfNotFound: true), + /// update outline selection only if the parent of selected item match with expanded item + item.parent === notification.userInfo?["NSObject"] as? CEWorkspaceFile else { + return + } /// select active file under collapsed folder only if its parent is expanding if outlineView.isItemExpanded(item.parent) { updateSelection(itemID: item.id) @@ -359,7 +361,9 @@ extension ProjectNavigatorViewController: NSOutlineViewDelegate { expandParent(item: parent) } let row = outlineView.row(forItem: fileItem) + shouldSendSelectionUpdate = false outlineView.selectRowIndexes(.init(integer: row), byExtendingSelection: false) + shouldSendSelectionUpdate = true if row < 0 { let alert = NSAlert()