diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift b/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift index bd43c4876b..e582080fc0 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift @@ -15,6 +15,14 @@ private extension CGFloat { } final class CodeEditSplitViewController: NSSplitViewController { + private var workspace: WorkspaceDocument + private let widthStateName: String = "\(String(describing: CodeEditSplitViewController.self))-Width" + private let isNavigatorCollapsedStateName: String + = "\(String(describing: CodeEditSplitViewController.self))-IsNavigatorCollapsed" + private let isInspectorCollapsedStateName: String + = "\(String(describing: CodeEditSplitViewController.self))-IsInspectorCollapsed" + private var setWidthFromState = false + // Properties private(set) var isSnapped: Bool = false { willSet { @@ -29,7 +37,8 @@ final class CodeEditSplitViewController: NSSplitViewController { // MARK: - Initialization - init(feedbackPerformer: NSHapticFeedbackPerformer) { + init(workspace: WorkspaceDocument, feedbackPerformer: NSHapticFeedbackPerformer) { + self.workspace = workspace self.feedbackPerformer = feedbackPerformer super.init(nibName: nil, bundle: nil) } @@ -39,10 +48,26 @@ final class CodeEditSplitViewController: NSSplitViewController { fatalError("init(coder:) has not been implemented") } - // TODO: Set user preferences width if it is not the snap width -// override func viewWillAppear() { -// super.viewWillAppear() -// } + override func viewWillAppear() { + super.viewWillAppear() + let width = workspace.getFromWorkspaceState(key: self.widthStateName) as? CGFloat + splitView.setPosition(width ?? .snapWidth, ofDividerAt: .zero) + setWidthFromState = true + + if let firstSplitView = splitViewItems.first { + firstSplitView.isCollapsed = workspace.getFromWorkspaceState( + key: isNavigatorCollapsedStateName + ) as? Bool ?? false + } + + if let lastSplitView = splitViewItems.last { + lastSplitView.isCollapsed = workspace.getFromWorkspaceState( + key: isInspectorCollapsedStateName + ) as? Bool ?? true + } + + self.insertToolbarItemIfNeeded() + } // MARK: - NSSplitViewDelegate @@ -78,6 +103,28 @@ final class CodeEditSplitViewController: NSSplitViewController { return proposedPosition } + override func splitViewDidResizeSubviews(_ notification: Notification) { + guard let resizedDivider = notification.userInfo?["NSSplitViewDividerIndex"] as? Int else { + return + } + + if resizedDivider == 0 { + let panel = splitView.subviews[0] + let width = panel.frame.size.width + if width > 0 && setWidthFromState { + workspace.addToWorkspaceState(key: self.widthStateName, value: width) + } + } + } + + func saveNavigatorCollapsedState(isCollapsed: Bool) { + workspace.addToWorkspaceState(key: isNavigatorCollapsedStateName, value: isCollapsed) + } + + func saveInspectorCollapsedState(isCollapsed: Bool) { + workspace.addToWorkspaceState(key: isInspectorCollapsedStateName, value: isCollapsed) + } + /// Quick fix for list tracking separator needing to be added again after closing, /// then opening the inspector with a drag. private func insertToolbarItemIfNeeded() { diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift index ae5859673d..f66edd98f1 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift @@ -68,7 +68,7 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate { private func setupSplitView(with workspace: WorkspaceDocument) { let feedbackPerformer = NSHapticFeedbackManager.defaultPerformer - let splitVC = CodeEditSplitViewController(feedbackPerformer: feedbackPerformer) + let splitVC = CodeEditSplitViewController(workspace: workspace, feedbackPerformer: feedbackPerformer) let navigatorView = NavigatorSidebarView(workspace: workspace) let navigator = NSSplitViewItem( @@ -215,6 +215,9 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate { @objc func toggleFirstPanel() { guard let firstSplitView = splitViewController.splitViewItems.first else { return } firstSplitView.animator().isCollapsed.toggle() + if let codeEditSplitVC = splitViewController as? CodeEditSplitViewController { + codeEditSplitVC.saveNavigatorCollapsedState(isCollapsed: firstSplitView.isCollapsed) + } } @objc func toggleLastPanel() { @@ -225,6 +228,9 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate { } else { window?.toolbar?.insertItem(withItemIdentifier: .itemListTrackingSeparator, at: 4) } + if let codeEditSplitVC = splitViewController as? CodeEditSplitViewController { + codeEditSplitVC.saveInspectorCollapsedState(isCollapsed: lastSplitView.isCollapsed) + } } private func getSelectedCodeFile() -> CodeFileDocument? { diff --git a/CodeEdit/Features/Documents/WorkspaceDocument.swift b/CodeEdit/Features/Documents/WorkspaceDocument.swift index 3713bd64bc..04bdbbca14 100644 --- a/CodeEdit/Features/Documents/WorkspaceDocument.swift +++ b/CodeEdit/Features/Documents/WorkspaceDocument.swift @@ -22,12 +22,27 @@ import CodeEditKit @Published var selectionState: WorkspaceSelectionState = .init() @Published var fileItems: [WorkspaceClient.FileItem] = [] + var workspaceState: [String: Any] { + get { + let key = "workspaceState-\(self.fileURL?.absoluteString ?? "")" + return UserDefaults.standard.object(forKey: key) as? [String: Any] ?? [:] + } + set { + let key = "workspaceState-\(self.fileURL?.absoluteString ?? "")" + UserDefaults.standard.set(newValue, forKey: key) + } + } + var statusBarModel: StatusBarViewModel? var searchState: SearchState? var quickOpenViewModel: QuickOpenViewModel? var commandsPaletteState: CommandPaletteViewModel? var listenerModel: WorkspaceNotificationModel = .init() + private var cancellables = Set() + private let openTabsStateName: String = "\(String(describing: WorkspaceDocument.self))-OpenTabs" + private let activeTabStateName: String = "\(String(describing: WorkspaceDocument.self))-ActiveTab" + private var openedTabsFromState = false @Published var targets: [Target] = [] @@ -36,6 +51,14 @@ import CodeEditKit NotificationCenter.default.removeObserver(self) } + func getFromWorkspaceState(key: String) -> Any? { + return workspaceState[key] + } + + func addToWorkspaceState(key: String, value: Any) { + workspaceState.updateValue(value, forKey: key) + } + // MARK: Open Tabs /// Opens new tab /// - Parameter item: any item which can be represented as a tab @@ -147,6 +170,30 @@ import CodeEditKit closeTabs(items: range) } + /// Switched the active tab to current tab + /// - Parameter item: tab item that is now active. + func switchedTab(item: TabBarItemRepresentable) { + selectionState.selectedId = item.tabID + guard let fileItem = item as? WorkspaceClient.FileItem else { return } + self.addToWorkspaceState(key: activeTabStateName, value: fileItem.url.absoluteString) + } + + /// Tabs reordered + /// - Parameter openedTabs: reordered tabs + func reorderedTabs(openedTabs: [TabBarItemID]) { + selectionState.openedTabs = openedTabs + + if openedTabsFromState { + var openTabsInState: [String] = [] + for openTabId in openedTabs { + guard let item = selectionState.getItemByTab(id: openTabId) as? WorkspaceClient.FileItem + else { continue } + openTabsInState.append(item.url.absoluteString) + } + self.addToWorkspaceState(key: openTabsStateName, value: openTabsInState) + } + } + /// Closes an open temporary tab, does not save the temporary tab's file. /// Removes the tab item from `openedCodeFiles`, `openedExtensions`, and `openFileItems`. private func closeTemporaryTab() { @@ -196,6 +243,14 @@ import CodeEditKit selectionState.openedCodeFiles.removeValue(forKey: item) selectionState.openFileItems.remove(at: openFileItemIndex) removeTab(id: item.tabID) + + if openedTabsFromState { + var openTabsInState = self.getFromWorkspaceState(key: openTabsStateName) as? [String] ?? [] + if let index = openTabsInState.firstIndex(of: item.url.absoluteString) { + openTabsInState.remove(at: index) + self.addToWorkspaceState(key: openTabsStateName, value: openTabsInState) + } + } } private func closeExtensionTab(item: Plugin) { @@ -209,8 +264,19 @@ import CodeEditKit @objc func convertTemporaryTab() { if selectionState.selectedId == selectionState.temporaryTab && selectionState.temporaryTab != nil { + let item = selectionState.getItemByTab(id: selectionState.temporaryTab!) selectionState.previousTemporaryTab = selectionState.temporaryTab selectionState.temporaryTab = nil + + guard let file = item as? WorkspaceClient.FileItem else { return } + + if openedTabsFromState && item != nil { + var openTabsInState = self.getFromWorkspaceState(key: openTabsStateName) as? [String] ?? [] + if !openTabsInState.contains(file.url.absoluteString) { + openTabsInState.append(file.url.absoluteString) + self.addToWorkspaceState(key: openTabsStateName, value: openTabsInState) + } + } } } @@ -269,6 +335,27 @@ import CodeEditKit windowController.shouldCascadeWindows = false windowController.window?.setFrameAutosaveName(self.fileURL?.absoluteString ?? "Untitled") self.addWindowController(windowController) + + var activeTabID: TabBarItemID? + var activeTabInState = self.getFromWorkspaceState(key: activeTabStateName) as? String ?? "" + var openTabsInState = self.getFromWorkspaceState(key: openTabsStateName) as? [String] ?? [] + for openTab in openTabsInState { + let tabUrl = URL(string: openTab)! + if FileManager.default.fileExists(atPath: tabUrl.path) { + let item = WorkspaceClient.FileItem(url: tabUrl) + self.openTab(item: item) + self.convertTemporaryTab() + if activeTabInState == openTab { + activeTabID = item.tabID + } + } + } + + if activeTabID != nil { + selectionState.selectedId = activeTabID + } + + self.openedTabsFromState = true } // MARK: Set Up Workspace @@ -282,7 +369,7 @@ import CodeEditKit self.searchState = .init(self) self.quickOpenViewModel = .init(fileURL: url) self.commandsPaletteState = .init() - self.statusBarModel = .init(workspaceURL: url) + self.statusBarModel = .init(workspace: self, workspaceURL: url) NotificationCenter.default.addObserver( self, @@ -292,26 +379,10 @@ import CodeEditKit ) } - /// Retrieves selection state from UserDefaults using SHA256 hash of project path as key - /// - Throws: `DecodingError.dataCorrupted` error if retrived data from UserDefaults is not decodable - /// - Returns: retrived state from UserDefaults or default state if not found - private func readSelectionState() throws -> WorkspaceSelectionState { - guard let path = fileURL?.path, - let data = UserDefaults.standard.value(forKey: path.sha256()) as? Data else { return selectionState } - let state = try PropertyListDecoder().decode(WorkspaceSelectionState.self, from: data) - return state - } - override func read(from url: URL, ofType typeName: String) throws { try initWorkspaceState(url) // Initialize Workspace - do { - selectionState = try readSelectionState() - } catch { - Swift.print("couldn't retrieve selection state from user defaults") - } - workspaceClient? .getFiles .sink { [weak self] files in @@ -355,22 +426,7 @@ import CodeEditKit // MARK: Close Workspace - /// Saves selection state to UserDefaults using SHA256 hash of project path as key - /// - Throws: `EncodingError.invalidValue` error if sellection state is not encodable - private func saveSelectionState() throws { - guard let path = fileURL?.path else { return } - let hash = path.sha256() - let data = try PropertyListEncoder().encode(selectionState) - UserDefaults.standard.set(data, forKey: hash) - } - override func close() { - do { - try saveSelectionState() - } catch { - Swift.print("couldn't save selection state from user defaults") - } - selectionState.selectedId = nil selectionState.openedCodeFiles.removeAll() diff --git a/CodeEdit/Features/StatusBar/ViewModels/StatusBarViewModel.swift b/CodeEdit/Features/StatusBar/ViewModels/StatusBarViewModel.swift index 9d1080c486..be0b08fa3c 100644 --- a/CodeEdit/Features/StatusBar/ViewModels/StatusBarViewModel.swift +++ b/CodeEdit/Features/StatusBar/ViewModels/StatusBarViewModel.swift @@ -12,6 +12,10 @@ import SwiftUI /// A model class to host and manage data for the ``StatusBarView`` /// class StatusBarViewModel: ObservableObject { + private let isStatusBarDrawerCollapsedStateName: String + = "\(String(describing: StatusBarViewModel.self))-IsStatusBarDrawerCollapsed" + private let statusBarDrawerHeightStateName: String + = "\(String(describing: StatusBarViewModel.self))-StatusBarDrawerHeight" // TODO: Implement logic for updating values // TODO: Add @Published vars for indentation, encoding, linebreak @@ -54,6 +58,8 @@ class StatusBarViewModel: ObservableObject { /// Returns the font for status bar items to use private(set) var toolbarFont: Font = .system(size: 11) + private(set) var workspace: WorkspaceDocument + /// The base URL of the workspace private(set) var workspaceURL: URL @@ -69,7 +75,27 @@ class StatusBarViewModel: ObservableObject { /// Initialize with a GitClient /// - Parameter workspaceURL: the current workspace URL - init(workspaceURL: URL) { + init(workspace: WorkspaceDocument, workspaceURL: URL) { + self.workspace = workspace self.workspaceURL = workspaceURL + + var currentHeight = workspace.getFromWorkspaceState(key: statusBarDrawerHeightStateName) as? Double + ?? self.standardHeight + if currentHeight == 0 { + currentHeight = self.standardHeight + } + + self.isExpanded = workspace.getFromWorkspaceState(key: isStatusBarDrawerCollapsedStateName) as? Bool ?? false + if self.isExpanded { + self.currentHeight = currentHeight + } + } + + func saveIsExpandedToState() { + self.workspace.addToWorkspaceState(key: isStatusBarDrawerCollapsedStateName, value: self.isExpanded) + } + + func saveHeightToState(height: Double) { + self.workspace.addToWorkspaceState(key: statusBarDrawerHeightStateName, value: height) } } diff --git a/CodeEdit/Features/StatusBar/Views/StatusBarDrawer/StatusBarDrawer.swift b/CodeEdit/Features/StatusBar/Views/StatusBarDrawer/StatusBarDrawer.swift index 9280bcb1b1..d9a24b5e1d 100644 --- a/CodeEdit/Features/StatusBar/Views/StatusBarDrawer/StatusBarDrawer.swift +++ b/CodeEdit/Features/StatusBar/Views/StatusBarDrawer/StatusBarDrawer.swift @@ -32,26 +32,31 @@ struct StatusBarDrawer: View { var body: some View { VStack(spacing: 0) { - switch model.selectedTab { - case 0: - TerminalEmulatorView(url: model.workspaceURL) - .background { - if colorScheme == .dark { - if prefs.preferences.theme.selectedTheme == prefs.preferences.theme.selectedLightTheme { - Color.white + GeometryReader { geometryProxy in + switch model.selectedTab { + case 0: + TerminalEmulatorView(url: model.workspaceURL) + .background { + if colorScheme == .dark { + if prefs.preferences.theme.selectedTheme == prefs.preferences.theme.selectedLightTheme { + Color.white + } else { + EffectView(.underPageBackground) + } } else { - EffectView(.underPageBackground) + if prefs.preferences.theme.selectedTheme == prefs.preferences.theme.selectedDarkTheme { + Color.black + } else { + EffectView(.contentBackground) + } } - } else { - if prefs.preferences.theme.selectedTheme == prefs.preferences.theme.selectedDarkTheme { - Color.black - } else { - EffectView(.contentBackground) - } - } - } - default: Rectangle().foregroundColor(Color(nsColor: .textBackgroundColor)) + // When size changes, save new height to workspace state. + .onChange(of: geometryProxy.size.height) { _ in + model.saveHeightToState(height: geometryProxy.size.height) + } + default: Rectangle().foregroundColor(Color(nsColor: .textBackgroundColor)) + } } HStack(alignment: .center, spacing: 10) { FilterTextField(title: "Filter", text: $searchText) diff --git a/CodeEdit/Features/StatusBar/Views/StatusBarItems/StatusBarToggleDrawerButton.swift b/CodeEdit/Features/StatusBar/Views/StatusBarItems/StatusBarToggleDrawerButton.swift index e465cd861b..bf0cf3e613 100644 --- a/CodeEdit/Features/StatusBar/Views/StatusBarItems/StatusBarToggleDrawerButton.swift +++ b/CodeEdit/Features/StatusBar/Views/StatusBarItems/StatusBarToggleDrawerButton.swift @@ -27,6 +27,7 @@ internal struct StatusBarToggleDrawerButton: View { model.currentHeight = 300 } } + self.model.saveIsExpandedToState() } internal var body: some View { diff --git a/CodeEdit/Features/TabBar/Views/TabBarItemView.swift b/CodeEdit/Features/TabBar/Views/TabBarItemView.swift index a228b4e6d1..0490821683 100644 --- a/CodeEdit/Features/TabBar/Views/TabBarItemView.swift +++ b/CodeEdit/Features/TabBar/Views/TabBarItemView.swift @@ -95,7 +95,7 @@ struct TabBarItemView: View { private func switchAction() { // Only set the `selectedId` when they are not equal to avoid performance issue for now. if workspace.selectionState.selectedId != item.tabID { - workspace.selectionState.selectedId = item.tabID + workspace.switchedTab(item: item) } } diff --git a/CodeEdit/Features/TabBar/Views/TabBarView.swift b/CodeEdit/Features/TabBar/Views/TabBarView.swift index 186bbc6aa6..edccdc66cb 100644 --- a/CodeEdit/Features/TabBar/Views/TabBarView.swift +++ b/CodeEdit/Features/TabBar/Views/TabBarView.swift @@ -237,7 +237,7 @@ struct TabBarView: View { // In order to avoid the lag due to the update of workspace state. DispatchQueue.main.asyncAfter(deadline: .now() + 0.40) { if draggingStartLocation == nil { - workspace.selectionState.openedTabs = openedTabs + workspace.reorderedTabs(openedTabs: openedTabs) } } }) diff --git a/CodeEditTests/Features/Documents/DocumentsUnitTests.swift b/CodeEditTests/Features/Documents/DocumentsUnitTests.swift index 73ef89c2c2..6cce3e50fa 100644 --- a/CodeEditTests/Features/Documents/DocumentsUnitTests.swift +++ b/CodeEditTests/Features/Documents/DocumentsUnitTests.swift @@ -18,7 +18,7 @@ final class DocumentsUnitTests: XCTestCase { override func setUp() { super.setUp() hapticFeedbackPerformerMock = .init() - splitViewController = .init(feedbackPerformer: hapticFeedbackPerformerMock) + splitViewController = .init(workspace: WorkspaceDocument(), feedbackPerformer: hapticFeedbackPerformerMock) } override func tearDown() {