Skip to content

Commit f20f28e

Browse files
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 <[email protected]>
1 parent 698b75e commit f20f28e

File tree

11 files changed

+253
-129
lines changed

11 files changed

+253
-129
lines changed

CodeEdit.xcodeproj/project.pbxproj

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
04540D5E27DD08C300E91B77 /* WorkspaceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B658FB3127DA9E0F00EA4DBD /* WorkspaceView.swift */; };
1414
04660F6A27E51E5C00477777 /* CodeEditWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04660F6927E51E5C00477777 /* CodeEditWindowController.swift */; };
1515
0485EB1F27E7458B00138301 /* WorkspaceCodeFileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0485EB1E27E7458B00138301 /* WorkspaceCodeFileView.swift */; };
16+
04BC1CDE2AD9B4B000A83EA5 /* EditorFileTabCloseButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04BC1CDD2AD9B4B000A83EA5 /* EditorFileTabCloseButton.swift */; };
1617
04C3255B2801F86400C8DA2D /* ProjectNavigatorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 285FEC6D27FE4B4A00E57D53 /* ProjectNavigatorViewController.swift */; };
1718
04C3255C2801F86900C8DA2D /* ProjectNavigatorMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 285FEC7127FE4EEF00E57D53 /* ProjectNavigatorMenu.swift */; };
1819
200412EF280F3EAC00BCAF5C /* HistoryInspectorNoHistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 200412EE280F3EAC00BCAF5C /* HistoryInspectorNoHistoryView.swift */; };
@@ -343,6 +344,7 @@
343344
B6041F4D29D7A4E9000F3454 /* SettingsPageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6041F4C29D7A4E9000F3454 /* SettingsPageView.swift */; };
344345
B6041F5229D7D6D6000F3454 /* SettingsWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6041F5129D7D6D6000F3454 /* SettingsWindow.swift */; };
345346
B60BE8BD297A167600841125 /* AcknowledgementRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B60BE8BC297A167600841125 /* AcknowledgementRowView.swift */; };
347+
B6152B802ADAE421004C6012 /* CodeEditWindowControllerExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6152B7F2ADAE421004C6012 /* CodeEditWindowControllerExtensions.swift */; };
346348
B61A606129F188AB009B43F9 /* ExternalLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = B61A606029F188AB009B43F9 /* ExternalLink.swift */; };
347349
B61A606929F4481A009B43F9 /* MonospacedFontPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = B61A606829F4481A009B43F9 /* MonospacedFontPicker.swift */; };
348350
B61DA9DF29D929E100BF4A43 /* GeneralSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B61DA9DE29D929E100BF4A43 /* GeneralSettingsView.swift */; };
@@ -482,6 +484,7 @@
482484
04660F6927E51E5C00477777 /* CodeEditWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodeEditWindowController.swift; sourceTree = "<group>"; };
483485
0468438427DC76E200F8E88E /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
484486
0485EB1E27E7458B00138301 /* WorkspaceCodeFileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceCodeFileView.swift; sourceTree = "<group>"; };
487+
04BC1CDD2AD9B4B000A83EA5 /* EditorFileTabCloseButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditorFileTabCloseButton.swift; sourceTree = "<group>"; };
485488
200412EE280F3EAC00BCAF5C /* HistoryInspectorNoHistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryInspectorNoHistoryView.swift; sourceTree = "<group>"; };
486489
201169D62837B2E300F92B46 /* SourceControlNavigatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceControlNavigatorView.swift; sourceTree = "<group>"; };
487490
201169D82837B31200F92B46 /* SourceControlSearchToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceControlSearchToolbar.swift; sourceTree = "<group>"; };
@@ -798,6 +801,7 @@
798801
B6041F4C29D7A4E9000F3454 /* SettingsPageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsPageView.swift; sourceTree = "<group>"; };
799802
B6041F5129D7D6D6000F3454 /* SettingsWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsWindow.swift; sourceTree = "<group>"; };
800803
B60BE8BC297A167600841125 /* AcknowledgementRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AcknowledgementRowView.swift; sourceTree = "<group>"; };
804+
B6152B7F2ADAE421004C6012 /* CodeEditWindowControllerExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodeEditWindowControllerExtensions.swift; sourceTree = "<group>"; };
801805
B61A606029F188AB009B43F9 /* ExternalLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExternalLink.swift; sourceTree = "<group>"; };
802806
B61A606829F4481A009B43F9 /* MonospacedFontPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MonospacedFontPicker.swift; sourceTree = "<group>"; };
803807
B61DA9DE29D929E100BF4A43 /* GeneralSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralSettingsView.swift; sourceTree = "<group>"; };
@@ -1295,6 +1299,7 @@
12951299
children = (
12961300
043C321327E31FF6006AE443 /* CodeEditDocumentController.swift */,
12971301
04660F6927E51E5C00477777 /* CodeEditWindowController.swift */,
1302+
B6152B7F2ADAE421004C6012 /* CodeEditWindowControllerExtensions.swift */,
12981303
4E7F066529602E7B00BB3C12 /* CodeEditSplitViewController.swift */,
12991304
);
13001305
path = Controllers;
@@ -2330,6 +2335,7 @@
23302335
58AFAA272933C65C00482B53 /* Models */,
23312336
B6C6A42F29771F7100A3D28F /* EditorTabBackground.swift */,
23322337
B6C6A42D29771A8D00A3D28F /* EditorTabButtonStyle.swift */,
2338+
04BC1CDD2AD9B4B000A83EA5 /* EditorFileTabCloseButton.swift */,
23332339
B6C6A429297716A500A3D28F /* EditorTabCloseButton.swift */,
23342340
587FB98F29C1246400B519DD /* EditorTabView.swift */,
23352341
);
@@ -3037,6 +3043,7 @@
30373043
587B9DA329300ABD00AC7927 /* SettingsTextEditor.swift in Sources */,
30383044
B6F0517B29D9E46400D72287 /* SourceControlSettingsView.swift in Sources */,
30393045
6C147C4D29A32AA30089B630 /* EditorView.swift in Sources */,
3046+
B6152B802ADAE421004C6012 /* CodeEditWindowControllerExtensions.swift in Sources */,
30403047
587B9E7B29301D8F00AC7927 /* GitHubRouter.swift in Sources */,
30413048
201169E22837B3D800F92B46 /* SourceControlNavigatorChangesView.swift in Sources */,
30423049
850C631029D6B01D00E1444C /* SettingsView.swift in Sources */,
@@ -3204,6 +3211,7 @@
32043211
58D01C99293167DC00C5B6B4 /* String+MD5.swift in Sources */,
32053212
20EBB505280C329800F3A5DA /* HistoryInspectorItemView.swift in Sources */,
32063213
5878DAB2291D627C00DD95A3 /* EditorPathBarView.swift in Sources */,
3214+
04BC1CDE2AD9B4B000A83EA5 /* EditorFileTabCloseButton.swift in Sources */,
32073215
6C6BD70129CD172700235D17 /* ExtensionsListView.swift in Sources */,
32083216
043C321627E3201F006AE443 /* WorkspaceDocument.swift in Sources */,
32093217
58F2EAEC292FB2B0004A9BDE /* IgnoredFiles.swift in Sources */,
@@ -4145,8 +4153,8 @@
41454153
isa = XCRemoteSwiftPackageReference;
41464154
repositoryURL = "https://github.com/pointfreeco/swift-snapshot-testing.git";
41474155
requirement = {
4148-
kind = exactVersion;
4149-
version = 1.9.0;
4156+
kind = upToNextMinorVersion;
4157+
minimumVersion = 1.14.2;
41504158
};
41514159
};
41524160
58798288292ED15F0085B254 /* XCRemoteSwiftPackageReference "SwiftTerm" */ = {
@@ -4170,7 +4178,7 @@
41704178
repositoryURL = "https://github.com/CodeEditApp/CodeEditTextView";
41714179
requirement = {
41724180
kind = exactVersion;
4173-
version = 0.6.7;
4181+
version = 0.6.8;
41744182
};
41754183
};
41764184
6C0F3A3A2A1D0D5000223D19 /* XCRemoteSwiftPackageReference "CodeEditKit" */ = {

CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

Lines changed: 16 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile.swift

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import Foundation
99
import SwiftUI
1010
import UniformTypeIdentifiers
11+
import Combine
1112

1213
/// An object containing all necessary information and actions for a specific file in the workspace
1314
///
@@ -54,7 +55,18 @@ final class CEWorkspaceFile: Codable, Comparable, Hashable, Identifiable, Editor
5455
/// If the item already is the top-level ``CEWorkspaceFile`` this returns `nil`.
5556
var parent: CEWorkspaceFile?
5657

57-
var fileDocument: CodeFileDocument?
58+
private let fileDocumentSubject = PassthroughSubject<Void, Never>()
59+
60+
var fileDocument: CodeFileDocument? {
61+
didSet {
62+
fileDocumentSubject.send()
63+
}
64+
}
65+
66+
/// Publisher for fileDocument property
67+
var fileDocumentPublisher: AnyPublisher<Void, Never> {
68+
fileDocumentSubject.eraseToAnyPublisher()
69+
}
5870

5971
var fileIdentifier = UUID().uuidString
6072

CodeEdit/Features/CodeFile/CodeFile.swift

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import UniformTypeIdentifiers
1212
import QuickLookUI
1313
import CodeEditTextView
1414
import CodeEditLanguages
15+
import Combine
1516

1617
enum CodeFileError: Error {
1718
case failedToDecode
@@ -71,6 +72,13 @@ final class CodeFileDocument: NSDocument, ObservableObject, QLPreviewItem {
7172

7273
@Published var cursorPosition = (1, 1)
7374

75+
private let isDocumentEditedSubject = PassthroughSubject<Bool, Never>()
76+
77+
/// Publisher for isDocumentEdited property
78+
var isDocumentEditedPublisher: AnyPublisher<Bool, Never> {
79+
isDocumentEditedSubject.eraseToAnyPublisher()
80+
}
81+
7482
// MARK: - NSDocument
7583

7684
override class var autosavesInPlace: Bool {
@@ -118,4 +126,26 @@ final class CodeFileDocument: NSDocument, ObservableObject, QLPreviewItem {
118126
guard let content = String(data: data, encoding: .utf8) else { return }
119127
self.content = content
120128
}
129+
130+
/// Triggered when change occured
131+
override func updateChangeCount(_ change: NSDocument.ChangeType) {
132+
super.updateChangeCount(change)
133+
134+
if CodeFileDocument.autosavesInPlace {
135+
return
136+
}
137+
138+
self.isDocumentEditedSubject.send(self.isDocumentEdited)
139+
}
140+
141+
/// Triggered when changes saved
142+
override func updateChangeCount(withToken changeCountToken: Any, for saveOperation: NSDocument.SaveOperationType) {
143+
super.updateChangeCount(withToken: changeCountToken, for: saveOperation)
144+
145+
if CodeFileDocument.autosavesInPlace {
146+
return
147+
}
148+
149+
self.isDocumentEditedSubject.send(self.isDocumentEdited)
150+
}
121151
}

CodeEdit/Features/CodeFile/CodeFileView.swift

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ struct CodeFileView: View {
3737
@Environment(\.colorScheme)
3838
private var colorScheme
3939

40+
@EnvironmentObject private var editorManager: EditorManager
41+
4042
@StateObject private var themeModel: ThemeModel = .shared
4143

4244
private var cancellables = [AnyCancellable]()
@@ -45,6 +47,8 @@ struct CodeFileView: View {
4547

4648
private let systemFont: NSFont = .monospacedSystemFont(ofSize: 11, weight: .medium)
4749

50+
private let undoManager = CEUndoManager()
51+
4852
init(codeFile: CodeFileDocument, isEditable: Bool = true) {
4953
self.codeFile = codeFile
5054
self.isEditable = isEditable
@@ -62,13 +66,7 @@ struct CodeFileView: View {
6266
}
6367
.store(in: &cancellables)
6468

65-
codeFile
66-
.$content
67-
.dropFirst()
68-
.sink { _ in
69-
codeFile.updateChangeCount(.changeDone)
70-
}
71-
.store(in: &cancellables)
69+
codeFile.undoManager = self.undoManager.manager
7270
}
7371

7472
@State private var selectedTheme = ThemeModel.shared.selectedTheme ?? ThemeModel.shared.themes.first!
@@ -114,7 +112,8 @@ struct CodeFileView: View {
114112
contentInsets: edgeInsets.nsEdgeInsets,
115113
isEditable: isEditable,
116114
letterSpacing: letterSpacing,
117-
bracketPairHighlight: bracketPairHighlight
115+
bracketPairHighlight: bracketPairHighlight,
116+
undoManager: undoManager
118117
)
119118

120119
.id(codeFile.fileURL)

CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift

Lines changed: 2 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -52,30 +52,6 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs
5252
fatalError("init(coder:) has not been implemented")
5353
}
5454

55-
/// These are example items that added as commands to command palette
56-
func registerCommands() {
57-
CommandManager.shared.addCommand(
58-
name: "Quick Open",
59-
title: "Quick Open",
60-
id: "quick_open",
61-
command: CommandClosureWrapper(closure: { self.openQuickly(self) })
62-
)
63-
64-
CommandManager.shared.addCommand(
65-
name: "Toggle Navigator",
66-
title: "Toggle Navigator",
67-
id: "toggle_left_sidebar",
68-
command: CommandClosureWrapper(closure: { self.toggleFirstPanel() })
69-
)
70-
71-
CommandManager.shared.addCommand(
72-
name: "Toggle Inspector",
73-
title: "Toggle Inspector",
74-
id: "toggle_right_sidebar",
75-
command: CommandClosureWrapper(closure: { self.toggleLastPanel() })
76-
)
77-
}
78-
7955
private func setupSplitView(with workspace: WorkspaceDocument) {
8056
let feedbackPerformer = NSHapticFeedbackManager.defaultPerformer
8157
let splitVC = CodeEditSplitViewController(workspace: workspace, feedbackPerformer: feedbackPerformer)
@@ -240,7 +216,8 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs
240216
}
241217

242218
@IBAction func saveDocument(_ sender: Any) {
243-
getSelectedCodeFile()?.save(sender)
219+
guard let codeFile = getSelectedCodeFile() else { return }
220+
codeFile.save(sender)
244221
workspace?.editorManager.activeEditor.temporaryTab = nil
245222
}
246223

@@ -310,44 +287,3 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs
310287
}
311288
}
312289
}
313-
314-
extension CodeEditWindowController {
315-
@objc
316-
func toggleFirstPanel() {
317-
guard let firstSplitView = splitViewController.splitViewItems.first else { return }
318-
firstSplitView.animator().isCollapsed.toggle()
319-
if let codeEditSplitVC = splitViewController as? CodeEditSplitViewController {
320-
codeEditSplitVC.saveNavigatorCollapsedState(isCollapsed: firstSplitView.isCollapsed)
321-
}
322-
}
323-
324-
@objc
325-
func toggleLastPanel() {
326-
guard let lastSplitView = splitViewController.splitViewItems.last else { return }
327-
328-
if let toolbar = window?.toolbar,
329-
lastSplitView.isCollapsed,
330-
!toolbar.items.map(\.itemIdentifier).contains(.itemListTrackingSeparator) {
331-
window?.toolbar?.insertItem(withItemIdentifier: .itemListTrackingSeparator, at: 4)
332-
}
333-
NSAnimationContext.runAnimationGroup { _ in
334-
lastSplitView.animator().isCollapsed.toggle()
335-
} completionHandler: { [weak self] in
336-
if lastSplitView.isCollapsed {
337-
self?.window?.animator().toolbar?.removeItem(at: 4)
338-
}
339-
}
340-
341-
if let codeEditSplitVC = splitViewController as? CodeEditSplitViewController {
342-
codeEditSplitVC.saveInspectorCollapsedState(isCollapsed: lastSplitView.isCollapsed)
343-
codeEditSplitVC.hideInspectorToolbarBackground()
344-
}
345-
}
346-
}
347-
348-
extension NSToolbarItem.Identifier {
349-
static let toggleFirstSidebarItem: NSToolbarItem.Identifier = NSToolbarItem.Identifier("ToggleFirstSidebarItem")
350-
static let toggleLastSidebarItem: NSToolbarItem.Identifier = NSToolbarItem.Identifier("ToggleLastSidebarItem")
351-
static let itemListTrackingSeparator = NSToolbarItem.Identifier("ItemListTrackingSeparator")
352-
static let branchPicker: NSToolbarItem.Identifier = NSToolbarItem.Identifier("BranchPicker")
353-
}

0 commit comments

Comments
 (0)