Skip to content

Commit 451d4c2

Browse files
Fix Navigator Key Navigation Explosion (#1803)
1 parent f3524cc commit 451d4c2

File tree

8 files changed

+189
-116
lines changed

8 files changed

+189
-116
lines changed

CodeEdit.xcodeproj/project.pbxproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,7 @@
382382
6C5B63DE29C76213005454BA /* WindowCodeFileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C5B63DD29C76213005454BA /* WindowCodeFileView.swift */; };
383383
6C5C891B2A3F736500A94FE1 /* FocusedValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C5C891A2A3F736500A94FE1 /* FocusedValues.swift */; };
384384
6C5FDF7A29E6160000BC08C0 /* AppSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C5FDF7929E6160000BC08C0 /* AppSettings.swift */; };
385+
6C6362D42C3E321A0025570D /* Editor+History.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C6362D32C3E321A0025570D /* Editor+History.swift */; };
385386
6C66C31329D05CDC00DE9ED2 /* GRDB in Frameworks */ = {isa = PBXBuildFile; productRef = 6C66C31229D05CDC00DE9ED2 /* GRDB */; };
386387
6C6BD6EF29CD12E900235D17 /* ExtensionManagerWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C6BD6EE29CD12E900235D17 /* ExtensionManagerWindow.swift */; };
387388
6C6BD6F129CD13FA00235D17 /* ExtensionDiscovery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C6BD6F029CD13FA00235D17 /* ExtensionDiscovery.swift */; };
@@ -404,6 +405,7 @@
404405
6C85BB402C2105ED00EB5DEF /* CodeEditKit in Frameworks */ = {isa = PBXBuildFile; productRef = 6C85BB3F2C2105ED00EB5DEF /* CodeEditKit */; };
405406
6C85BB412C21061A00EB5DEF /* GitHubComment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587B9E3C29301D8F00AC7927 /* GitHubComment.swift */; };
406407
6C85BB442C210EFD00EB5DEF /* SwiftUIIntrospect in Frameworks */ = {isa = PBXBuildFile; productRef = 6C85BB432C210EFD00EB5DEF /* SwiftUIIntrospect */; };
408+
6C85F7562C3CA638008E9836 /* EditorHistoryMenus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C85F7552C3CA638008E9836 /* EditorHistoryMenus.swift */; };
407409
6C91D57229B176FF0059A90D /* EditorManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C91D57129B176FF0059A90D /* EditorManager.swift */; };
408410
6C9619202C3F27E3009733CE /* ProjectNavigatorUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C96191B2C3F27E3009733CE /* ProjectNavigatorUITests.swift */; };
409411
6C9619222C3F27F1009733CE /* Query.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C9619212C3F27F1009733CE /* Query.swift */; };
@@ -1004,6 +1006,7 @@
10041006
6C5B63DD29C76213005454BA /* WindowCodeFileView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WindowCodeFileView.swift; sourceTree = "<group>"; };
10051007
6C5C891A2A3F736500A94FE1 /* FocusedValues.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FocusedValues.swift; sourceTree = "<group>"; };
10061008
6C5FDF7929E6160000BC08C0 /* AppSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettings.swift; sourceTree = "<group>"; };
1009+
6C6362D32C3E321A0025570D /* Editor+History.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Editor+History.swift"; sourceTree = "<group>"; };
10071010
6C6BD6EE29CD12E900235D17 /* ExtensionManagerWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionManagerWindow.swift; sourceTree = "<group>"; };
10081011
6C6BD6F029CD13FA00235D17 /* ExtensionDiscovery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionDiscovery.swift; sourceTree = "<group>"; };
10091012
6C6BD6F529CD145F00235D17 /* ExtensionInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionInfo.swift; sourceTree = "<group>"; };
@@ -1017,6 +1020,7 @@
10171020
6C82D6B829BFE34900495C54 /* HelpCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HelpCommands.swift; sourceTree = "<group>"; };
10181021
6C82D6BB29C00CD900495C54 /* FirstResponderPropertyWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirstResponderPropertyWrapper.swift; sourceTree = "<group>"; };
10191022
6C82D6C529C012AD00495C54 /* NSApp+openWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSApp+openWindow.swift"; sourceTree = "<group>"; };
1023+
6C85F7552C3CA638008E9836 /* EditorHistoryMenus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditorHistoryMenus.swift; sourceTree = "<group>"; };
10201024
6C91D57129B176FF0059A90D /* EditorManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditorManager.swift; sourceTree = "<group>"; };
10211025
6C96191B2C3F27E3009733CE /* ProjectNavigatorUITests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProjectNavigatorUITests.swift; sourceTree = "<group>"; };
10221026
6C9619212C3F27F1009733CE /* Query.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Query.swift; sourceTree = "<group>"; };
@@ -2319,6 +2323,7 @@
23192323
DE6F77862813625500D00A76 /* EditorTabBarDivider.swift */,
23202324
287776E827E34BC700D46668 /* EditorTabBarView.swift */,
23212325
B6AB09A22AAABFEC0003A3A6 /* EditorTabBarLeadingAccessories.swift */,
2326+
6C85F7552C3CA638008E9836 /* EditorHistoryMenus.swift */,
23222327
B6AB09A42AAAC00F0003A3A6 /* EditorTabBarTrailingAccessories.swift */,
23232328
);
23242329
path = Views;
@@ -3030,6 +3035,7 @@
30303035
isa = PBXGroup;
30313036
children = (
30323037
6C147C3D29A3281D0089B630 /* Editor.swift */,
3038+
6C6362D32C3E321A0025570D /* Editor+History.swift */,
30333039
5994B6D92BD6B408006A4C5F /* Editor+TabSwitch.swift */,
30343040
6CA1AE942B46950000378EAB /* EditorInstance.swift */,
30353041
6C147C3E29A3281D0089B630 /* EditorLayout.swift */,
@@ -3998,6 +4004,7 @@
39984004
30B088162C0D53080063A882 /* LSPCache.swift in Sources */,
39994005
B6F0517929D9E3C900D72287 /* SourceControlGitView.swift in Sources */,
40004006
587B9E8329301D8F00AC7927 /* GitHubPullRequest.swift in Sources */,
4007+
6C85F7562C3CA638008E9836 /* EditorHistoryMenus.swift in Sources */,
40014008
5878DA82291863F900DD95A3 /* AcknowledgementsView.swift in Sources */,
40024009
587B9E8529301D8F00AC7927 /* GitHubReview.swift in Sources */,
40034010
58D01C9A293167DC00C5B6B4 /* CodeEditKeychain.swift in Sources */,
@@ -4033,6 +4040,7 @@
40334040
30B087FC2C0D53080063A882 /* LanguageServer+CallHierarchy.swift in Sources */,
40344041
6CFF967C29BEBD5200182D6F /* WindowCommands.swift in Sources */,
40354042
587B9E7229301D8F00AC7927 /* GitJSONPostRouter.swift in Sources */,
4043+
6C6362D42C3E321A0025570D /* Editor+History.swift in Sources */,
40364044
6C85BB412C21061A00EB5DEF /* GitHubComment.swift in Sources */,
40374045
5878DAB0291D627C00DD95A3 /* EditorPathBarMenu.swift in Sources */,
40384046
04BA7C242AE2E7CD00584E1C /* SourceControlNavigatorSyncView.swift in Sources */,
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
//
2+
// Editor+History.swift
3+
// CodeEdit
4+
//
5+
// Created by Khan Winter on 7/9/24.
6+
//
7+
8+
import Foundation
9+
10+
/// Methods for modifying the history list on the editor.
11+
extension Editor {
12+
/// Add the tab to the history list.
13+
/// - Parameter tab: The tab to add to the history.
14+
func addToHistory(_ tab: Tab) {
15+
if history.first != tab {
16+
history.prepend(tab)
17+
}
18+
}
19+
20+
/// Clear any tabs in the "future" on the history list. Resets the history offset and removes any tabs that were
21+
/// available to navigate forwards to.
22+
func clearFuture() {
23+
guard historyOffset > 0 else { return } // nothing to clear, avoid an out of bounds error
24+
history.removeFirst(historyOffset)
25+
historyOffset = 0
26+
}
27+
28+
/// Move backwards in the history list by one place.
29+
func goBackInHistory() {
30+
if canGoBackInHistory {
31+
historyOffset += 1
32+
}
33+
}
34+
35+
/// Move forwards in the history list by one place.
36+
func goForwardInHistory() {
37+
if canGoForwardInHistory {
38+
historyOffset -= 1
39+
}
40+
}
41+
42+
// TODO: move to @Observable so this works better
43+
/// Warning: NOT published!
44+
var canGoBackInHistory: Bool {
45+
historyOffset != history.count - 1 && !history.isEmpty
46+
}
47+
48+
// TODO: move to @Observable so this works better
49+
/// Warning: NOT published!
50+
var canGoForwardInHistory: Bool {
51+
historyOffset != 0
52+
}
53+
54+
/// Called by the ``Editor`` class when the history offset is changed.
55+
///
56+
/// This method updates the selected tab to the current tab in the history offset.
57+
/// If the tab is not opened, it is opened without modifying the history list.
58+
/// - Warning: Do not use except in the ``historyOffset``'s `didSet`.
59+
func historyOffsetDidChange() {
60+
let tab = history[historyOffset]
61+
62+
if !tabs.contains(tab) {
63+
if let temporaryTab, tabs.contains(temporaryTab) {
64+
closeTab(file: temporaryTab.file, fromHistory: true)
65+
}
66+
temporaryTab = tab
67+
openTab(file: tab.file, fromHistory: true)
68+
}
69+
selectedTab = tab
70+
}
71+
}

CodeEdit/Features/Editor/Models/Editor.swift

Lines changed: 19 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -35,22 +35,16 @@ final class Editor: ObservableObject, Identifiable {
3535
}
3636

3737
/// The current offset in the history list.
38+
/// When set, updates the ``selectedTab`` to the tab indicated by the offset.
39+
/// See the ``historyOffsetDidChange()`` method for more details.
3840
@Published var historyOffset: Int = 0 {
3941
didSet {
40-
let tab = history[historyOffset]
41-
42-
if !tabs.contains(tab) {
43-
if let temporaryTab, tabs.contains(temporaryTab) {
44-
closeTab(file: temporaryTab.file, fromHistory: true)
45-
}
46-
temporaryTab = tab
47-
openTab(file: tab.file, fromHistory: true)
48-
}
49-
selectedTab = tab
42+
historyOffsetDidChange()
5043
}
5144
}
5245

53-
/// History of tab switching.
46+
/// Maintains the list of tabs that have been switched to.
47+
/// - Warning: Use the ``addToHistory(_:)`` or ``clearFuture()`` methods to modify this. Do not modify directly.
5448
@Published var history: Deque<Tab> = []
5549

5650
/// Currently selected tab.
@@ -106,22 +100,26 @@ final class Editor: ObservableObject, Identifiable {
106100

107101
/// Closes a tab in the editor.
108102
/// This will also write any changes to the file on disk and will add the tab to the tab history.
109-
/// - Parameter item: the tab to close.
103+
/// - Parameters:
104+
/// - file: The tab to close
105+
/// - fromHistory: If `true`, does not clear tabs ahead of the ``historyOffset``
106+
/// Used when opening tabs from the history queue where tabs ahead of the ``historyOffset`` should
107+
/// not be removed.
110108
func closeTab(file: CEWorkspaceFile, fromHistory: Bool = false) {
111109
guard canCloseTab(file: file) else { return }
112110

113111
if temporaryTab?.file == file {
114112
temporaryTab = nil
115113
}
116114
if !fromHistory {
117-
historyOffset = 0
115+
clearFuture()
118116
}
119117
if file != selectedTab?.file {
120-
history.prepend(EditorInstance(file: file))
118+
addToHistory(EditorInstance(file: file))
121119
}
122120
removeTab(file)
123121
if let selectedTab {
124-
history.prepend(selectedTab)
122+
addToHistory(selectedTab)
125123
}
126124
// Reset change count to 0
127125
file.fileDocument?.updateChangeCount(.changeCleared)
@@ -148,16 +146,15 @@ final class Editor: ObservableObject, Identifiable {
148146
// Item is already opened in a tab.
149147
guard !tabs.contains(item) || !asTemporary else {
150148
selectedTab = item
151-
history.prepend(item)
149+
addToHistory(item)
152150
return
153151
}
154152

155153
switch (temporaryTab, asTemporary) {
156154
case (.some(let tab), true):
157155
if let index = tabs.firstIndex(of: tab) {
158-
history.removeFirst(historyOffset)
159-
history.prepend(item)
160-
historyOffset = 0
156+
clearFuture()
157+
addToHistory(item)
161158
tabs.remove(tab)
162159
tabs.insert(item, at: index)
163160
self.selectedTab = item
@@ -198,9 +195,8 @@ final class Editor: ObservableObject, Identifiable {
198195

199196
selectedTab = item
200197
if !fromHistory {
201-
history.removeFirst(historyOffset)
202-
history.prepend(item)
203-
historyOffset = 0
198+
clearFuture()
199+
addToHistory(item)
204200
}
205201
do {
206202
try openFile(item: item)
@@ -225,31 +221,8 @@ final class Editor: ObservableObject, Identifiable {
225221
CodeEditDocumentController.shared.addDocument(codeFile)
226222
}
227223

228-
func goBackInHistory() {
229-
if canGoBackInHistory {
230-
historyOffset += 1
231-
}
232-
}
233-
234-
func goForwardInHistory() {
235-
if canGoForwardInHistory {
236-
historyOffset -= 1
237-
}
238-
}
239-
240-
// TODO: move to @Observable so this works better
241-
/// Warning: NOT published!
242-
var canGoBackInHistory: Bool {
243-
historyOffset != history.count-1 && !history.isEmpty
244-
}
245-
246-
// TODO: move to @Observable so this works better
247-
/// Warning: NOT published!
248-
var canGoForwardInHistory: Bool {
249-
historyOffset != 0
250-
}
251-
252224
/// Check if tab can be closed
225+
///
253226
/// If document edited it will show dialog where user can save document before closing or cancel.
254227
private func canCloseTab(file: CEWorkspaceFile) -> Bool {
255228
guard let codeFile = file.fileDocument else { return true }

CodeEdit/Features/Editor/TabBar/Tabs/Tab/EditorTabView.swift

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,8 @@ struct EditorTabView: View {
8989
if editor.selectedTab?.file != item {
9090
let tabItem = EditorInstance(file: item)
9191
editor.selectedTab = tabItem
92-
editor.history.removeFirst(editor.historyOffset)
93-
editor.history.prepend(tabItem)
94-
editor.historyOffset = 0
92+
editor.clearFuture()
93+
editor.addToHistory(tabItem)
9594
}
9695
}
9796

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
//
2+
// EditorHistoryMenus.swift
3+
// CodeEdit
4+
//
5+
// Created by Khan Winter on 7/8/24.
6+
//
7+
8+
import SwiftUI
9+
10+
struct EditorHistoryMenus: View {
11+
@EnvironmentObject private var editorManager: EditorManager
12+
@EnvironmentObject private var editor: Editor
13+
14+
var body: some View {
15+
Group {
16+
Menu {
17+
ForEach(
18+
Array(editor.history.dropFirst(editor.historyOffset+1).enumerated()),
19+
id: \.offset
20+
) { index, tab in
21+
Button {
22+
editorManager.activeEditor = editor
23+
editor.historyOffset += index + 1
24+
} label: {
25+
HStack {
26+
tab.file.icon
27+
Text(tab.file.name)
28+
}
29+
}
30+
}
31+
} label: {
32+
Image(systemName: "chevron.left")
33+
.opacity(editor.historyOffset == editor.history.count - 1 || editor.history.isEmpty ? 0.5 : 1)
34+
.frame(height: EditorTabBarView.height - 2)
35+
.padding(.horizontal, 4)
36+
} primaryAction: {
37+
editorManager.activeEditor = editor
38+
editor.goBackInHistory()
39+
}
40+
.disabled(editor.historyOffset == editor.history.count - 1 || editor.history.isEmpty)
41+
.help("Navigate back")
42+
43+
Menu {
44+
ForEach(
45+
Array(editor.history.prefix(editor.historyOffset).reversed().enumerated()),
46+
id: \.offset
47+
) { index, tab in
48+
Button {
49+
editorManager.activeEditor = editor
50+
editor.historyOffset -= index + 1
51+
} label: {
52+
HStack {
53+
tab.file.icon
54+
Text(tab.file.name)
55+
}
56+
}
57+
}
58+
} label: {
59+
Image(systemName: "chevron.right")
60+
.opacity(editor.historyOffset == 0 ? 0.5 : 1)
61+
.frame(height: EditorTabBarView.height - 2)
62+
.padding(.horizontal, 4)
63+
} primaryAction: {
64+
editorManager.activeEditor = editor
65+
editor.goForwardInHistory()
66+
}
67+
.disabled(editor.historyOffset == 0)
68+
.help("Navigate forward")
69+
}
70+
.buttonStyle(.icon)
71+
.controlSize(.small)
72+
.font(EditorTabBarAccessoryIcon.iconFont)
73+
}
74+
}
75+
76+
#Preview {
77+
EditorHistoryMenus()
78+
}

0 commit comments

Comments
 (0)