diff --git a/CodeEdit/Features/Editor/Models/Editor/Editor.swift b/CodeEdit/Features/Editor/Models/Editor/Editor.swift index 0ec2765154..782b956b71 100644 --- a/CodeEdit/Features/Editor/Models/Editor/Editor.swift +++ b/CodeEdit/Features/Editor/Models/Editor/Editor.swift @@ -9,8 +9,13 @@ import Foundation import OrderedCollections import DequeModule import AppKit +import OSLog final class Editor: ObservableObject, Identifiable { + enum EditorError: Error { + case noWorkspaceAttached + } + typealias Tab = EditorInstance /// Set of open tabs. @@ -57,6 +62,8 @@ final class Editor: ObservableObject, Identifiable { weak var parent: SplitViewData? weak var workspace: WorkspaceDocument? + private let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "", category: "Editor") + init() { self.tabs = [] self.temporaryTab = nil @@ -185,29 +192,45 @@ final class Editor: ObservableObject, Identifiable { switch (temporaryTab, asTemporary) { case (.some(let tab), true): - if let index = tabs.firstIndex(of: tab) { - clearFuture() - addToHistory(item) - tabs.remove(tab) - tabs.insert(item, at: index) - self.selectedTab = item - temporaryTab = item - } - + replaceTemporaryTab(tab: tab, with: item) case (.some(let tab), false) where tab == item: temporaryTab = nil case (.some(let tab), false) where tab != item: openTab(file: item.file) - + case (.some, false): + // A temporary tab exists, but we don't want to open this one as temporary. + // Clear the temp tab and open the new one. + openTab(file: item.file) case (.none, true): openTab(file: item.file) temporaryTab = item - case (.none, false): openTab(file: item.file) + } + } + + /// Replaces the given temporary tab with a new tab item. + /// - Parameters: + /// - tab: The temporary tab to replace. + /// - newItem: The new tab to replace it with and open as a temporary tab. + private func replaceTemporaryTab(tab: Tab, with newItem: Tab) { + if let index = tabs.firstIndex(of: tab) { + do { + try openFile(item: newItem) + } catch { + logger.error("Error opening file: \(error)") + } - default: - break + clearFuture() + addToHistory(newItem) + tabs.remove(tab) + tabs.insert(newItem, at: index) + self.selectedTab = newItem + temporaryTab = newItem + } else { + // If we couldn't find the current temporary tab (invalid state) we should still do *something* + openTab(file: newItem.file) + temporaryTab = newItem } } @@ -236,16 +259,20 @@ final class Editor: ObservableObject, Identifiable { do { try openFile(item: item) } catch { - print(error) + logger.error("Error opening file: \(error)") } } private func openFile(item: Tab) throws { // If this isn't attached to a workspace, loading a new NSDocument will cause a loose document we can't close - guard item.file.fileDocument == nil && workspace != nil else { + guard item.file.fileDocument == nil else { return } + guard workspace != nil else { + throw EditorError.noWorkspaceAttached + } + try item.file.loadCodeFile() } diff --git a/CodeEdit/Features/Editor/Models/EditorLayout/EditorLayout+StateRestoration.swift b/CodeEdit/Features/Editor/Models/EditorLayout/EditorLayout+StateRestoration.swift index b8692f974c..bb273a09a1 100644 --- a/CodeEdit/Features/Editor/Models/EditorLayout/EditorLayout+StateRestoration.swift +++ b/CodeEdit/Features/Editor/Models/EditorLayout/EditorLayout+StateRestoration.swift @@ -13,6 +13,13 @@ extension EditorManager { /// Restores the tab manager from a captured state obtained using `saveRestorationState` /// - Parameter workspace: The workspace to retrieve state from. func restoreFromState(_ workspace: WorkspaceDocument) { + defer { + // No matter what, set the workspace on each editor. Even if we fail to read data. + flattenedEditors.forEach { editor in + editor.workspace = workspace + } + } + do { guard let data = workspace.getFromWorkspaceState(.openTabs) as? Data else { return diff --git a/CodeEdit/Features/Editor/Views/EditorAreaView.swift b/CodeEdit/Features/Editor/Views/EditorAreaView.swift index c617f8dd30..544aed54e5 100644 --- a/CodeEdit/Features/Editor/Views/EditorAreaView.swift +++ b/CodeEdit/Features/Editor/Views/EditorAreaView.swift @@ -77,7 +77,6 @@ struct EditorAreaView: View { self.codeFile = { [weak latestValue] in latestValue } } } - } else { CEContentUnavailableView("No Editor") .padding(.top, editorInsetAmount) diff --git a/CodeEditUITests/Features/NavigatorArea/ProjectNavigator/ProjectNavigatorFileManagementUITests.swift b/CodeEditUITests/Features/NavigatorArea/ProjectNavigator/ProjectNavigatorFileManagementUITests.swift index 72cb59f782..4335ed8694 100644 --- a/CodeEditUITests/Features/NavigatorArea/ProjectNavigator/ProjectNavigatorFileManagementUITests.swift +++ b/CodeEditUITests/Features/NavigatorArea/ProjectNavigator/ProjectNavigatorFileManagementUITests.swift @@ -57,6 +57,7 @@ final class ProjectNavigatorFileManagementUITests: XCTestCase { XCTFail("newFile.txt did not appear") return } + guard Query.Navigator.getProjectNavigatorRow( fileTitle: "New Folder", navigator @@ -95,6 +96,15 @@ final class ProjectNavigatorFileManagementUITests: XCTestCase { let newFileRow = selectedRows.firstMatch XCTAssertEqual(newFileRow.descendants(matching: .textField).firstMatch.value as? String, title) + + let tabBar = Query.Window.getTabBar(window) + XCTAssertTrue(tabBar.exists) + let readmeTab = Query.TabBar.getTab(labeled: title, tabBar) + XCTAssertTrue(readmeTab.exists) + + let newFileEditor = Query.Window.getFirstEditor(window) + XCTAssertTrue(newFileEditor.exists) + XCTAssertNotNil(newFileEditor.value as? String) } } } diff --git a/CodeEditUITests/Features/NavigatorArea/ProjectNavigator/ProjectNavigatorUITests.swift b/CodeEditUITests/Features/NavigatorArea/ProjectNavigator/ProjectNavigatorUITests.swift index ca834c4488..baac35964a 100644 --- a/CodeEditUITests/Features/NavigatorArea/ProjectNavigator/ProjectNavigatorUITests.swift +++ b/CodeEditUITests/Features/NavigatorArea/ProjectNavigator/ProjectNavigatorUITests.swift @@ -35,6 +35,10 @@ final class ProjectNavigatorUITests: XCTestCase { let readmeTab = Query.TabBar.getTab(labeled: "README.md", tabBar) XCTAssertTrue(readmeTab.exists) + let readmeEditor = Query.Window.getFirstEditor(window) + XCTAssertTrue(readmeEditor.exists) + XCTAssertNotNil(readmeEditor.value as? String) + let rowCount = navigator.descendants(matching: .outlineRow).count // Open a folder diff --git a/CodeEditUITests/Query.swift b/CodeEditUITests/Query.swift index 7e9387d09d..fab5eda77e 100644 --- a/CodeEditUITests/Query.swift +++ b/CodeEditUITests/Query.swift @@ -43,6 +43,12 @@ enum Query { static func getUtilityArea(_ window: XCUIElement) -> XCUIElement { return window.descendants(matching: .any).matching(identifier: "UtilityArea").element } + + static func getFirstEditor(_ window: XCUIElement) -> XCUIElement { + return window.descendants(matching: .any) + .matching(NSPredicate(format: "label CONTAINS[c] 'Text Editor'")) + .firstMatch + } } enum Navigator {