From f20f28e507f5d1232b43e8bcd5d42c63e2396dd3 Mon Sep 17 00:00:00 2001 From: Austin Condiff Date: Mon, 16 Oct 2023 20:57:02 -0500 Subject: [PATCH] Unsaved changes indicator in editor tab (#1441) * File tabs now indicate when unsaved changes are present. * Unsaved changes tab indicator (#1449) * Fixed SwiftLint errors and fixed inspector in Sonoma * Use CEUndoManager in CEWorkspaceFile (#1451) * Unsaved changes tab indicator, fix onReceive and force re-render when item.fileDocument changed * rename view to EditorFileTabCloseButton * override updateChangeCount and add isDocumentEdited Publisher, remove isDirty property * Use CEUndoManager in CodeFileDocument * update CodeEditTextView version * Fixed SwiftLint error * Updated snapshot testing package * Fixed small PR issue --------- Co-authored-by: avinizhanov <42622715+avinizhanov@users.noreply.github.com> --- CodeEdit.xcodeproj/project.pbxproj | 14 +++- .../xcshareddata/swiftpm/Package.resolved | 23 ++++-- .../CEWorkspace/Models/CEWorkspaceFile.swift | 14 +++- CodeEdit/Features/CodeFile/CodeFile.swift | 30 ++++++++ CodeEdit/Features/CodeFile/CodeFileView.swift | 15 ++-- .../CodeEditWindowController.swift | 68 +---------------- .../CodeEditWindowControllerExtensions.swift | 73 ++++++++++++++++++ .../Tabs/Tab/EditorFileTabCloseButton.swift | 44 +++++++++++ .../Tabs/Tab/EditorTabCloseButton.swift | 20 +++-- .../TabBar/Tabs/Tab/EditorTabView.swift | 7 +- CodeEdit/WindowSplitView.swift | 74 ++++++++++--------- 11 files changed, 253 insertions(+), 129 deletions(-) create mode 100644 CodeEdit/Features/Documents/Controllers/CodeEditWindowControllerExtensions.swift create mode 100644 CodeEdit/Features/Editor/TabBar/Tabs/Tab/EditorFileTabCloseButton.swift diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index 26c18f9cc..a5efabbd9 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -13,6 +13,7 @@ 04540D5E27DD08C300E91B77 /* WorkspaceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B658FB3127DA9E0F00EA4DBD /* WorkspaceView.swift */; }; 04660F6A27E51E5C00477777 /* CodeEditWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04660F6927E51E5C00477777 /* CodeEditWindowController.swift */; }; 0485EB1F27E7458B00138301 /* WorkspaceCodeFileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0485EB1E27E7458B00138301 /* WorkspaceCodeFileView.swift */; }; + 04BC1CDE2AD9B4B000A83EA5 /* EditorFileTabCloseButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04BC1CDD2AD9B4B000A83EA5 /* EditorFileTabCloseButton.swift */; }; 04C3255B2801F86400C8DA2D /* ProjectNavigatorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 285FEC6D27FE4B4A00E57D53 /* ProjectNavigatorViewController.swift */; }; 04C3255C2801F86900C8DA2D /* ProjectNavigatorMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 285FEC7127FE4EEF00E57D53 /* ProjectNavigatorMenu.swift */; }; 200412EF280F3EAC00BCAF5C /* HistoryInspectorNoHistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 200412EE280F3EAC00BCAF5C /* HistoryInspectorNoHistoryView.swift */; }; @@ -343,6 +344,7 @@ B6041F4D29D7A4E9000F3454 /* SettingsPageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6041F4C29D7A4E9000F3454 /* SettingsPageView.swift */; }; B6041F5229D7D6D6000F3454 /* SettingsWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6041F5129D7D6D6000F3454 /* SettingsWindow.swift */; }; B60BE8BD297A167600841125 /* AcknowledgementRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B60BE8BC297A167600841125 /* AcknowledgementRowView.swift */; }; + B6152B802ADAE421004C6012 /* CodeEditWindowControllerExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6152B7F2ADAE421004C6012 /* CodeEditWindowControllerExtensions.swift */; }; B61A606129F188AB009B43F9 /* ExternalLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = B61A606029F188AB009B43F9 /* ExternalLink.swift */; }; B61A606929F4481A009B43F9 /* MonospacedFontPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = B61A606829F4481A009B43F9 /* MonospacedFontPicker.swift */; }; B61DA9DF29D929E100BF4A43 /* GeneralSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B61DA9DE29D929E100BF4A43 /* GeneralSettingsView.swift */; }; @@ -482,6 +484,7 @@ 04660F6927E51E5C00477777 /* CodeEditWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodeEditWindowController.swift; sourceTree = ""; }; 0468438427DC76E200F8E88E /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 0485EB1E27E7458B00138301 /* WorkspaceCodeFileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceCodeFileView.swift; sourceTree = ""; }; + 04BC1CDD2AD9B4B000A83EA5 /* EditorFileTabCloseButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditorFileTabCloseButton.swift; sourceTree = ""; }; 200412EE280F3EAC00BCAF5C /* HistoryInspectorNoHistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryInspectorNoHistoryView.swift; sourceTree = ""; }; 201169D62837B2E300F92B46 /* SourceControlNavigatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceControlNavigatorView.swift; sourceTree = ""; }; 201169D82837B31200F92B46 /* SourceControlSearchToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceControlSearchToolbar.swift; sourceTree = ""; }; @@ -798,6 +801,7 @@ B6041F4C29D7A4E9000F3454 /* SettingsPageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsPageView.swift; sourceTree = ""; }; B6041F5129D7D6D6000F3454 /* SettingsWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsWindow.swift; sourceTree = ""; }; B60BE8BC297A167600841125 /* AcknowledgementRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AcknowledgementRowView.swift; sourceTree = ""; }; + B6152B7F2ADAE421004C6012 /* CodeEditWindowControllerExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodeEditWindowControllerExtensions.swift; sourceTree = ""; }; B61A606029F188AB009B43F9 /* ExternalLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExternalLink.swift; sourceTree = ""; }; B61A606829F4481A009B43F9 /* MonospacedFontPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MonospacedFontPicker.swift; sourceTree = ""; }; B61DA9DE29D929E100BF4A43 /* GeneralSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralSettingsView.swift; sourceTree = ""; }; @@ -1295,6 +1299,7 @@ children = ( 043C321327E31FF6006AE443 /* CodeEditDocumentController.swift */, 04660F6927E51E5C00477777 /* CodeEditWindowController.swift */, + B6152B7F2ADAE421004C6012 /* CodeEditWindowControllerExtensions.swift */, 4E7F066529602E7B00BB3C12 /* CodeEditSplitViewController.swift */, ); path = Controllers; @@ -2330,6 +2335,7 @@ 58AFAA272933C65C00482B53 /* Models */, B6C6A42F29771F7100A3D28F /* EditorTabBackground.swift */, B6C6A42D29771A8D00A3D28F /* EditorTabButtonStyle.swift */, + 04BC1CDD2AD9B4B000A83EA5 /* EditorFileTabCloseButton.swift */, B6C6A429297716A500A3D28F /* EditorTabCloseButton.swift */, 587FB98F29C1246400B519DD /* EditorTabView.swift */, ); @@ -3037,6 +3043,7 @@ 587B9DA329300ABD00AC7927 /* SettingsTextEditor.swift in Sources */, B6F0517B29D9E46400D72287 /* SourceControlSettingsView.swift in Sources */, 6C147C4D29A32AA30089B630 /* EditorView.swift in Sources */, + B6152B802ADAE421004C6012 /* CodeEditWindowControllerExtensions.swift in Sources */, 587B9E7B29301D8F00AC7927 /* GitHubRouter.swift in Sources */, 201169E22837B3D800F92B46 /* SourceControlNavigatorChangesView.swift in Sources */, 850C631029D6B01D00E1444C /* SettingsView.swift in Sources */, @@ -3204,6 +3211,7 @@ 58D01C99293167DC00C5B6B4 /* String+MD5.swift in Sources */, 20EBB505280C329800F3A5DA /* HistoryInspectorItemView.swift in Sources */, 5878DAB2291D627C00DD95A3 /* EditorPathBarView.swift in Sources */, + 04BC1CDE2AD9B4B000A83EA5 /* EditorFileTabCloseButton.swift in Sources */, 6C6BD70129CD172700235D17 /* ExtensionsListView.swift in Sources */, 043C321627E3201F006AE443 /* WorkspaceDocument.swift in Sources */, 58F2EAEC292FB2B0004A9BDE /* IgnoredFiles.swift in Sources */, @@ -4145,8 +4153,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/pointfreeco/swift-snapshot-testing.git"; requirement = { - kind = exactVersion; - version = 1.9.0; + kind = upToNextMinorVersion; + minimumVersion = 1.14.2; }; }; 58798288292ED15F0085B254 /* XCRemoteSwiftPackageReference "SwiftTerm" */ = { @@ -4170,7 +4178,7 @@ repositoryURL = "https://github.com/CodeEditApp/CodeEditTextView"; requirement = { kind = exactVersion; - version = 0.6.7; + version = 0.6.8; }; }; 6C0F3A3A2A1D0D5000223D19 /* XCRemoteSwiftPackageReference "CodeEditKit" */ = { diff --git a/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 9975f9a27..50eba8e5f 100644 --- a/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -39,10 +39,10 @@ { "identity" : "codeedittextview", "kind" : "remoteSourceControl", - "location" : "https://github.com/CodeEditApp/CodeEditTextView.git", + "location" : "https://github.com/CodeEditApp/CodeEditTextView", "state" : { - "revision" : "7f130bd50bb9eb6bacf6a42700cce571ec82bd64", - "version" : "0.6.7" + "revision" : "6a04ca72975b25b28829e419a43cce5cabb97891", + "version" : "0.6.8" } }, { @@ -194,8 +194,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-snapshot-testing.git", "state" : { - "revision" : "f8a9c997c3c1dab4e216a8ec9014e23144cbab37", - "version" : "1.9.0" + "revision" : "bb0ea08db8e73324fe6c3727f755ca41a23ff2f4", + "version" : "1.14.2" + } + }, + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-syntax.git", + "state" : { + "revision" : "74203046135342e4a4a627476dd6caf8b28fe11b", + "version" : "509.0.0" } }, { @@ -239,8 +248,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/Wouter01/SwiftUI-WindowManagement", "state" : { - "revision" : "03642ad06a3aa51e8284eb22146a208269cdc1ca", - "version" : "2.1.0" + "revision" : "adbebf5d7df325f3d7bf07dc832e5e162a9003f5", + "version" : "2.1.1" } }, { diff --git a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile.swift b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile.swift index ca504de43..ac6acc55f 100644 --- a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile.swift +++ b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile.swift @@ -8,6 +8,7 @@ import Foundation import SwiftUI import UniformTypeIdentifiers +import Combine /// An object containing all necessary information and actions for a specific file in the workspace /// @@ -54,7 +55,18 @@ final class CEWorkspaceFile: Codable, Comparable, Hashable, Identifiable, Editor /// If the item already is the top-level ``CEWorkspaceFile`` this returns `nil`. var parent: CEWorkspaceFile? - var fileDocument: CodeFileDocument? + private let fileDocumentSubject = PassthroughSubject() + + var fileDocument: CodeFileDocument? { + didSet { + fileDocumentSubject.send() + } + } + + /// Publisher for fileDocument property + var fileDocumentPublisher: AnyPublisher { + fileDocumentSubject.eraseToAnyPublisher() + } var fileIdentifier = UUID().uuidString diff --git a/CodeEdit/Features/CodeFile/CodeFile.swift b/CodeEdit/Features/CodeFile/CodeFile.swift index 0c37a176d..8d59bf9f0 100644 --- a/CodeEdit/Features/CodeFile/CodeFile.swift +++ b/CodeEdit/Features/CodeFile/CodeFile.swift @@ -12,6 +12,7 @@ import UniformTypeIdentifiers import QuickLookUI import CodeEditTextView import CodeEditLanguages +import Combine enum CodeFileError: Error { case failedToDecode @@ -71,6 +72,13 @@ final class CodeFileDocument: NSDocument, ObservableObject, QLPreviewItem { @Published var cursorPosition = (1, 1) + private let isDocumentEditedSubject = PassthroughSubject() + + /// Publisher for isDocumentEdited property + var isDocumentEditedPublisher: AnyPublisher { + isDocumentEditedSubject.eraseToAnyPublisher() + } + // MARK: - NSDocument override class var autosavesInPlace: Bool { @@ -118,4 +126,26 @@ final class CodeFileDocument: NSDocument, ObservableObject, QLPreviewItem { guard let content = String(data: data, encoding: .utf8) else { return } self.content = content } + + /// Triggered when change occured + override func updateChangeCount(_ change: NSDocument.ChangeType) { + super.updateChangeCount(change) + + if CodeFileDocument.autosavesInPlace { + return + } + + self.isDocumentEditedSubject.send(self.isDocumentEdited) + } + + /// Triggered when changes saved + override func updateChangeCount(withToken changeCountToken: Any, for saveOperation: NSDocument.SaveOperationType) { + super.updateChangeCount(withToken: changeCountToken, for: saveOperation) + + if CodeFileDocument.autosavesInPlace { + return + } + + self.isDocumentEditedSubject.send(self.isDocumentEdited) + } } diff --git a/CodeEdit/Features/CodeFile/CodeFileView.swift b/CodeEdit/Features/CodeFile/CodeFileView.swift index 3c3ecc3b5..bf5ac3e5f 100644 --- a/CodeEdit/Features/CodeFile/CodeFileView.swift +++ b/CodeEdit/Features/CodeFile/CodeFileView.swift @@ -37,6 +37,8 @@ struct CodeFileView: View { @Environment(\.colorScheme) private var colorScheme + @EnvironmentObject private var editorManager: EditorManager + @StateObject private var themeModel: ThemeModel = .shared private var cancellables = [AnyCancellable]() @@ -45,6 +47,8 @@ struct CodeFileView: View { private let systemFont: NSFont = .monospacedSystemFont(ofSize: 11, weight: .medium) + private let undoManager = CEUndoManager() + init(codeFile: CodeFileDocument, isEditable: Bool = true) { self.codeFile = codeFile self.isEditable = isEditable @@ -62,13 +66,7 @@ struct CodeFileView: View { } .store(in: &cancellables) - codeFile - .$content - .dropFirst() - .sink { _ in - codeFile.updateChangeCount(.changeDone) - } - .store(in: &cancellables) + codeFile.undoManager = self.undoManager.manager } @State private var selectedTheme = ThemeModel.shared.selectedTheme ?? ThemeModel.shared.themes.first! @@ -114,7 +112,8 @@ struct CodeFileView: View { contentInsets: edgeInsets.nsEdgeInsets, isEditable: isEditable, letterSpacing: letterSpacing, - bracketPairHighlight: bracketPairHighlight + bracketPairHighlight: bracketPairHighlight, + undoManager: undoManager ) .id(codeFile.fileURL) diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift index 23a33ecaa..41aab954d 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift @@ -52,30 +52,6 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs fatalError("init(coder:) has not been implemented") } - /// These are example items that added as commands to command palette - func registerCommands() { - CommandManager.shared.addCommand( - name: "Quick Open", - title: "Quick Open", - id: "quick_open", - command: CommandClosureWrapper(closure: { self.openQuickly(self) }) - ) - - CommandManager.shared.addCommand( - name: "Toggle Navigator", - title: "Toggle Navigator", - id: "toggle_left_sidebar", - command: CommandClosureWrapper(closure: { self.toggleFirstPanel() }) - ) - - CommandManager.shared.addCommand( - name: "Toggle Inspector", - title: "Toggle Inspector", - id: "toggle_right_sidebar", - command: CommandClosureWrapper(closure: { self.toggleLastPanel() }) - ) - } - private func setupSplitView(with workspace: WorkspaceDocument) { let feedbackPerformer = NSHapticFeedbackManager.defaultPerformer let splitVC = CodeEditSplitViewController(workspace: workspace, feedbackPerformer: feedbackPerformer) @@ -240,7 +216,8 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs } @IBAction func saveDocument(_ sender: Any) { - getSelectedCodeFile()?.save(sender) + guard let codeFile = getSelectedCodeFile() else { return } + codeFile.save(sender) workspace?.editorManager.activeEditor.temporaryTab = nil } @@ -310,44 +287,3 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs } } } - -extension CodeEditWindowController { - @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() { - guard let lastSplitView = splitViewController.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 - if lastSplitView.isCollapsed { - self?.window?.animator().toolbar?.removeItem(at: 4) - } - } - - if let codeEditSplitVC = splitViewController as? CodeEditSplitViewController { - codeEditSplitVC.saveInspectorCollapsedState(isCollapsed: lastSplitView.isCollapsed) - codeEditSplitVC.hideInspectorToolbarBackground() - } - } -} - -extension NSToolbarItem.Identifier { - static let toggleFirstSidebarItem: NSToolbarItem.Identifier = NSToolbarItem.Identifier("ToggleFirstSidebarItem") - static let toggleLastSidebarItem: NSToolbarItem.Identifier = NSToolbarItem.Identifier("ToggleLastSidebarItem") - static let itemListTrackingSeparator = NSToolbarItem.Identifier("ItemListTrackingSeparator") - static let branchPicker: NSToolbarItem.Identifier = NSToolbarItem.Identifier("BranchPicker") -} diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditWindowControllerExtensions.swift b/CodeEdit/Features/Documents/Controllers/CodeEditWindowControllerExtensions.swift new file mode 100644 index 000000000..f5899054c --- /dev/null +++ b/CodeEdit/Features/Documents/Controllers/CodeEditWindowControllerExtensions.swift @@ -0,0 +1,73 @@ +// +// CodeEditWindowControllerExtensions.swift +// CodeEdit +// +// Created by Austin Condiff on 10/14/23. +// + +import SwiftUI + +extension CodeEditWindowController { + @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() { + guard let lastSplitView = splitViewController.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 + if lastSplitView.isCollapsed { + self?.window?.animator().toolbar?.removeItem(at: 4) + } + } + + if let codeEditSplitVC = splitViewController as? CodeEditSplitViewController { + codeEditSplitVC.saveInspectorCollapsedState(isCollapsed: lastSplitView.isCollapsed) + codeEditSplitVC.hideInspectorToolbarBackground() + } + } + + /// These are example items that added as commands to command palette + func registerCommands() { + CommandManager.shared.addCommand( + name: "Quick Open", + title: "Quick Open", + id: "quick_open", + command: CommandClosureWrapper(closure: { self.openQuickly(self) }) + ) + + CommandManager.shared.addCommand( + name: "Toggle Navigator", + title: "Toggle Navigator", + id: "toggle_left_sidebar", + command: CommandClosureWrapper(closure: { self.toggleFirstPanel() }) + ) + + CommandManager.shared.addCommand( + name: "Toggle Inspector", + title: "Toggle Inspector", + id: "toggle_right_sidebar", + command: CommandClosureWrapper(closure: { self.toggleLastPanel() }) + ) + } +} + +extension NSToolbarItem.Identifier { + static let toggleFirstSidebarItem: NSToolbarItem.Identifier = NSToolbarItem.Identifier("ToggleFirstSidebarItem") + static let toggleLastSidebarItem: NSToolbarItem.Identifier = NSToolbarItem.Identifier("ToggleLastSidebarItem") + static let itemListTrackingSeparator = NSToolbarItem.Identifier("ItemListTrackingSeparator") + static let branchPicker: NSToolbarItem.Identifier = NSToolbarItem.Identifier("BranchPicker") +} diff --git a/CodeEdit/Features/Editor/TabBar/Tabs/Tab/EditorFileTabCloseButton.swift b/CodeEdit/Features/Editor/TabBar/Tabs/Tab/EditorFileTabCloseButton.swift new file mode 100644 index 000000000..cd4029f80 --- /dev/null +++ b/CodeEdit/Features/Editor/TabBar/Tabs/Tab/EditorFileTabCloseButton.swift @@ -0,0 +1,44 @@ +// +// FileEditorTabCloseButton.swift +// CodeEdit +// +// Created by Albert Vinizhanau on 10/13/23. +// + +import Foundation +import SwiftUI +import Combine + +struct EditorFileTabCloseButton: View { + var isActive: Bool + var isHoveringTab: Bool + var isDragging: Bool + var closeAction: () -> Void + @Binding var closeButtonGestureActive: Bool + var item: CEWorkspaceFile + + @State private var isDocumentEdited: Bool = false + @State private var id: Int = 0 + + var body: some View { + EditorTabCloseButton( + isActive: isActive, + isHoveringTab: isHoveringTab, + isDragging: isDragging, + closeAction: closeAction, + closeButtonGestureActive: $closeButtonGestureActive, + isDocumentEdited: isDocumentEdited + ) + .id(id) + // Detects if file document changed, when this view created item.fileDocument is nil + .onReceive(item.fileDocumentPublisher, perform: { _ in + // Force rerender so isDocumentEdited publisher is updated + self.id += 1 + }) + .onReceive( + item.fileDocument?.isDocumentEditedPublisher.eraseToAnyPublisher() ?? Empty().eraseToAnyPublisher() + ) { newValue in + self.isDocumentEdited = newValue + } + } +} diff --git a/CodeEdit/Features/Editor/TabBar/Tabs/Tab/EditorTabCloseButton.swift b/CodeEdit/Features/Editor/TabBar/Tabs/Tab/EditorTabCloseButton.swift index e84e0559e..75b734cd5 100644 --- a/CodeEdit/Features/Editor/TabBar/Tabs/Tab/EditorTabCloseButton.swift +++ b/CodeEdit/Features/Editor/TabBar/Tabs/Tab/EditorTabCloseButton.swift @@ -12,8 +12,8 @@ struct EditorTabCloseButton: View { var isHoveringTab: Bool var isDragging: Bool var closeAction: () -> Void - @Binding var closeButtonGestureActive: Bool + var isDocumentEdited: Bool = false @Environment(\.colorScheme) var colorScheme @@ -22,24 +22,28 @@ struct EditorTabCloseButton: View { var tabBarStyle @State private var isPressingClose: Bool = false - @State private var isHoveringClose: Bool = false let buttonSize: CGFloat = 16 var body: some View { - HStack { + HStack(alignment: .center) { if tabBarStyle == .xcode { - Image(systemName: "xmark") - .font(.system(size: 11.5, weight: .regular, design: .rounded)) + Image(systemName: isDocumentEdited && !isHoveringTab ? "circlebadge.fill" : "xmark") + .font( + .system( + size: isDocumentEdited && !isHoveringTab ? 9.5 : 11.5, + weight: .regular, + design: .rounded + ) + ) .foregroundColor( isActive ? colorScheme == .dark ? .primary : Color(.controlAccentColor) : .secondary ) - .padding(.top, -0.5) } else { - Image(systemName: "xmark") + Image(systemName: isDocumentEdited && !isHoveringTab ? "circlebadge.fill" : "xmark") .font(.system(size: 9.5, weight: .medium, design: .rounded)) } } @@ -87,7 +91,7 @@ struct EditorTabCloseButton: View { } .accessibilityLabel(Text("Close")) // Only show when the mouse is hovering and there is no tab dragging. - .opacity(isHoveringTab && !isDragging ? 1 : 0) + .opacity((isHoveringTab || isDocumentEdited == true) && !isDragging ? 1 : 0) .animation(.easeInOut(duration: 0.08), value: isHoveringTab) .padding(.leading, 4) } diff --git a/CodeEdit/Features/Editor/TabBar/Tabs/Tab/EditorTabView.swift b/CodeEdit/Features/Editor/TabBar/Tabs/Tab/EditorTabView.swift index 25a807282..9c2fc629c 100644 --- a/CodeEdit/Features/Editor/TabBar/Tabs/Tab/EditorTabView.swift +++ b/CodeEdit/Features/Editor/TabBar/Tabs/Tab/EditorTabView.swift @@ -159,13 +159,14 @@ struct EditorTabView: View { .padding(.horizontal, tabBarStyle == .native ? 28 : 23) .overlay { ZStack { - // Close Button - EditorTabCloseButton( + // Close Button with is file changed indicator + EditorFileTabCloseButton( isActive: isActive, isHoveringTab: isHovering, isDragging: draggingTabId != nil || onDragTabId != nil, closeAction: closeAction, - closeButtonGestureActive: $closeButtonGestureActive + closeButtonGestureActive: $closeButtonGestureActive, + item: item ) } .frame(maxWidth: .infinity, alignment: .leading) diff --git a/CodeEdit/WindowSplitView.swift b/CodeEdit/WindowSplitView.swift index 8f35b048b..46e82b68e 100644 --- a/CodeEdit/WindowSplitView.swift +++ b/CodeEdit/WindowSplitView.swift @@ -13,28 +13,32 @@ struct WindowSplitView: View { @State var showInspector = true @State var window: NSWindow = .init() - var body: some View { - WindowObserver(window: window) { - NavigationSplitView(columnVisibility: $visibility) { - NavigatorAreaView(workspace: workspace, viewModel: NavigatorSidebarViewModel()) - .toolbar { - ToolbarItem { - Button { - withAnimation(.linear(duration: 0)) { - if visibility == .detailOnly { - visibility = .all - } else { - visibility = .detailOnly - } - } - } label: { - Image(systemName: "sidebar.left") - .imageScale(.large) + var navigatorAreaView: some View { + NavigatorAreaView(workspace: workspace, viewModel: NavigatorSidebarViewModel()) + .toolbar { + ToolbarItem { + Button { + withAnimation(.linear(duration: 0)) { + if visibility == .detailOnly { + visibility = .all + } else { + visibility = .detailOnly } } + } label: { + Image(systemName: "sidebar.left") + .imageScale(.large) } - } detail: { - if #available(macOS 14.0, *) { + } + } + } + + var body: some View { + WindowObserver(window: window) { + if #available(macOS 14.0, *) { + NavigationSplitView(columnVisibility: $visibility) { + navigatorAreaView + } detail: { WorkspaceView() .toolbar { ToolbarItem(id: "com.apple.SwiftUI.navigationSplitView.toggleSidebar") { @@ -45,22 +49,26 @@ struct WindowSplitView: View { } .defaultCustomization(.hidden, options: []) } -#if swift(>=5.9) // Fix build on Xcode 14 - .inspector(isPresented: $showInspector) { - InspectorAreaView(viewModel: InspectorAreaViewModel()) - .inspectorColumnWidth(min: 100, ideal: 200, max: 400) - .toolbar { - Spacer() - Button { - showInspector.toggle() - } label: { - Image(systemName: "sidebar.right") - .imageScale(.large) - } - } + } +#if swift(>=5.9) // Fixes build on Xcode 14 + .inspector(isPresented: $showInspector) { + InspectorAreaView(viewModel: InspectorAreaViewModel()) + .inspectorColumnWidth(min: 100, ideal: 200, max: 400) + .toolbar { + Spacer() + Button { + showInspector.toggle() + } label: { + Image(systemName: "sidebar.right") + .imageScale(.large) + } } + } #endif - } else { + } else { + NavigationSplitView(columnVisibility: $visibility) { + navigatorAreaView + } detail: { WorkspaceView() } }