From dbd6c1acbf57aadd303c368de1c9a7e462ff8a70 Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Sun, 14 Jul 2024 07:01:12 -0500 Subject: [PATCH 1/9] Fix Workspace Leak --- CodeEdit.xcodeproj/project.pbxproj | 93 +++--- .../xcshareddata/swiftpm/Package.resolved | 21 +- CodeEdit/AppDelegate.swift | 6 +- .../CEWorkspace/Models/CEWorkspaceFile.swift | 4 +- .../Models/CEWorkspaceFileManager.swift | 1 + .../Models/CEWorkspaceSettings.swift | 24 +- .../Views/ToolbarBranchPicker.swift | 4 +- .../Commands/Views/QuickActionsView.swift | 2 +- .../{ => CodeFileDocument}/FileEncoding.swift | 0 .../CodeEditSplitViewController.swift | 26 +- .../CodeEditWindowController.swift | 67 +++-- .../CodeEditWindowControllerExtensions.swift | 27 +- .../Features/Documents/LazyStringLoader.swift | 35 --- .../WorkspaceDocument+Find.swift | 0 .../WorkspaceDocument+FindAndReplace.swift | 0 .../WorkspaceDocument+Index.swift | 0 .../WorkspaceDocument+Listeners.swift | 0 .../WorkspaceDocument+SearchState.swift | 0 .../WorkspaceDocument.swift | 42 ++- .../WorkspaceStateKey.swift | 0 CodeEdit/Features/Editor/Models/Editor.swift | 3 + .../Views/EditorTabBarContextMenu.swift | 2 +- .../Editor/Views/EditorLayoutView.swift | 4 +- .../Extensions/ExtensionDiscovery.swift | 79 +++--- .../FileInspector/FileInspectorView.swift | 14 +- .../Features/Keybindings/CommandManager.swift | 25 +- CodeEdit/Features/LSP/LSPEventHandler.swift | 14 +- .../FindNavigatorListViewController.swift | 17 +- .../OutlineView/FileSystemTableViewCell.swift | 30 +- .../OutlineView/StandardTableViewCell.swift | 114 ++++---- .../OutlineView/ProjectNavigatorMenu.swift | 2 +- .../ProjectNavigatorOutlineView.swift | 12 +- .../ProjectNavigatorTableViewCell.swift | 11 +- ...ewController+NSOutlineViewDataSource.swift | 119 ++++++++ ...ViewController+NSOutlineViewDelegate.swift | 159 +++++++++++ ...troller+OutlineTableViewCellDelegate.swift | 4 +- .../ProjectNavigatorViewController.swift | 267 +----------------- .../SourceControlNavigatorChangesList.swift | 6 +- .../Models/KeybindingsSettings.swift | 10 - .../Models/TextEditingSettings.swift | 6 +- .../StatusBarToggleUtilityAreaButton.swift | 4 +- .../UtilityAreaTerminalView.swift | 30 +- .../ViewModels/UtilityAreaViewModel.swift | 17 ++ .../WindowCommands/FileCommands.swift | 8 +- .../WindowCommands/ViewCommands.swift | 186 ++++++------ CodeEdit/Utils/Environment/Env+Window.swift | 9 +- .../String+AppearancesOfSubstring.swift | 0 .../Extensions/String}/String+Character.swift | 0 CodeEdit/WindowObserver.swift | 2 +- CodeEdit/WorkspaceView.swift | 4 +- 50 files changed, 790 insertions(+), 720 deletions(-) rename CodeEdit/Features/Documents/{ => CodeFileDocument}/FileEncoding.swift (100%) delete mode 100644 CodeEdit/Features/Documents/LazyStringLoader.swift rename CodeEdit/Features/Documents/{ => WorkspaceDocument}/WorkspaceDocument+Find.swift (100%) rename CodeEdit/Features/Documents/{ => WorkspaceDocument}/WorkspaceDocument+FindAndReplace.swift (100%) rename CodeEdit/Features/Documents/{ => WorkspaceDocument}/WorkspaceDocument+Index.swift (100%) rename CodeEdit/Features/Documents/{ => WorkspaceDocument}/WorkspaceDocument+Listeners.swift (100%) rename CodeEdit/Features/Documents/{ => WorkspaceDocument}/WorkspaceDocument+SearchState.swift (100%) rename CodeEdit/Features/Documents/{ => WorkspaceDocument}/WorkspaceDocument.swift (88%) rename CodeEdit/Features/Documents/{ => WorkspaceDocument}/WorkspaceStateKey.swift (100%) create mode 100644 CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDataSource.swift create mode 100644 CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift rename CodeEdit/{Features/Documents => Utils/Extensions/String}/String+AppearancesOfSubstring.swift (100%) rename CodeEdit/{Features/Documents => Utils/Extensions/String}/String+Character.swift (100%) diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index f5ed17227f..44374e70e4 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -310,7 +310,6 @@ 613DF55E2B08DD5D00E9D902 /* FileHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 613DF55D2B08DD5D00E9D902 /* FileHelper.swift */; }; 61538B902B111FE800A88846 /* String+AppearancesOfSubstring.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61538B8F2B111FE800A88846 /* String+AppearancesOfSubstring.swift */; }; 61538B932B11201900A88846 /* String+Character.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61538B922B11201900A88846 /* String+Character.swift */; }; - 615AA21A2B0CFD480013FCCC /* LazyStringLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615AA2192B0CFD480013FCCC /* LazyStringLoader.swift */; }; 617DB3D02C25AFAE00B58BFE /* TaskNotificationHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 617DB3CF2C25AFAE00B58BFE /* TaskNotificationHandler.swift */; }; 617DB3D32C25AFEA00B58BFE /* TaskNotificationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 617DB3D22C25AFEA00B58BFE /* TaskNotificationModel.swift */; }; 617DB3D62C25B02D00B58BFE /* TaskNotificationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 617DB3D52C25B02D00B58BFE /* TaskNotificationView.swift */; }; @@ -325,10 +324,10 @@ 61A53A812B4449F00093BF8A /* WorkspaceDocument+Index.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61A53A802B4449F00093BF8A /* WorkspaceDocument+Index.swift */; }; 661EF7B82BEE215300C3E577 /* ImageFileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 661EF7B72BEE215300C3E577 /* ImageFileView.swift */; }; 661EF7BD2BEE215300C3E577 /* LoadingFileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 661EF7BC2BEE215300C3E577 /* LoadingFileView.swift */; }; - 669A50512C380C1800304CD8 /* String+escapedWhiteSpaces.swift in Sources */ = {isa = PBXBuildFile; fileRef = 669A50502C380C1800304CD8 /* String+escapedWhiteSpaces.swift */; }; - 669A50532C380C8E00304CD8 /* Collection+subscript_safe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 669A50522C380C8E00304CD8 /* Collection+subscript_safe.swift */; }; 664935422C35A5BC00461C35 /* NSTableViewWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 664935412C35A5BC00461C35 /* NSTableViewWrapper.swift */; }; 6653EE552C34817900B82DE2 /* QuickSearchResultLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6653EE542C34817900B82DE2 /* QuickSearchResultLabel.swift */; }; + 669A50512C380C1800304CD8 /* String+escapedWhiteSpaces.swift in Sources */ = {isa = PBXBuildFile; fileRef = 669A50502C380C1800304CD8 /* String+escapedWhiteSpaces.swift */; }; + 669A50532C380C8E00304CD8 /* Collection+subscript_safe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 669A50522C380C8E00304CD8 /* Collection+subscript_safe.swift */; }; 669BC4082BED306400D1197C /* AnyFileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 669BC4072BED306400D1197C /* AnyFileView.swift */; }; 66AF6CE22BF17CC300D83C9D /* StatusBarViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66AF6CE12BF17CC300D83C9D /* StatusBarViewModel.swift */; }; 66AF6CE42BF17F6800D83C9D /* StatusBarFileInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66AF6CE32BF17F6800D83C9D /* StatusBarFileInfoView.swift */; }; @@ -417,7 +416,10 @@ 6CBA0D512A1BF524002C6FAA /* SegmentedControlImproved.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CBA0D502A1BF524002C6FAA /* SegmentedControlImproved.swift */; }; 6CBD1BC62978DE53006639D5 /* Font+Caption3.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CBD1BC52978DE53006639D5 /* Font+Caption3.swift */; }; 6CBE1CFB2B71DAA6003AC32E /* Loopable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CBE1CFA2B71DAA6003AC32E /* Loopable.swift */; }; - 6CBE1D002B720565003AC32E /* CodeEditSourceEditor in Frameworks */ = {isa = PBXBuildFile; productRef = 6CBE1CFF2B720565003AC32E /* CodeEditSourceEditor */; }; + 6CC17B4F2C432AE000834E2C /* CodeEditSourceEditor in Frameworks */ = {isa = PBXBuildFile; productRef = 6CC17B4E2C432AE000834E2C /* CodeEditSourceEditor */; }; + 6CC17B512C43311900834E2C /* ProjectNavigatorViewController+NSOutlineViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CC17B502C43311900834E2C /* ProjectNavigatorViewController+NSOutlineViewDataSource.swift */; }; + 6CC17B532C43314000834E2C /* ProjectNavigatorViewController+NSOutlineViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CC17B522C43314000834E2C /* ProjectNavigatorViewController+NSOutlineViewDelegate.swift */; }; + 6CC17B592C43F53700834E2C /* CodeEditSourceEditor in Frameworks */ = {isa = PBXBuildFile; productRef = 6CC17B582C43F53700834E2C /* CodeEditSourceEditor */; }; 6CC9E4B229B5669900C97388 /* Environment+ActiveEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CC9E4B129B5669900C97388 /* Environment+ActiveEditor.swift */; }; 6CD03B6A29FC773F001BD1D0 /* SettingsInjector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CD03B6929FC773F001BD1D0 /* SettingsInjector.swift */; }; 6CDA84AD284C1BA000C1CC3A /* EditorTabBarContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CDA84AC284C1BA000C1CC3A /* EditorTabBarContextMenu.swift */; }; @@ -931,7 +933,6 @@ 613DF55D2B08DD5D00E9D902 /* FileHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileHelper.swift; sourceTree = ""; }; 61538B8F2B111FE800A88846 /* String+AppearancesOfSubstring.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+AppearancesOfSubstring.swift"; sourceTree = ""; }; 61538B922B11201900A88846 /* String+Character.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Character.swift"; sourceTree = ""; }; - 615AA2192B0CFD480013FCCC /* LazyStringLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazyStringLoader.swift; sourceTree = ""; }; 617DB3CF2C25AFAE00B58BFE /* TaskNotificationHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskNotificationHandler.swift; sourceTree = ""; }; 617DB3D22C25AFEA00B58BFE /* TaskNotificationModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskNotificationModel.swift; sourceTree = ""; }; 617DB3D52C25B02D00B58BFE /* TaskNotificationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskNotificationView.swift; sourceTree = ""; }; @@ -1022,6 +1023,8 @@ 6CBA0D502A1BF524002C6FAA /* SegmentedControlImproved.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SegmentedControlImproved.swift; sourceTree = ""; }; 6CBD1BC52978DE53006639D5 /* Font+Caption3.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Font+Caption3.swift"; sourceTree = ""; }; 6CBE1CFA2B71DAA6003AC32E /* Loopable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Loopable.swift; sourceTree = ""; }; + 6CC17B502C43311900834E2C /* ProjectNavigatorViewController+NSOutlineViewDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProjectNavigatorViewController+NSOutlineViewDataSource.swift"; sourceTree = ""; }; + 6CC17B522C43314000834E2C /* ProjectNavigatorViewController+NSOutlineViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProjectNavigatorViewController+NSOutlineViewDelegate.swift"; sourceTree = ""; }; 6CC9E4B129B5669900C97388 /* Environment+ActiveEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Environment+ActiveEditor.swift"; sourceTree = ""; }; 6CD03B6929FC773F001BD1D0 /* SettingsInjector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsInjector.swift; sourceTree = ""; }; 6CDA84AC284C1BA000C1CC3A /* EditorTabBarContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditorTabBarContextMenu.swift; sourceTree = ""; }; @@ -1191,9 +1194,9 @@ 6C85BB402C2105ED00EB5DEF /* CodeEditKit in Frameworks */, 6C66C31329D05CDC00DE9ED2 /* GRDB in Frameworks */, 58F2EB1E292FB954004A9BDE /* Sparkle in Frameworks */, - 6CBE1D002B720565003AC32E /* CodeEditSourceEditor in Frameworks */, 6C147C4529A329350089B630 /* OrderedCollections in Frameworks */, 6C0617D62BDB4432008C9C42 /* LogStream in Frameworks */, + 6CC17B4F2C432AE000834E2C /* CodeEditSourceEditor in Frameworks */, 30CB64912C16CA8100CC8A9E /* LanguageServerProtocol in Frameworks */, 6C6BD6F429CD142C00235D17 /* CollectionConcurrencyKit in Frameworks */, 6C85BB442C210EFD00EB5DEF /* SwiftUIIntrospect in Frameworks */, @@ -1201,6 +1204,7 @@ 5879828A292ED15F0085B254 /* SwiftTerm in Frameworks */, 2816F594280CF50500DD548B /* CodeEditSymbols in Frameworks */, 30CB64942C16CA9100CC8A9E /* LanguageClient in Frameworks */, + 6CC17B592C43F53700834E2C /* CodeEditSourceEditor in Frameworks */, 6C6BD6F829CD14D100235D17 /* CodeEditKit in Frameworks */, 6C81916B29B41DD300B75C92 /* DequeModule in Frameworks */, ); @@ -1227,20 +1231,10 @@ 043C321227E31FE8006AE443 /* Documents */ = { isa = PBXGroup; children = ( + 6CC17B552C4344F100834E2C /* CodeFileDocument */, 5831E3CE2933F3DE00D5A6D2 /* Controllers */, 611191F82B08CC8000D4459B /* Indexer */, - 58798249292E78D80085B254 /* CodeFileDocument.swift */, - 6C48B5C42C0A2835001E9955 /* FileEncoding.swift */, - 043C321527E3201F006AE443 /* WorkspaceDocument.swift */, - 043BCF02281DA18A000AC47C /* WorkspaceDocument+SearchState.swift */, - 61A53A802B4449F00093BF8A /* WorkspaceDocument+Index.swift */, - 61A53A7D2B4449870093BF8A /* WorkspaceDocument+Find.swift */, - 610C0FD92B44438F00A01CA7 /* WorkspaceDocument+FindAndReplace.swift */, - 615AA2192B0CFD480013FCCC /* LazyStringLoader.swift */, - 6C05A8AE284D0CA3007F4EAA /* WorkspaceDocument+Listeners.swift */, - 6C092EDF2A53BFCF00489202 /* WorkspaceStateKey.swift */, - 61538B8F2B111FE800A88846 /* String+AppearancesOfSubstring.swift */, - 61538B922B11201900A88846 /* String+Character.swift */, + 6CC17B542C43448C00834E2C /* WorkspaceDocument */, ); path = Documents; sourceTree = ""; @@ -1357,6 +1351,8 @@ children = ( 2847019D27FDDF7600F87B6B /* ProjectNavigatorOutlineView.swift */, 285FEC6D27FE4B4A00E57D53 /* ProjectNavigatorViewController.swift */, + 6CC17B522C43314000834E2C /* ProjectNavigatorViewController+NSOutlineViewDelegate.swift */, + 6CC17B502C43311900834E2C /* ProjectNavigatorViewController+NSOutlineViewDataSource.swift */, 285FEC6F27FE4B9800E57D53 /* ProjectNavigatorTableViewCell.swift */, 285FEC7127FE4EEF00E57D53 /* ProjectNavigatorMenu.swift */, D7DC4B75298FFBE900D6C83D /* ProjectNavigatorViewController+OutlineTableViewCellDelegate.swift */, @@ -2343,18 +2339,18 @@ 58D01C87293167DC00C5B6B4 /* Extensions */ = { isa = PBXGroup; children = ( - 669A504F2C380BFD00304CD8 /* Collection */, - 6C82D6C429C0129E00495C54 /* NSApplication */, - 77A01E922BCA9C0400F0EA38 /* NSWindow */, 588847672992AAB800996D95 /* Array */, - 6CBD1BC42978DE3E006639D5 /* Text */, - 5831E3D02934036D00D5A6D2 /* NSTableView */, - 5831E3CA2933E86F00D5A6D2 /* View */, 5831E3C72933E7F700D5A6D2 /* Bundle */, 5831E3C62933E7E600D5A6D2 /* Color */, + 669A504F2C380BFD00304CD8 /* Collection */, 5831E3C82933E80500D5A6D2 /* Date */, + 6C82D6C429C0129E00495C54 /* NSApplication */, + 5831E3D02934036D00D5A6D2 /* NSTableView */, + 77A01E922BCA9C0400F0EA38 /* NSWindow */, 58D01C8B293167DC00C5B6B4 /* String */, 5831E3CB2933E89A00D5A6D2 /* SwiftTerm */, + 6CBD1BC42978DE3E006639D5 /* Text */, + 5831E3CA2933E86F00D5A6D2 /* View */, ); path = Extensions; sourceTree = ""; @@ -2362,13 +2358,15 @@ 58D01C8B293167DC00C5B6B4 /* String */ = { isa = PBXGroup; children = ( + 61538B8F2B111FE800A88846 /* String+AppearancesOfSubstring.swift */, + 61538B922B11201900A88846 /* String+Character.swift */, + 669A50502C380C1800304CD8 /* String+escapedWhiteSpaces.swift */, + 85745D622A38F8D900089AAB /* String+HighlightOccurrences.swift */, + 6CED16E32A3E660D000EC962 /* String+Lines.swift */, 58D01C8E293167DC00C5B6B4 /* String+MD5.swift */, D7E201AD27E8B3C000CB86D0 /* String+Ranges.swift */, - 6CED16E32A3E660D000EC962 /* String+Lines.swift */, 58D01C8D293167DC00C5B6B4 /* String+RemoveOccurrences.swift */, 58D01C8C293167DC00C5B6B4 /* String+SHA256.swift */, - 85745D622A38F8D900089AAB /* String+HighlightOccurrences.swift */, - 669A50502C380C1800304CD8 /* String+escapedWhiteSpaces.swift */, ); path = String; sourceTree = ""; @@ -2732,6 +2730,29 @@ path = Text; sourceTree = ""; }; + 6CC17B542C43448C00834E2C /* WorkspaceDocument */ = { + isa = PBXGroup; + children = ( + 043C321527E3201F006AE443 /* WorkspaceDocument.swift */, + 043BCF02281DA18A000AC47C /* WorkspaceDocument+SearchState.swift */, + 61A53A802B4449F00093BF8A /* WorkspaceDocument+Index.swift */, + 61A53A7D2B4449870093BF8A /* WorkspaceDocument+Find.swift */, + 610C0FD92B44438F00A01CA7 /* WorkspaceDocument+FindAndReplace.swift */, + 6C05A8AE284D0CA3007F4EAA /* WorkspaceDocument+Listeners.swift */, + 6C092EDF2A53BFCF00489202 /* WorkspaceStateKey.swift */, + ); + path = WorkspaceDocument; + sourceTree = ""; + }; + 6CC17B552C4344F100834E2C /* CodeFileDocument */ = { + isa = PBXGroup; + children = ( + 58798249292E78D80085B254 /* CodeFileDocument.swift */, + 6C48B5C42C0A2835001E9955 /* FileEncoding.swift */, + ); + path = CodeFileDocument; + sourceTree = ""; + }; 77A01E1A2BB33F1E00F0EA38 /* Views */ = { isa = PBXGroup; children = ( @@ -3310,10 +3331,11 @@ 6C6BD6F729CD14D100235D17 /* CodeEditKit */, 6C66C31229D05CDC00DE9ED2 /* GRDB */, 6CB4463F2B6DFF3A00539ED0 /* CodeEditSourceEditor */, - 6CBE1CFF2B720565003AC32E /* CodeEditSourceEditor */, 6C0617D52BDB4432008C9C42 /* LogStream */, 6C85BB3F2C2105ED00EB5DEF /* CodeEditKit */, 6C85BB432C210EFD00EB5DEF /* SwiftUIIntrospect */, + 6CC17B4E2C432AE000834E2C /* CodeEditSourceEditor */, + 6CC17B582C43F53700834E2C /* CodeEditSourceEditor */, ); productName = CodeEdit; productReference = B658FB2C27DA9E0F00EA4DBD /* CodeEdit.app */; @@ -3405,12 +3427,12 @@ 6C147C4329A329350089B630 /* XCRemoteSwiftPackageReference "swift-collections" */, 6C6BD6F229CD142C00235D17 /* XCRemoteSwiftPackageReference "collectionconcurrencykit" */, 6C66C31129D05CC800DE9ED2 /* XCRemoteSwiftPackageReference "GRDB.swift" */, - 6CBE1CFE2B720565003AC32E /* XCRemoteSwiftPackageReference "CodeEditSourceEditor" */, 6C0617D42BDB4432008C9C42 /* XCRemoteSwiftPackageReference "LogStream" */, 6C85BB3E2C2105ED00EB5DEF /* XCRemoteSwiftPackageReference "CodeEditKit" */, 6C85BB422C210EFD00EB5DEF /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */, 303E88452C276FD100EEA8D9 /* XCRemoteSwiftPackageReference "LanguageClient" */, 303E88462C276FD600EEA8D9 /* XCRemoteSwiftPackageReference "LanguageServerProtocol" */, + 6CC17B572C43F53700834E2C /* XCRemoteSwiftPackageReference "CodeEditSourceEditor" */, ); productRefGroup = B658FB2D27DA9E0F00EA4DBD /* Products */; projectDirPath = ""; @@ -3687,6 +3709,7 @@ B60718372B170638009CDAB4 /* SourceControlNavigatorRenameBranchView.swift in Sources */, 6C578D8129CD294800DC73B2 /* ExtensionActivatorView.swift in Sources */, B6F0517D29D9E4B100D72287 /* TerminalSettingsView.swift in Sources */, + 6CC17B532C43314000834E2C /* ProjectNavigatorViewController+NSOutlineViewDelegate.swift in Sources */, 587B9E8C29301D8F00AC7927 /* GitHubOpenness.swift in Sources */, 5894E59729FEF7740077E59C /* CEWorkspaceFile+Recursion.swift in Sources */, 9D36E1BF2B5E7D7500443C41 /* GitBranchesGroup.swift in Sources */, @@ -3755,6 +3778,7 @@ 613DF55E2B08DD5D00E9D902 /* FileHelper.swift in Sources */, 58798235292E30B90085B254 /* FeedbackModel.swift in Sources */, 04C3255C2801F86900C8DA2D /* ProjectNavigatorMenu.swift in Sources */, + 6CC17B512C43311900834E2C /* ProjectNavigatorViewController+NSOutlineViewDataSource.swift in Sources */, 587B9E6429301D8F00AC7927 /* GitLabCommit.swift in Sources */, B6E55C3B2A95368E003ECC7D /* EditorTabsOverflowShadow.swift in Sources */, 58A5DFA229339F6400D1BD5D /* KeybindingManager.swift in Sources */, @@ -3863,7 +3887,6 @@ 6C049A372A49E2DB00D42923 /* DirectoryEventStream.swift in Sources */, 30B088062C0D53080063A882 /* LanguageServer+DocumentSymbol.swift in Sources */, 04BA7C0E2AE2A76E00584E1C /* SourceControlNavigatorChangesCommitView.swift in Sources */, - 615AA21A2B0CFD480013FCCC /* LazyStringLoader.swift in Sources */, 6CAAF68A29BC9C2300A1F48A /* (null) in Sources */, 30AB4EBD2BF71CA800ED4431 /* DeveloperSettingsView.swift in Sources */, 6C6BD6EF29CD12E900235D17 /* ExtensionManagerWindow.swift in Sources */, @@ -5247,7 +5270,7 @@ minimumVersion = 1.2.0; }; }; - 6CBE1CFE2B720565003AC32E /* XCRemoteSwiftPackageReference "CodeEditSourceEditor" */ = { + 6CC17B572C43F53700834E2C /* XCRemoteSwiftPackageReference "CodeEditSourceEditor" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/CodeEditApp/CodeEditSourceEditor"; requirement = { @@ -5336,9 +5359,13 @@ isa = XCSwiftPackageProductDependency; productName = CodeEditSourceEditor; }; - 6CBE1CFF2B720565003AC32E /* CodeEditSourceEditor */ = { + 6CC17B4E2C432AE000834E2C /* CodeEditSourceEditor */ = { + isa = XCSwiftPackageProductDependency; + productName = CodeEditSourceEditor; + }; + 6CC17B582C43F53700834E2C /* CodeEditSourceEditor */ = { isa = XCSwiftPackageProductDependency; - package = 6CBE1CFE2B720565003AC32E /* XCRemoteSwiftPackageReference "CodeEditSourceEditor" */; + package = 6CC17B572C43F53700834E2C /* XCRemoteSwiftPackageReference "CodeEditSourceEditor" */; productName = CodeEditSourceEditor; }; /* End XCSwiftPackageProductDependency section */ diff --git a/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index b5dee36909..c6458df54c 100644 --- a/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "f3dd40f19b32a2b522dee7a1e94ceb60afbdb1a028938883adffc7328ef9d804", + "originHash" : "59bb530acdf3bfbb14acc0701532467038359ce8bdcd8935b8d88825a447556c", "pins" : [ { "identity" : "anycodable", @@ -31,7 +31,7 @@ { "identity" : "codeeditsourceeditor", "kind" : "remoteSourceControl", - "location" : "https://github.com/CodeEditApp/CodeEditSourceEditor.git", + "location" : "https://github.com/CodeEditApp/CodeEditSourceEditor", "state" : { "revision" : "cf85789d527d569e94edfd674c5ac8071b244dd9", "version" : "0.7.3" @@ -136,15 +136,6 @@ "version" : "1.3.0" } }, - { - "identity" : "mainoffender", - "kind" : "remoteSourceControl", - "location" : "https://github.com/mattmassicotte/MainOffender", - "state" : { - "revision" : "8de872d9256ff7f9913cbc5dd560568ab164be45", - "version" : "0.2.1" - } - }, { "identity" : "processenv", "kind" : "remoteSourceControl", @@ -258,8 +249,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/ChimeHQ/TextFormation", "state" : { - "revision" : "f6faed6abd768ae95b70d10113d4008a7cac57a7", - "version" : "0.8.2" + "revision" : "b1ce9a14bd86042bba4de62236028dc4ce9db6a1", + "version" : "0.9.0" } }, { @@ -267,8 +258,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/ChimeHQ/TextStory", "state" : { - "revision" : "8883fa739aa213e70e6cb109bfbf0a0b551e4cb5", - "version" : "0.8.0" + "revision" : "8dc9148b46fcf93b08ea9d4ef9bdb5e4f700e008", + "version" : "0.9.0" } } ], diff --git a/CodeEdit/AppDelegate.swift b/CodeEdit/AppDelegate.swift index c9da64926e..e86b55a79f 100644 --- a/CodeEdit/AppDelegate.swift +++ b/CodeEdit/AppDelegate.swift @@ -13,7 +13,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject { private let updater = SoftwareUpdater() @Environment(\.openWindow) - private var openWindow + var openWindow func applicationDidFinishLaunching(_ notification: Notification) { setupServiceContainer() @@ -209,8 +209,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject { defaults.removeObject(forKey: "openInCEFiles") } - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - self.checkForFilesToOpen() + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in + self?.checkForFilesToOpen() } } diff --git a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile.swift b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile.swift index 3596a944b2..53606c219a 100644 --- a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile.swift +++ b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile.swift @@ -85,11 +85,11 @@ final class CEWorkspaceFile: Codable, Comparable, Hashable, Identifiable, Editor /// Returns a parent ``CEWorkspaceFile``. /// /// If the item already is the top-level ``CEWorkspaceFile`` this returns `nil`. - var parent: CEWorkspaceFile? + weak var parent: CEWorkspaceFile? private let fileDocumentSubject = PassthroughSubject() - var fileDocument: CodeFileDocument? { + weak var fileDocument: CodeFileDocument? { didSet { fileDocumentSubject.send(fileDocument) } diff --git a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager.swift b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager.swift index 56453a0923..42bfd1d462 100644 --- a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager.swift +++ b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager.swift @@ -387,6 +387,7 @@ final class CEWorkspaceFileManager { } deinit { + fsEventStream?.cancel() observers.removeAllObjects() } } diff --git a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceSettings.swift b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceSettings.swift index f3d20687d7..67cefa3c1f 100644 --- a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceSettings.swift +++ b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceSettings.swift @@ -10,20 +10,12 @@ import Combine /// The CodeEdit workspace settings model. final class CEWorkspaceSettings: ObservableObject { - @ObservedObject private var workspace: WorkspaceDocument @Published public var preferences: CEWorkspaceSettingsData = .init() - private var storeTask: AnyCancellable! + private var storeTask: AnyCancellable? private let fileManager = FileManager.default - private var folderURL: URL? { - guard let workspaceURL = workspace.fileURL else { - return nil - } - - return workspaceURL - .appendingPathComponent(".codeedit", isDirectory: true) - } + private(set) var folderURL: URL? private var settingsURL: URL? { folderURL? @@ -32,8 +24,7 @@ final class CEWorkspaceSettings: ObservableObject { } init(workspaceDocument: WorkspaceDocument) { - self.workspace = workspaceDocument - + folderURL = workspaceDocument.fileURL?.appendingPathComponent(".codeedit", isDirectory: true) loadSettings() self.storeTask = self.$preferences.throttle(for: 2.0, scheduler: RunLoop.main, latest: true).sink { @@ -41,6 +32,15 @@ final class CEWorkspaceSettings: ObservableObject { } } + func cleanUp() { + storeTask?.cancel() + storeTask = nil + } + + deinit { + cleanUp() + } + /// Load and construct ``CEWorkspaceSettings`` model from `.codeedit/settings.json` private func loadSettings() { if let settingsURL = settingsURL { diff --git a/CodeEdit/Features/CodeEditUI/Views/ToolbarBranchPicker.swift b/CodeEdit/Features/CodeEditUI/Views/ToolbarBranchPicker.swift index 6e17ba2b71..db50b1b634 100644 --- a/CodeEdit/Features/CodeEditUI/Views/ToolbarBranchPicker.swift +++ b/CodeEdit/Features/CodeEditUI/Views/ToolbarBranchPicker.swift @@ -11,8 +11,8 @@ import Combine /// A view that pops up a branch picker. struct ToolbarBranchPicker: View { - private var workspaceFileManager: CEWorkspaceFileManager? - private var sourceControlManager: SourceControlManager? + private weak var workspaceFileManager: CEWorkspaceFileManager? + private weak var sourceControlManager: SourceControlManager? @Environment(\.controlActiveState) private var controlActive diff --git a/CodeEdit/Features/Commands/Views/QuickActionsView.swift b/CodeEdit/Features/Commands/Views/QuickActionsView.swift index bf8e465fe0..12d60e1148 100644 --- a/CodeEdit/Features/Commands/Views/QuickActionsView.swift +++ b/CodeEdit/Features/Commands/Views/QuickActionsView.swift @@ -31,7 +31,7 @@ struct QuickActionsView: View { func callHandler(command: Command) { closePalette() - command.closureWrapper.call() + command.closureWrapper() selectedItem = nil state.commandQuery = "" state.filteredCommands = [] diff --git a/CodeEdit/Features/Documents/FileEncoding.swift b/CodeEdit/Features/Documents/CodeFileDocument/FileEncoding.swift similarity index 100% rename from CodeEdit/Features/Documents/FileEncoding.swift rename to CodeEdit/Features/Documents/CodeFileDocument/FileEncoding.swift diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift b/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift index af83e56ac8..64e55dd50a 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift @@ -14,8 +14,8 @@ final class CodeEditSplitViewController: NSSplitViewController { static let snapWidth: CGFloat = 272 static let minSnapWidth: CGFloat = snapWidth - 10 - private var workspace: WorkspaceDocument - private var navigatorViewModel: NavigatorSidebarViewModel + private weak var workspace: WorkspaceDocument? + private weak var navigatorViewModel: NavigatorSidebarViewModel? private weak var windowRef: NSWindow? private unowned var hapticPerformer: NSHapticFeedbackPerformer @@ -47,12 +47,14 @@ final class CodeEditSplitViewController: NSSplitViewController { return } + guard let workspace, let navigatorViewModel else { return } + splitView.translatesAutoresizingMaskIntoConstraints = false let settingsView = SettingsInjector { NavigatorAreaView(workspace: workspace, viewModel: navigatorViewModel) .environmentObject(workspace) - .environmentObject(workspace.editorManager) + .environmentObject(workspace.editorManager!) } let navigator = NSSplitViewItem(sidebarWithViewController: NSHostingController(rootView: settingsView)) @@ -64,12 +66,12 @@ final class CodeEditSplitViewController: NSSplitViewController { addSplitViewItem(navigator) let workspaceView = SettingsInjector { - WindowObserver(window: windowRef) { + WindowObserver(window: WindowBox(value: windowRef)) { WorkspaceView() .environmentObject(workspace) - .environmentObject(workspace.editorManager) - .environmentObject(workspace.statusBarViewModel) - .environmentObject(workspace.utilityAreaModel) + .environmentObject(workspace.editorManager!) + .environmentObject(workspace.statusBarViewModel!) + .environmentObject(workspace.utilityAreaModel!) } } @@ -82,7 +84,7 @@ final class CodeEditSplitViewController: NSSplitViewController { let inspectorView = SettingsInjector { InspectorAreaView(viewModel: InspectorAreaViewModel()) .environmentObject(workspace) - .environmentObject(workspace.editorManager) + .environmentObject(workspace.editorManager!) } let inspector = NSSplitViewItem(inspectorWithViewController: NSHostingController(rootView: inspectorView)) @@ -98,6 +100,8 @@ final class CodeEditSplitViewController: NSSplitViewController { override func viewWillAppear() { super.viewWillAppear() + guard let workspace else { return } + let navigatorWidth = workspace.getFromWorkspaceState(.splitViewWidth) as? CGFloat splitView.setPosition(navigatorWidth ?? Self.minSidebarWidth, ofDividerAt: 0) @@ -178,16 +182,16 @@ final class CodeEditSplitViewController: NSSplitViewController { let panel = splitView.subviews[0] let width = panel.frame.size.width if width > 0 { - workspace.addToWorkspaceState(key: .splitViewWidth, value: width) + workspace?.addToWorkspaceState(key: .splitViewWidth, value: width) } } } func saveNavigatorCollapsedState(isCollapsed: Bool) { - workspace.addToWorkspaceState(key: .navigatorCollapsed, value: isCollapsed) + workspace?.addToWorkspaceState(key: .navigatorCollapsed, value: isCollapsed) } func saveInspectorCollapsedState(isCollapsed: Bool) { - workspace.addToWorkspaceState(key: .inspectorCollapsed, value: isCollapsed) + workspace?.addToWorkspaceState(key: .inspectorCollapsed, value: isCollapsed) } } diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift index ec9131dace..7878cbd2c3 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift @@ -9,7 +9,7 @@ import Cocoa import SwiftUI import Combine -final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, ObservableObject { +final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, ObservableObject, NSWindowDelegate { @Published var navigatorCollapsed = false @Published var inspectorCollapsed = false @Published var toolbarCollapsed = false @@ -25,10 +25,12 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs var taskNotificationHandler: TaskNotificationHandler - var splitViewController: NSSplitViewController! - internal var cancellables = [AnyCancellable]() + var splitViewController: CodeEditSplitViewController? { + contentViewController as? CodeEditSplitViewController + } + init( window: NSWindow?, workspace: WorkspaceDocument?, @@ -36,10 +38,13 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs ) { self.taskNotificationHandler = taskNotificationHandler super.init(window: window) + window?.delegate = self guard let workspace else { return } self.workspace = workspace self.workspaceSettings = CEWorkspaceSettings(workspaceDocument: workspace) - setupSplitView(with: workspace) + guard let splitViewController = setupSplitView(with: workspace) else { + fatalError("Failed to set up content view.") + } // Previous: // An NSHostingController is used, so the root viewController of the window is a SwiftUI-managed one. @@ -69,37 +74,40 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs registerCommands() } - deinit { cancellables.forEach({ $0.cancel() }) } + deinit { + cancellables.forEach({ $0.cancel() }) + cancellables.removeAll() + } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - private func setupSplitView(with workspace: WorkspaceDocument) { + private func setupSplitView(with workspace: WorkspaceDocument) -> CodeEditSplitViewController? { guard let window else { assertionFailure("No window found for this controller. Cannot set up content.") - return + return nil } let navigatorModel = NavigatorSidebarViewModel() navigatorSidebarViewModel = navigatorModel - self.splitViewController = CodeEditSplitViewController( + self.listenToDocumentEdited(workspace: workspace) + return CodeEditSplitViewController( workspace: workspace, navigatorViewModel: navigatorModel, windowRef: window ) - self.listenToDocumentEdited(workspace: workspace) } private func getSelectedCodeFile() -> CodeFileDocument? { - workspace?.editorManager.activeEditor.selectedTab?.file.fileDocument + workspace?.editorManager?.activeEditor.selectedTab?.file.fileDocument } @IBAction func saveDocument(_ sender: Any) { guard let codeFile = getSelectedCodeFile() else { return } codeFile.save(sender) - workspace?.editorManager.activeEditor.temporaryTab = nil + workspace?.editorManager?.activeEditor.temporaryTab = nil } @IBAction func openCommandPalette(_ sender: Any) { @@ -125,7 +133,7 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs } } - @IBAction func openQuickly(_ sender: Any) { + @IBAction func openQuickly(_ sender: Any?) { if let workspace, let state = workspace.openQuicklyViewModel { if let quickOpenPanel { if quickOpenPanel.isKeyWindow { @@ -142,7 +150,7 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs let contentView = OpenQuicklyView(state: state) { panel.close() } openFile: { file in - workspace.editorManager.openTab(item: file) + workspace.editorManager?.openTab(item: file) }.environmentObject(workspace) panel.contentView = NSHostingView(rootView: SettingsInjector { contentView }) @@ -153,18 +161,41 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs } @IBAction func closeCurrentTab(_ sender: Any) { - if (workspace?.editorManager.activeEditor.tabs ?? []).isEmpty { + if (workspace?.editorManager?.activeEditor.tabs ?? []).isEmpty { self.closeActiveEditor(self) } else { - workspace?.editorManager.activeEditor.closeSelectedTab() + workspace?.editorManager?.activeEditor.closeSelectedTab() } } @IBAction func closeActiveEditor(_ sender: Any) { - if workspace?.editorManager.editorLayout.findSomeEditor(except: workspace?.editorManager.activeEditor) == nil { - NSApp.sendAction(#selector(NSWindow.close), to: nil, from: nil) + if workspace?.editorManager?.editorLayout.findSomeEditor( + except: workspace?.editorManager?.activeEditor + ) == nil { + NSApp.sendAction(#selector(NSWindow.performClose(_:)), to: nil, from: nil) } else { - workspace?.editorManager.activeEditor.close() + workspace?.editorManager?.activeEditor.close() + } + } + + func windowShouldClose(_ sender: NSWindow) -> Bool { + cancellables.forEach({ $0.cancel() }) + cancellables.removeAll() + + for _ in 0..<(splitViewController?.children.count ?? 0) { + splitViewController?.removeChild(at: 0) } + contentViewController?.removeFromParent() + contentViewController = nil + + workspaceSettingsWindow?.close() + workspaceSettingsWindow = nil + workspaceSettings?.cleanUp() + workspaceSettings = nil + quickOpenPanel = nil + commandPalettePanel = nil + navigatorSidebarViewModel = nil + workspace = nil + return true } } diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditWindowControllerExtensions.swift b/CodeEdit/Features/Documents/Controllers/CodeEditWindowControllerExtensions.swift index f1a8586151..5d556257ee 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditWindowControllerExtensions.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditWindowControllerExtensions.swift @@ -11,17 +11,14 @@ import Combine extension CodeEditWindowController { @objc func toggleFirstPanel() { - guard let firstSplitView = splitViewController.splitViewItems.first else { return } + guard let firstSplitView = splitViewController?.splitViewItems.first else { return } firstSplitView.animator().isCollapsed.toggle() - if let codeEditSplitVC = splitViewController as? CodeEditSplitViewController { - codeEditSplitVC.saveNavigatorCollapsedState(isCollapsed: firstSplitView.isCollapsed) - } + splitViewController?.saveNavigatorCollapsedState(isCollapsed: firstSplitView.isCollapsed) } @objc func toggleLastPanel() { - guard let lastSplitView = splitViewController.splitViewItems.last, - let codeEditSplitVC = splitViewController as? CodeEditSplitViewController else { + guard let lastSplitView = splitViewController?.splitViewItems.last else { return } @@ -29,7 +26,7 @@ extension CodeEditWindowController { lastSplitView.animator().isCollapsed.toggle() } - codeEditSplitVC.saveInspectorCollapsedState(isCollapsed: lastSplitView.isCollapsed) + splitViewController?.saveInspectorCollapsedState(isCollapsed: lastSplitView.isCollapsed) } /// These are example items that added as commands to command palette @@ -38,27 +35,27 @@ extension CodeEditWindowController { name: "Quick Open", title: "Quick Open", id: "quick_open", - command: CommandClosureWrapper(closure: { self.openQuickly(self) }) + command: { [weak self] in self?.openQuickly(nil) } ) CommandManager.shared.addCommand( name: "Toggle Navigator", title: "Toggle Navigator", id: "toggle_left_sidebar", - command: CommandClosureWrapper(closure: { self.toggleFirstPanel() }) + command: { [weak self] in self?.toggleFirstPanel() } ) CommandManager.shared.addCommand( name: "Toggle Inspector", title: "Toggle Inspector", id: "toggle_right_sidebar", - command: CommandClosureWrapper(closure: { self.toggleLastPanel() }) + command: { [weak self] in self?.toggleLastPanel() } ) } // Listen to changes in all tabs/files internal func listenToDocumentEdited(workspace: WorkspaceDocument) { - workspace.editorManager.$activeEditor + workspace.editorManager?.$activeEditor .flatMap({ editor in editor.$tabs }) @@ -82,7 +79,7 @@ extension CodeEditWindowController { // Listen to change of tabs, if closed tab without saving content, // we also need to recalculate isDocumentEdited - workspace.editorManager.$activeEditor + workspace.editorManager?.$activeEditor .flatMap({ editor in editor.$tabs }) @@ -94,12 +91,12 @@ extension CodeEditWindowController { // Recalculate documentEdited by checking if any tab/file is edited private func updateDocumentEdited(workspace: WorkspaceDocument) { - let hasEditedDocuments = !workspace - .editorManager + let hasEditedDocuments = !(workspace + .editorManager? .editorLayout .gatherOpenFiles() .filter({ $0.fileDocument?.isDocumentEdited == true }) - .isEmpty + .isEmpty ?? true) self.setDocumentEdited(hasEditedDocuments) } diff --git a/CodeEdit/Features/Documents/LazyStringLoader.swift b/CodeEdit/Features/Documents/LazyStringLoader.swift deleted file mode 100644 index e2b82b6499..0000000000 --- a/CodeEdit/Features/Documents/LazyStringLoader.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// LazyStringLoader.swift -// CodeEdit -// -// Created by Tommy Ludwig on 21.11.23. -// - -import Foundation - -class LazyStringLoader { - let fileURL: URL - var fileHandle: FileHandle? - let chunkSize: Int - let queue = DispatchQueue(label: "com.CodeEdit.LazyLoader") - - init(fileURL: URL, chunkSize: Int = 1024) { - self.fileURL = fileURL - self.chunkSize = chunkSize - } - - func getNextChunk() -> String? { - if fileHandle == nil { - do { - fileHandle = try FileHandle(forReadingFrom: fileURL) - guard let data = try fileHandle?.read(upToCount: chunkSize) else { - return nil - } - return String(decoding: data, as: UTF8.self) - } catch { - return nil - } - } - return nil - } -} diff --git a/CodeEdit/Features/Documents/WorkspaceDocument+Find.swift b/CodeEdit/Features/Documents/WorkspaceDocument/WorkspaceDocument+Find.swift similarity index 100% rename from CodeEdit/Features/Documents/WorkspaceDocument+Find.swift rename to CodeEdit/Features/Documents/WorkspaceDocument/WorkspaceDocument+Find.swift diff --git a/CodeEdit/Features/Documents/WorkspaceDocument+FindAndReplace.swift b/CodeEdit/Features/Documents/WorkspaceDocument/WorkspaceDocument+FindAndReplace.swift similarity index 100% rename from CodeEdit/Features/Documents/WorkspaceDocument+FindAndReplace.swift rename to CodeEdit/Features/Documents/WorkspaceDocument/WorkspaceDocument+FindAndReplace.swift diff --git a/CodeEdit/Features/Documents/WorkspaceDocument+Index.swift b/CodeEdit/Features/Documents/WorkspaceDocument/WorkspaceDocument+Index.swift similarity index 100% rename from CodeEdit/Features/Documents/WorkspaceDocument+Index.swift rename to CodeEdit/Features/Documents/WorkspaceDocument/WorkspaceDocument+Index.swift diff --git a/CodeEdit/Features/Documents/WorkspaceDocument+Listeners.swift b/CodeEdit/Features/Documents/WorkspaceDocument/WorkspaceDocument+Listeners.swift similarity index 100% rename from CodeEdit/Features/Documents/WorkspaceDocument+Listeners.swift rename to CodeEdit/Features/Documents/WorkspaceDocument/WorkspaceDocument+Listeners.swift diff --git a/CodeEdit/Features/Documents/WorkspaceDocument+SearchState.swift b/CodeEdit/Features/Documents/WorkspaceDocument/WorkspaceDocument+SearchState.swift similarity index 100% rename from CodeEdit/Features/Documents/WorkspaceDocument+SearchState.swift rename to CodeEdit/Features/Documents/WorkspaceDocument/WorkspaceDocument+SearchState.swift diff --git a/CodeEdit/Features/Documents/WorkspaceDocument.swift b/CodeEdit/Features/Documents/WorkspaceDocument/WorkspaceDocument.swift similarity index 88% rename from CodeEdit/Features/Documents/WorkspaceDocument.swift rename to CodeEdit/Features/Documents/WorkspaceDocument/WorkspaceDocument.swift index f813e9b550..9f28598202 100644 --- a/CodeEdit/Features/Documents/WorkspaceDocument.swift +++ b/CodeEdit/Features/Documents/WorkspaceDocument/WorkspaceDocument.swift @@ -16,10 +16,6 @@ final class WorkspaceDocument: NSDocument, ObservableObject, NSToolbarDelegate { @Published var sortFoldersOnTop: Bool = true - var workspaceFileManager: CEWorkspaceFileManager? - - var editorManager = EditorManager() - private var workspaceState: [String: Any] { get { let key = "workspaceState-\(self.fileURL?.absoluteString ?? "")" @@ -31,8 +27,11 @@ final class WorkspaceDocument: NSDocument, ObservableObject, NSToolbarDelegate { } } - var statusBarViewModel = StatusBarViewModel() - var utilityAreaModel = UtilityAreaViewModel() + var workspaceFileManager: CEWorkspaceFileManager? + + var editorManager: EditorManager? = EditorManager() + var statusBarViewModel: StatusBarViewModel? = StatusBarViewModel() + var utilityAreaModel: UtilityAreaViewModel? = UtilityAreaViewModel() var searchState: SearchState? var openQuicklyViewModel: OpenQuicklyViewModel? var commandsPaletteState: QuickActionsViewModel? @@ -102,6 +101,8 @@ final class WorkspaceDocument: NSDocument, ObservableObject, NSToolbarDelegate { window.center() } self.addWindowController(windowController) + + window.makeKeyAndOrderFront(nil) } // MARK: Set Up Workspace @@ -112,7 +113,7 @@ final class WorkspaceDocument: NSDocument, ObservableObject, NSToolbarDelegate { let sourceControlManager = SourceControlManager( workspaceURL: url, - editorManager: editorManager + editorManager: editorManager! ) self.workspaceFileManager = .init( @@ -126,8 +127,8 @@ final class WorkspaceDocument: NSDocument, ObservableObject, NSToolbarDelegate { self.openQuicklyViewModel = .init(fileURL: url) self.commandsPaletteState = .init() - editorManager.restoreFromState(self) - utilityAreaModel.restoreFromState(self) + editorManager?.restoreFromState(self) + utilityAreaModel?.restoreFromState(self) } override func read(from url: URL, ofType typeName: String) throws { @@ -139,9 +140,20 @@ final class WorkspaceDocument: NSDocument, ObservableObject, NSToolbarDelegate { // MARK: Close Workspace override func close() { - editorManager.saveRestorationState(self) - utilityAreaModel.saveRestorationState(self) super.close() + editorManager?.saveRestorationState(self) + utilityAreaModel?.saveRestorationState(self) + + cancellables.forEach({ $0.cancel() }) + statusBarViewModel = nil + utilityAreaModel = nil + searchState = nil + editorManager = nil + openQuicklyViewModel = nil + commandsPaletteState = nil + sourceControlManager = nil + workspaceFileManager?.cleanUp() + workspaceFileManager = nil } /// Determines the windows should be closed. @@ -178,10 +190,10 @@ final class WorkspaceDocument: NSDocument, ObservableObject, NSToolbarDelegate { return } // Save unsaved changes before closing - let editedCodeFiles = editorManager.editorLayout + let editedCodeFiles = editorManager?.editorLayout .gatherOpenFiles() .compactMap(\.fileDocument) - .filter(\.isDocumentEdited) + .filter(\.isDocumentEdited) ?? [] for editedCodeFile in editedCodeFiles { let shouldClose = UnsafeMutablePointer.allocate(capacity: 1) @@ -207,9 +219,9 @@ final class WorkspaceDocument: NSDocument, ObservableObject, NSToolbarDelegate { implementation, to: (@convention(c)(Any, Selector, Any, Bool, UnsafeMutableRawPointer?) -> Void).self ) - let areAllOpenedCodeFilesClean = editorManager.editorLayout.gatherOpenFiles() + let areAllOpenedCodeFilesClean = editorManager?.editorLayout.gatherOpenFiles() .compactMap(\.fileDocument) - .allSatisfy { !$0.isDocumentEdited } + .allSatisfy { !$0.isDocumentEdited } ?? false function(object, shouldCloseSelector, self, areAllOpenedCodeFilesClean, contextInfo) } diff --git a/CodeEdit/Features/Documents/WorkspaceStateKey.swift b/CodeEdit/Features/Documents/WorkspaceDocument/WorkspaceStateKey.swift similarity index 100% rename from CodeEdit/Features/Documents/WorkspaceStateKey.swift rename to CodeEdit/Features/Documents/WorkspaceDocument/WorkspaceStateKey.swift diff --git a/CodeEdit/Features/Editor/Models/Editor.swift b/CodeEdit/Features/Editor/Models/Editor.swift index 805ec7816d..d532deb1c3 100644 --- a/CodeEdit/Features/Editor/Models/Editor.swift +++ b/CodeEdit/Features/Editor/Models/Editor.swift @@ -125,6 +125,9 @@ final class Editor: ObservableObject, Identifiable { } // Reset change count to 0 file.fileDocument?.updateChangeCount(.changeCleared) + if let codeFile = file.fileDocument { + codeFile.close() + } // remove file from memory file.fileDocument = nil } diff --git a/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarContextMenu.swift b/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarContextMenu.swift index 7732af00f1..7393f4fff7 100644 --- a/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarContextMenu.swift +++ b/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarContextMenu.swift @@ -139,7 +139,7 @@ struct EditorTabBarContextMenu: ViewModifier { let newEditor = Editor(files: [item]) splitEditor(edge, newEditor) tabs.closeTab(file: item) - workspace.editorManager.activeEditor = newEditor + workspace.editorManager?.activeEditor = newEditor } /// Copies the relative path from the workspace folder to the given file item to the pasteboard. diff --git a/CodeEdit/Features/Editor/Views/EditorLayoutView.swift b/CodeEdit/Features/Editor/Views/EditorLayoutView.swift index 0402aae008..2744a65d38 100644 --- a/CodeEdit/Features/Editor/Views/EditorLayoutView.swift +++ b/CodeEdit/Features/Editor/Views/EditorLayoutView.swift @@ -12,14 +12,14 @@ struct EditorLayoutView: View { @FocusState.Binding var focus: Editor? - @Environment(\.window) + @Environment(\.window.value) private var window @Environment(\.isAtEdge) private var isAtEdge var toolbarHeight: CGFloat { - window.contentView?.safeAreaInsets.top ?? .zero + window?.contentView?.safeAreaInsets.top ?? .zero } var body: some View { diff --git a/CodeEdit/Features/Extensions/ExtensionDiscovery.swift b/CodeEdit/Features/Extensions/ExtensionDiscovery.swift index 2eb309528b..36c28e96dc 100644 --- a/CodeEdit/Features/Extensions/ExtensionDiscovery.swift +++ b/CodeEdit/Features/Extensions/ExtensionDiscovery.swift @@ -25,30 +25,30 @@ final class ExtensionDiscovery: ObservableObject { /// These endpoints can be used to create new extension processes with XPC. @Published var extensions: [ExtensionInfo] = [] + private var discoverTask: Task? + private var availabilityTask: Task? + // Init is private as only 1 instance of this class may (needs to) exist. private init() { // Two separate tasks need to be used, as the awaits never finish. - Task { - await discover() - } - - Task { - await availabilityOverview() - } + discoverTask = discover() + availabilityTask = availabilityOverview() } /// Discover all the extensions approved by the user. Updates `extensions` when an extension gets enabled/disabled. /// Warning: This function will continue to run and won't return. Therefore, it should be ran in a separate `Task`. - private func discover() async { - print("Change in active extensions, reconnecting...") - do { - let sequence = try AppExtensionIdentity.matching(appExtensionPointIDs: Self.endPointIdentifier) + private func discover() -> Task { + Task { [weak self] in + do { + let sequence = try AppExtensionIdentity.matching(appExtensionPointIDs: Self.endPointIdentifier) - for await endpoints in sequence { - await updateExtensions(endpoints: endpoints, shouldRestartExisting: true) + for await endpoints in sequence { + guard !Task.isCancelled && self != nil else { return } + await self?.updateExtensions(endpoints: endpoints, shouldRestartExisting: true) + } + } catch { + print("Error while searching for extensions: \(error.localizedDescription)") } - } catch { - print("Error while searching for extensions: \(error.localizedDescription)") } } @@ -73,30 +73,33 @@ final class ExtensionDiscovery: ObservableObject { /// Observes extensions available on the system, and reports if extensions are disabled. /// These extensions must be enabled by the user first, before they can be discovered by `discover`. /// Warning: This function will continue to run and won't return. Therefore, it should be ran in a separate `Task`. - private func availabilityOverview() async { - for await availability in AppExtensionIdentity.availabilityUpdates { - print(availability) - do { - if availability.disabledCount > 0 { - print("Found \(availability.disabledCount) disabled extensions, trying to activate...") - try await activateDisabledExtensions() + private func availabilityOverview() -> Task { + Task { [weak self] in + for await availability in AppExtensionIdentity.availabilityUpdates { + guard !Task.isCancelled && self != nil else { return } + print(availability) + do { + if availability.disabledCount > 0 { + print("Found \(availability.disabledCount) disabled extensions, trying to activate...") + try await self?.activateDisabledExtensions() + } + + if availability.unapprovedCount > 0 { + print("Found \(availability.disabledCount) unapproved extensions, trying to activate...") + + let identifiers = [("com.tweety.TestCodeEdit.AutoActivatedExtension", "2MMGJGVTB4")] + try await self?.activateUnapprovedExtensions(with: identifiers) + } + + let sequence = try AppExtensionIdentity.matching(appExtensionPointIDs: Self.endPointIdentifier) + + let extensions = await sequence.first { _ in true } + + guard let extensions else { return } + await self?.updateExtensions(endpoints: extensions) + } catch { + print("Could not auto-activate extensions.") } - - if availability.unapprovedCount > 0 { - print("Found \(availability.disabledCount) unapproved extensions, trying to activate...") - - let identifiers = [("com.tweety.TestCodeEdit.AutoActivatedExtension", "2MMGJGVTB4")] - try await activateUnapprovedExtensions(with: identifiers) - } - - let sequence = try AppExtensionIdentity.matching(appExtensionPointIDs: Self.endPointIdentifier) - - let extensions = await sequence.first { _ in true } - - guard let extensions else { return } - await updateExtensions(endpoints: extensions) - } catch { - print("Could not auto-activate extensions.") } } } diff --git a/CodeEdit/Features/InspectorArea/FileInspector/FileInspectorView.swift b/CodeEdit/Features/InspectorArea/FileInspector/FileInspectorView.swift index b8d2c1c724..3707df1e8b 100644 --- a/CodeEdit/Features/InspectorArea/FileInspector/FileInspectorView.swift +++ b/CodeEdit/Features/InspectorArea/FileInspector/FileInspectorView.swift @@ -95,8 +95,8 @@ struct FileInspectorView: View { if !file.isFolder { editorManager.editorLayout.closeAllTabs(of: file) } - DispatchQueue.main.async { - workspace.workspaceFileManager?.move(file: file, to: destinationURL) + DispatchQueue.main.async { [weak workspace] in + workspace?.workspaceFileManager?.move(file: file, to: destinationURL) let newItem = CEWorkspaceFile(url: destinationURL) newItem.parent = file.parent if !newItem.isFolder { @@ -139,10 +139,10 @@ struct FileInspectorView: View { } // This is ugly but if the tab is opened at the same time as closing the others, it doesn't open // And if the files are re-built at the same time as the tab is opened, it causes a memory error - DispatchQueue.main.async { - workspace.workspaceFileManager?.move(file: file, to: newURL) + DispatchQueue.main.async { [weak workspace] in + workspace?.workspaceFileManager?.move(file: file, to: newURL) // If the parent directory doesn't exist in the workspace, don't open it in a tab. - if let newParent = workspace.workspaceFileManager?.getFile( + if let newParent = workspace?.workspaceFileManager?.getFile( newURL.deletingLastPathComponent().path ) { let newItem = CEWorkspaceFile(url: newURL) @@ -150,8 +150,8 @@ struct FileInspectorView: View { if !file.isFolder { editorManager.openTab(item: newItem) } - DispatchQueue.main.async { - _ = try? workspace.workspaceFileManager?.rebuildFiles(fromItem: newParent) + DispatchQueue.main.async { [weak workspace] in + _ = try? workspace?.workspaceFileManager?.rebuildFiles(fromItem: newParent) } } } diff --git a/CodeEdit/Features/Keybindings/CommandManager.swift b/CodeEdit/Features/Keybindings/CommandManager.swift index 394f58494d..f21f2ad81d 100644 --- a/CodeEdit/Features/Keybindings/CommandManager.swift +++ b/CodeEdit/Features/Keybindings/CommandManager.swift @@ -31,7 +31,7 @@ final class CommandManager: ObservableObject { static let shared: CommandManager = .init() - func addCommand(name: String, title: String, id: String, command: CommandClosureWrapper) { + func addCommand(name: String, title: String, id: String, command: @escaping () -> Void) { let command = Command.init(id: name, title: title, closureWrapper: command) commandsList[id] = command } @@ -41,7 +41,7 @@ final class CommandManager: ObservableObject { } func executeCommand(_ id: String) { - commandsList[id]?.closureWrapper.call() + commandsList[id]?.closureWrapper() } } @@ -62,24 +62,5 @@ struct Command: Identifiable, Hashable { let id: String let title: String - let closureWrapper: CommandClosureWrapper -} - -/// A simple wrapper for command closure -struct CommandClosureWrapper { - - /// A typealias of interface used for command closure declaration - typealias WorkspaceClientClosure = () -> Void - - let workspaceClientClosure: WorkspaceClientClosure? - - /// Initializer for closure wrapper - /// - Parameter closure: Function that contains all logic to run command. - init(closure: @escaping WorkspaceClientClosure) { - self.workspaceClientClosure = closure - } - - func call() { - workspaceClientClosure?() - } + let closureWrapper: () -> Void } diff --git a/CodeEdit/Features/LSP/LSPEventHandler.swift b/CodeEdit/Features/LSP/LSPEventHandler.swift index 187edf8e43..ae6ee28b40 100644 --- a/CodeEdit/Features/LSP/LSPEventHandler.swift +++ b/CodeEdit/Features/LSP/LSPEventHandler.swift @@ -46,25 +46,25 @@ extension LSPService { // swiftlint:disable:next cyclomatic_complexity private func handleRequest(_ request: ServerRequest) { switch request { - case let .workspaceConfiguration(params, handler): + case let .workspaceConfiguration(params, _): print("workspaceConfiguration: \(params)") case let .workspaceFolders(handler): print("workspaceFolders: \(String(describing: handler))") - case let .workspaceApplyEdit(params, handler): + case let .workspaceApplyEdit(params, _): print("workspaceApplyEdit: \(params)") - case let .clientRegisterCapability(params, handler): + case let .clientRegisterCapability(params, _): print("clientRegisterCapability: \(params)") - case let .clientUnregisterCapability(params, handler): + case let .clientUnregisterCapability(params, _): print("clientUnregisterCapability: \(params)") case let .workspaceCodeLensRefresh(handler): print("workspaceCodeLensRefresh: \(String(describing: handler))") case let .workspaceSemanticTokenRefresh(handler): print("workspaceSemanticTokenRefresh: \(String(describing: handler))") - case let .windowShowMessageRequest(params, handler): + case let .windowShowMessageRequest(params, _): print("windowShowMessageRequest: \(params)") - case let .windowShowDocument(params, handler): + case let .windowShowDocument(params, _): print("windowShowDocument: \(params)") - case let .windowWorkDoneProgressCreate(params, handler): + case let .windowWorkDoneProgressCreate(params, _): print("windowWorkDoneProgressCreate: \(params)") default: diff --git a/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorResultList/FindNavigatorListViewController.swift b/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorResultList/FindNavigatorListViewController.swift index 501686b721..b10b795214 100644 --- a/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorResultList/FindNavigatorListViewController.swift +++ b/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorResultList/FindNavigatorListViewController.swift @@ -181,7 +181,7 @@ extension FindNavigatorListViewController: NSOutlineViewDelegate { ) // We're using a medium label for file names b/c it makes it easier to // distinguish quickly which results are from which files. - view.label.font = .systemFont(ofSize: 13, weight: .medium) + view.textField?.font = .systemFont(ofSize: 13, weight: .medium) return view } } @@ -197,13 +197,13 @@ extension FindNavigatorListViewController: NSOutlineViewDelegate { let selectedMatch = self.selectedItem as? SearchResultMatchModel if selectedItem == nil || selectedMatch != item { self.selectedItem = item - workspace.editorManager.openTab(item: item.file) + workspace.editorManager?.openTab(item: item.file) } } else if let item = outlineView.item(atRow: selectedIndex) as? SearchResultModel { let selectedFile = self.selectedItem as? SearchResultModel if selectedItem == nil || selectedFile != item { self.selectedItem = item - workspace.editorManager.openTab(item: item.file) + workspace.editorManager?.openTab(item: item.file) } } } @@ -231,6 +231,17 @@ extension FindNavigatorListViewController: NSOutlineViewDelegate { outlineView.noteHeightOfRows(withIndexesChanged: indexes) } + // swiftlint:disable:next function_parameter_count + func outlineView( + _ outlineView: NSOutlineView, + toolTipFor cell: NSCell, + rect: NSRectPointer, + tableColumn: NSTableColumn?, + item: Any, + mouseLocation: NSPoint + ) -> String { + return "" + } } // MARK: - NSMenuDelegate diff --git a/CodeEdit/Features/NavigatorArea/OutlineView/FileSystemTableViewCell.swift b/CodeEdit/Features/NavigatorArea/OutlineView/FileSystemTableViewCell.swift index c2ca1beef6..8b86579373 100644 --- a/CodeEdit/Features/NavigatorArea/OutlineView/FileSystemTableViewCell.swift +++ b/CodeEdit/Features/NavigatorArea/OutlineView/FileSystemTableViewCell.swift @@ -9,7 +9,7 @@ import SwiftUI class FileSystemTableViewCell: StandardTableViewCell { - var fileItem: CEWorkspaceFile! + weak var fileItem: CEWorkspaceFile? var changeLabelLargeWidth: NSLayoutConstraint! var changeLabelSmallWidth: NSLayoutConstraint! @@ -38,15 +38,14 @@ class FileSystemTableViewCell: StandardTableViewCell { func addIcon(item: CEWorkspaceFile) { fileItem = item - icon.image = item.nsIcon - icon.contentTintColor = color(for: item) - toolTip = item.labelFileName() - label.stringValue = item.labelFileName() + imageView?.image = item.nsIcon + imageView?.contentTintColor = color(for: item) + textField?.stringValue = item.labelFileName() } func addModel() { - secondaryLabel.stringValue = fileItem.gitStatus?.description ?? "" - if secondaryLabel.stringValue == "?" { secondaryLabel.stringValue = "A" } + secondaryLabel?.stringValue = fileItem?.gitStatus?.description ?? "" + if secondaryLabel?.stringValue == "?" { secondaryLabel?.stringValue = "A" } } /// *Not Implemented* @@ -90,20 +89,27 @@ class FileSystemTableViewCell: StandardTableViewCell { return NSColor(named: "CoolGray") ?? NSColor(.gray) } } + + deinit { + toolTip = nil + } } let errorRed = NSColor(red: 1, green: 0, blue: 0, alpha: 0.2) extension FileSystemTableViewCell: NSTextFieldDelegate { func controlTextDidChange(_ obj: Notification) { - label.backgroundColor = fileItem.validateFileName(for: label?.stringValue ?? "") ? .none : errorRed + guard let fileItem else { return } + textField?.backgroundColor = fileItem.validateFileName(for: textField?.stringValue ?? "") ? .none : errorRed } + func controlTextDidEndEditing(_ obj: Notification) { - label.backgroundColor = fileItem.validateFileName(for: label?.stringValue ?? "") ? .none : errorRed - if fileItem.validateFileName(for: label?.stringValue ?? "") { - let newURL = fileItem.url.deletingLastPathComponent().appendingPathComponent(label?.stringValue ?? "") + guard let fileItem else { return } + textField?.backgroundColor = fileItem.validateFileName(for: textField?.stringValue ?? "") ? .none : errorRed + if fileItem.validateFileName(for: textField?.stringValue ?? "") { + let newURL = fileItem.url.deletingLastPathComponent().appendingPathComponent(textField?.stringValue ?? "") workspace?.workspaceFileManager?.move(file: fileItem, to: newURL) } else { - label?.stringValue = fileItem.labelFileName() + textField?.stringValue = fileItem.labelFileName() } } } diff --git a/CodeEdit/Features/NavigatorArea/OutlineView/StandardTableViewCell.swift b/CodeEdit/Features/NavigatorArea/OutlineView/StandardTableViewCell.swift index e4611b308a..694baaa53a 100644 --- a/CodeEdit/Features/NavigatorArea/OutlineView/StandardTableViewCell.swift +++ b/CodeEdit/Features/NavigatorArea/OutlineView/StandardTableViewCell.swift @@ -9,20 +9,15 @@ import SwiftUI class StandardTableViewCell: NSTableCellView { - var label: NSTextField! - var secondaryLabel: NSTextField! - var icon: NSImageView! + weak var secondaryLabel: NSTextField? + weak var workspace: WorkspaceDocument? - var workspace: WorkspaceDocument? - - var secondaryLabelRightAlignmed: Bool = true { + var secondaryLabelRightAligned: Bool = true { didSet { resizeSubviews(withOldSize: .zero) } } - private let prefs = Settings.shared.preferences.general - /// Initializes the `TableViewCell` with an `icon` and `label` /// Both the icon and label will be colored, and sized based on the user's preferences. /// - Parameters: @@ -42,16 +37,17 @@ class StandardTableViewCell: NSTableCellView { private func setupViews(frame frameRect: NSRect, isEditable: Bool) { // Create the label - label = createLabel() - configLabel(label: self.label, isEditable: isEditable) + let label = createLabel() + configLabel(label: label, isEditable: isEditable) self.textField = label // Create the secondary label - secondaryLabel = createSecondaryLabel() + let secondaryLabel = createSecondaryLabel() configSecondaryLabel(secondaryLabel: secondaryLabel) + self.secondaryLabel = secondaryLabel // Create the icon - icon = createIcon() + let icon = createIcon() configIcon(icon: icon) addSubview(icon) imageView = icon @@ -111,16 +107,25 @@ class StandardTableViewCell: NSTableCellView { let iconWidth: CGFloat = 22 override func resizeSubviews(withOldSize oldSize: NSSize) { super.resizeSubviews(withOldSize: oldSize) + guard let imageView, textField != nil, secondaryLabel != nil else { + assertionFailure( + "Missing child view:" + + " imageView \(imageView == nil)" + + ", textField: \(textField == nil)" + + ", label: \(secondaryLabel == nil)" + ) + return + } - icon.frame = NSRect( + imageView.frame = NSRect( x: 2, y: 4, width: iconWidth, height: frame.height ) // center align the image - if let alignmentRect = icon.image?.alignmentRect { - icon.frame = NSRect( + if let alignmentRect = imageView.image?.alignmentRect { + imageView.frame = NSRect( x: (iconWidth - alignmentRect.width) / 2, y: 4, width: alignmentRect.width, @@ -129,45 +134,54 @@ class StandardTableViewCell: NSTableCellView { } // right align the secondary label - if secondaryLabelRightAlignmed { - let secondLabelWidth = secondaryLabel.frame.size.width - let newSize = secondaryLabel.sizeThatFits( - CGSize(width: secondLabelWidth, height: CGFloat.greatestFiniteMagnitude) - ) - // somehow, a width of 0 makes it resize properly. - secondaryLabel.frame = NSRect( - x: frame.width - newSize.width, - y: 3.5, - width: 0, - height: newSize.height - ) - - label.frame = NSRect( - x: iconWidth + 2, - y: 3.5, - width: secondaryLabel.frame.minX - icon.frame.maxX - 5, - height: 25 - ) - - // put the secondary label right after the primary label + if secondaryLabelRightAligned { + rightAlignSecondary() } else { - let mainLabelWidth = label.frame.size.width - let newSize = label.sizeThatFits(CGSize(width: mainLabelWidth, height: CGFloat.greatestFiniteMagnitude)) - label.frame = NSRect( - x: iconWidth + 2, - y: 2.5, - width: newSize.width, - height: 25 - ) - secondaryLabel.frame = NSRect( - x: label.frame.maxX + 2, - y: 2.5, - width: frame.width - label.frame.maxX - 2, - height: 25 - ) + // put the secondary label right after the primary label + leftAlignSecondary() } } + private func rightAlignSecondary() { + guard let secondaryLabel, let textField, let imageView else { return } + let secondLabelWidth = secondaryLabel.frame.size.width + let newSize = secondaryLabel.sizeThatFits( + CGSize(width: secondLabelWidth, height: CGFloat.greatestFiniteMagnitude) + ) + // somehow, a width of 0 makes it resize properly. + secondaryLabel.frame = NSRect( + x: frame.width - newSize.width, + y: 3.5, + width: 0, + height: newSize.height + ) + + textField.frame = NSRect( + x: iconWidth + 2, + y: 3.5, + width: secondaryLabel.frame.minX - imageView.frame.maxX - 5, + height: 25 + ) + } + + private func leftAlignSecondary() { + guard let secondaryLabel, let textField else { return } + let mainLabelWidth = textField.frame.size.width + let newSize = textField.sizeThatFits(CGSize(width: mainLabelWidth, height: CGFloat.greatestFiniteMagnitude)) + textField.frame = NSRect( + x: iconWidth + 2, + y: 2.5, + width: newSize.width, + height: 25 + ) + secondaryLabel.frame = NSRect( + x: textField.frame.maxX + 2, + y: 2.5, + width: frame.width - textField.frame.maxX - 2, + height: 25 + ) + } + /// *Not Implemented* required init?(coder: NSCoder) { fatalError(""" diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenu.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenu.swift index 78d2b7e8da..b6a8dd8e7d 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenu.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenu.swift @@ -194,7 +194,7 @@ final class ProjectNavigatorMenu: NSMenu { @objc private func openInTab() { if let item { - workspace?.editorManager.openTab(item: item) + workspace?.editorManager?.openTab(item: item) } } diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift index 851f0c5962..d682f8704c 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift @@ -35,7 +35,7 @@ struct ProjectNavigatorOutlineView: NSViewControllerRepresentable { nsViewController.shownFileExtensions = prefs.preferences.general.shownFileExtensions nsViewController.hiddenFileExtensions = prefs.preferences.general.hiddenFileExtensions /// if the window becomes active from background, it will restore the selection to outline view. - nsViewController.updateSelection(itemID: workspace.editorManager.activeEditor.selectedTab?.file.id) + nsViewController.updateSelection(itemID: workspace.editorManager?.activeEditor.selectedTab?.file.id) return } @@ -56,7 +56,7 @@ struct ProjectNavigatorOutlineView: NSViewControllerRepresentable { self?.controller?.reveal(fileItem) }) .store(in: &cancellables) - workspace.editorManager.tabBarTabIdSubject + workspace.editorManager?.tabBarTabIdSubject .sink { [weak self] itemID in self?.controller?.updateSelection(itemID: itemID) } @@ -64,8 +64,8 @@ struct ProjectNavigatorOutlineView: NSViewControllerRepresentable { } var cancellables: Set = [] - var workspace: WorkspaceDocument - var controller: ProjectNavigatorViewController? + weak var workspace: WorkspaceDocument? + weak var controller: ProjectNavigatorViewController? func fileManagerUpdated(updatedItems: Set) { guard let outlineView = controller?.outlineView else { return } @@ -74,11 +74,11 @@ struct ProjectNavigatorOutlineView: NSViewControllerRepresentable { outlineView.reloadItem(item, reloadChildren: true) } - controller?.updateSelection(itemID: workspace.editorManager.activeEditor.selectedTab?.file.id) + controller?.updateSelection(itemID: workspace?.editorManager?.activeEditor.selectedTab?.file.id) } deinit { - workspace.workspaceFileManager?.removeObserver(self) + workspace?.workspaceFileManager?.removeObserver(self) } } } diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorTableViewCell.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorTableViewCell.swift index 538e414921..ac761a1b46 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorTableViewCell.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorTableViewCell.swift @@ -14,7 +14,7 @@ protocol OutlineTableViewCellDelegate: AnyObject { /// A `NSTableCellView` showing an ``icon`` and a ``label`` final class ProjectNavigatorTableViewCell: FileSystemTableViewCell { - private var delegate: OutlineTableViewCellDelegate? + private weak var delegate: OutlineTableViewCellDelegate? /// Initializes the `OutlineTableViewCell` with an `icon` and `label` /// Both the icon and label will be colored, and sized based on the user's preferences. @@ -51,14 +51,15 @@ final class ProjectNavigatorTableViewCell: FileSystemTableViewCell { } override func controlTextDidEndEditing(_ obj: Notification) { - label.backgroundColor = fileItem.validateFileName(for: label?.stringValue ?? "") ? .none : errorRed - if fileItem.validateFileName(for: label?.stringValue ?? "") { + guard let fileItem else { return } + textField?.backgroundColor = fileItem.validateFileName(for: textField?.stringValue ?? "") ? .none : errorRed + if fileItem.validateFileName(for: textField?.stringValue ?? "") { let destinationURL = fileItem.url .deletingLastPathComponent() - .appendingPathComponent(label?.stringValue ?? "") + .appendingPathComponent(textField?.stringValue ?? "") delegate?.moveFile(file: fileItem, to: destinationURL) } else { - label?.stringValue = fileItem.labelFileName() + textField?.stringValue = fileItem.labelFileName() } } } diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDataSource.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDataSource.swift new file mode 100644 index 0000000000..b2782a3e7f --- /dev/null +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDataSource.swift @@ -0,0 +1,119 @@ +// +// ProjectNavigatorViewController+NSOutlineViewDataSource.swift +// CodeEdit +// +// Created by Khan Winter on 7/13/24. +// + +import AppKit + +extension ProjectNavigatorViewController: NSOutlineViewDataSource { + func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int { + if let item = item as? CEWorkspaceFile { + return item.isFolder ? workspace?.workspaceFileManager?.childrenOfFile(item)?.count ?? 0 : 0 + } + return content.count + } + + func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any { + if let item = item as? CEWorkspaceFile, + let children = workspace?.workspaceFileManager?.childrenOfFile(item) { + return children[index] + } + return content[index] + } + + func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: Any) -> Bool { + if let item = item as? CEWorkspaceFile { + return item.isFolder + } + return false + } + + /// write dragged file(s) to pasteboard + func outlineView(_ outlineView: NSOutlineView, pasteboardWriterForItem item: Any) -> NSPasteboardWriting? { + guard let fileItem = item as? CEWorkspaceFile else { return nil } + return fileItem.url as NSURL + } + + /// declare valid drop target + func outlineView( + _ outlineView: NSOutlineView, + validateDrop info: NSDraggingInfo, + proposedItem item: Any?, + proposedChildIndex index: Int + ) -> NSDragOperation { + guard let fileItem = item as? CEWorkspaceFile else { return [] } + // -1 index indicates that we are hovering over a row in outline view (folder or file) + if index == -1 { + if !fileItem.isFolder { + outlineView.setDropItem(fileItem.parent, dropChildIndex: index) + } + return info.draggingSourceOperationMask == .copy ? .copy : .move + } + return [] + } + + /// handle successful or unsuccessful drop + func outlineView( + _ outlineView: NSOutlineView, + acceptDrop info: NSDraggingInfo, + item: Any?, + childIndex index: Int + ) -> Bool { + guard let pasteboardItems = info.draggingPasteboard.readObjects(forClasses: [NSURL.self]) else { return false } + let fileItemURLS = pasteboardItems.compactMap { $0 as? URL } + + guard let fileItemDestination = item as? CEWorkspaceFile else { return false } + let destParentURL = fileItemDestination.url + + for fileItemURL in fileItemURLS { + let destURL = destParentURL.appendingPathComponent(fileItemURL.lastPathComponent) + // cancel dropping file item on self or in parent directory + if fileItemURL == destURL || fileItemURL == destParentURL { + return false + } + + // Needs to come before call to .removeItem or else race condition occurs + var srcFileItem: CEWorkspaceFile? = workspace?.workspaceFileManager?.getFile(fileItemURL.path) + // If srcFileItem is nil, fileItemUrl is an external file url. + if srcFileItem == nil { + srcFileItem = CEWorkspaceFile(url: URL(fileURLWithPath: fileItemURL.path)) + } + + guard let srcFileItem else { + return false + } + + if CEWorkspaceFile.fileManager.fileExists(atPath: destURL.path) { + let shouldReplace = replaceFileDialog(fileName: fileItemURL.lastPathComponent) + guard shouldReplace else { + return false + } + do { + try CEWorkspaceFile.fileManager.removeItem(at: destURL) + } catch { + fatalError(error.localizedDescription) + } + } + if info.draggingSourceOperationMask == .copy { + self.copyFile(file: srcFileItem, to: destURL) + } else { + self.moveFile(file: srcFileItem, to: destURL) + } + } + return true + } + + func replaceFileDialog(fileName: String) -> Bool { + let alert = NSAlert() + alert.messageText = """ + A file or folder with the name \(fileName) already exists in the destination folder. Do you want to replace it? + """ + alert.informativeText = "This action is irreversible!" + alert.alertStyle = .warning + alert.addButton(withTitle: "Replace") + alert.addButton(withTitle: "Cancel") + return alert.runModal() == .alertFirstButtonReturn + } +} diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift new file mode 100644 index 0000000000..37215b3d71 --- /dev/null +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift @@ -0,0 +1,159 @@ +// +// ProjectNavigatorViewController+NSOutlineViewDelegate.swift +// CodeEdit +// +// Created by Khan Winter on 7/13/24. +// + +import AppKit + +extension ProjectNavigatorViewController: NSOutlineViewDelegate { + func outlineView( + _ outlineView: NSOutlineView, + shouldShowCellExpansionFor tableColumn: NSTableColumn?, + item: Any + ) -> Bool { + true + } + + func outlineView(_ outlineView: NSOutlineView, shouldShowOutlineCellForItem item: Any) -> Bool { + true + } + + func outlineView(_ outlineView: NSOutlineView, viewFor tableColumn: NSTableColumn?, item: Any) -> NSView? { + guard let tableColumn else { return nil } + + let frameRect = NSRect(x: 0, y: 0, width: tableColumn.width, height: rowHeight) + + return ProjectNavigatorTableViewCell(frame: frameRect, item: item as? CEWorkspaceFile, delegate: self) + } + + func outlineViewSelectionDidChange(_ notification: Notification) { + guard let outlineView = notification.object as? NSOutlineView else { return } + + let selectedIndex = outlineView.selectedRow + + guard let item = outlineView.item(atRow: selectedIndex) as? CEWorkspaceFile else { return } + + if !item.isFolder && shouldSendSelectionUpdate { + DispatchQueue.main.async { + self.workspace?.editorManager?.activeEditor.openTab(file: item, asTemporary: true) + } + } + } + + func outlineView(_ outlineView: NSOutlineView, heightOfRowByItem item: Any) -> CGFloat { + rowHeight // This can be changed to 20 to match Xcode's row height. + } + + func outlineViewItemDidExpand(_ notification: Notification) { + guard + let id = workspace?.editorManager?.activeEditor.selectedTab?.file.id, + let item = workspace?.workspaceFileManager?.getFile(id, createIfNotFound: true) + else { return } + /// update outline selection only if the parent of selected item match with expanded item + guard item.parent === notification.userInfo?["NSObject"] as? CEWorkspaceFile else { return } + /// select active file under collapsed folder only if its parent is expanding + if outlineView.isItemExpanded(item.parent) { + updateSelection(itemID: item.id) + } + } + + func outlineViewItemDidCollapse(_ notification: Notification) {} + + func outlineView(_ outlineView: NSOutlineView, itemForPersistentObject object: Any) -> Any? { + guard let id = object as? CEWorkspaceFile.ID, + let item = workspace?.workspaceFileManager?.getFile(id, createIfNotFound: true) else { return nil } + return item + } + + func outlineView(_ outlineView: NSOutlineView, persistentObjectForItem item: Any?) -> Any? { + guard let item = item as? CEWorkspaceFile else { return nil } + return item.id + } + + /// Finds and selects an ``Item`` from an array of ``Item`` and their `children` based on the `id`. + /// - Parameters: + /// - id: the id of the item item + /// - collection: the array to search for + /// - forcesReveal: The boolean to indicates whether or not it should force to reveal the selected file. + func select(by id: EditorTabID, forcesReveal: Bool) { + guard case .codeEditor(let path) = id, + let item = workspace?.workspaceFileManager?.getFile(path, createIfNotFound: true) else { + return + } + // If the user has set "Reveal file on selection change" to on or it is forced to reveal, + // we need to reveal the item before selecting the row. + if Settings.shared.preferences.general.revealFileOnFocusChange || forcesReveal { + reveal(item) + } + let row = outlineView.row(forItem: item) + if row == -1 { + outlineView.deselectRow(outlineView.selectedRow) + } + shouldSendSelectionUpdate = false + outlineView.selectRowIndexes(.init(integer: row), byExtendingSelection: false) + shouldSendSelectionUpdate = true + } + + /// Reveals the given `fileItem` in the outline view by expanding all the parent directories of the file. + /// If the file is not found, it will present an alert saying so. + /// - Parameter fileItem: The file to reveal. + public func reveal(_ fileItem: CEWorkspaceFile) { + if let parent = fileItem.parent { + expandParent(item: parent) + } + let row = outlineView.row(forItem: fileItem) + outlineView.selectRowIndexes(.init(integer: row), byExtendingSelection: false) + + if row < 0 { + let alert = NSAlert() + alert.messageText = NSLocalizedString( + "Could not find file", + comment: "Could not find file" + ) + alert.runModal() + return + } else { + let visibleRect = scrollView.contentView.visibleRect + let visibleRows = outlineView.rows(in: visibleRect) + guard !visibleRows.contains(row) else { + /// in case that the selected file is not fully visible (some parts are out of the visible rect), + /// `scrollRowToVisible(_:)` method brings the file where it can be fully visible. + outlineView.scrollRowToVisible(row) + return + } + let rowRect = outlineView.rect(ofRow: row) + let centerY = rowRect.midY - (visibleRect.height / 2) + let center = NSPoint(x: 0, y: centerY) + /// `scroll(_:)` method alone doesn't bring the selected file to the center in some cases. + /// calling `scrollRowToVisible(_:)` method before it makes the file reveal in the center more correctly. + outlineView.scrollRowToVisible(row) + outlineView.scroll(center) + } + } + + /// Method for recursively expanding a file's parent directories. + /// - Parameter item: + private func expandParent(item: CEWorkspaceFile) { + if let parent = item.parent as CEWorkspaceFile? { + expandParent(item: parent) + } + outlineView.expandItem(item) + } + + // swiftlint:disable:next function_parameter_count + func outlineView( + _ outlineView: NSOutlineView, + toolTipFor cell: NSCell, + rect: NSRectPointer, + tableColumn: NSTableColumn?, + item: Any, + mouseLocation: NSPoint + ) -> String { + if let file = item as? CEWorkspaceFile { + return file.name + } + return "" + } +} diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+OutlineTableViewCellDelegate.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+OutlineTableViewCellDelegate.swift index fbb3edd38f..633d770937 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+OutlineTableViewCellDelegate.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+OutlineTableViewCellDelegate.swift @@ -12,11 +12,11 @@ import Foundation extension ProjectNavigatorViewController: OutlineTableViewCellDelegate { func moveFile(file: CEWorkspaceFile, to destination: URL) { if !file.isFolder { - workspace?.editorManager.editorLayout.closeAllTabs(of: file) + workspace?.editorManager?.editorLayout.closeAllTabs(of: file) } workspace?.workspaceFileManager?.move(file: file, to: destination) if !file.isFolder { - workspace?.editorManager.openTab(item: .init(url: destination)) + workspace?.editorManager?.openTab(item: .init(url: destination)) } } diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift index df961ef065..08a9dc2cb7 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift @@ -20,13 +20,13 @@ final class ProjectNavigatorViewController: NSViewController { /// Gets the folder structure /// /// Also creates a top level item "root" which represents the projects root directory and automatically expands it. - private var content: [CEWorkspaceFile] { + var content: [CEWorkspaceFile] { guard let folderURL = workspace?.workspaceFileManager?.folderUrl else { return [] } guard let root = workspace?.workspaceFileManager?.getFile(folderURL.path) else { return [] } return [root] } - var workspace: WorkspaceDocument? + weak var workspace: WorkspaceDocument? var iconColor: SettingsData.FileIconStyle = .color { willSet { @@ -52,7 +52,7 @@ final class ProjectNavigatorViewController: NSViewController { /// This helps determine whether or not to send an `openTab` when the selection changes. /// Used b/c the state may update when the selection changes, but we don't necessarily want /// to open the file a second time. - private var shouldSendSelectionUpdate: Bool = true + var shouldSendSelectionUpdate: Bool = true /// Setup the ``scrollView`` and ``outlineView`` override func loadView() { @@ -92,6 +92,11 @@ final class ProjectNavigatorViewController: NSViewController { super.init(nibName: nil, bundle: nil) } + deinit { + outlineView?.removeFromSuperview() + scrollView?.removeFromSuperview() + } + required init?(coder: NSCoder) { fatalError() } @@ -99,7 +104,7 @@ final class ProjectNavigatorViewController: NSViewController { /// Forces to reveal the selected file through the command regardless of the auto reveal setting @objc func revealFile(_ sender: Any) { - updateSelection(itemID: workspace?.editorManager.activeEditor.selectedTab?.file.id, forcesReveal: true) + updateSelection(itemID: workspace?.editorManager?.activeEditor.selectedTab?.file.id, forcesReveal: true) } /// Updates the selection of the ``outlineView`` whenever it changes. @@ -112,7 +117,7 @@ final class ProjectNavigatorViewController: NSViewController { outlineView.deselectRow(outlineView.selectedRow) return } - select(by: .codeEditor(itemID), forcesReveal: forcesReveal) + self.select(by: .codeEditor(itemID), forcesReveal: forcesReveal) } /// Expand or collapse the folder on double click @@ -127,7 +132,7 @@ final class ProjectNavigatorViewController: NSViewController { outlineView.expandItem(item) } } else if Settings[\.navigation].navigationStyle == .openInTabs { - workspace?.editorManager.activeEditor.openTab(file: item, asTemporary: false) + workspace?.editorManager?.activeEditor.openTab(file: item, asTemporary: false) } } @@ -144,253 +149,3 @@ final class ProjectNavigatorViewController: NSViewController { // TODO: File filtering } - -// MARK: - NSOutlineViewDataSource - -extension ProjectNavigatorViewController: NSOutlineViewDataSource { - func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int { - if let item = item as? CEWorkspaceFile { - return item.isFolder ? workspace?.workspaceFileManager?.childrenOfFile(item)?.count ?? 0 : 0 - } - return content.count - } - - func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any { - if let item = item as? CEWorkspaceFile, - let children = workspace?.workspaceFileManager?.childrenOfFile(item) { - return children[index] - } - return content[index] - } - - func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: Any) -> Bool { - if let item = item as? CEWorkspaceFile { - return item.isFolder - } - return false - } - - /// write dragged file(s) to pasteboard - func outlineView(_ outlineView: NSOutlineView, pasteboardWriterForItem item: Any) -> NSPasteboardWriting? { - guard let fileItem = item as? CEWorkspaceFile else { return nil } - return fileItem.url as NSURL - } - - /// declare valid drop target - func outlineView( - _ outlineView: NSOutlineView, - validateDrop info: NSDraggingInfo, - proposedItem item: Any?, - proposedChildIndex index: Int - ) -> NSDragOperation { - guard let fileItem = item as? CEWorkspaceFile else { return [] } - // -1 index indicates that we are hovering over a row in outline view (folder or file) - if index == -1 { - if !fileItem.isFolder { - outlineView.setDropItem(fileItem.parent, dropChildIndex: index) - } - return info.draggingSourceOperationMask == .copy ? .copy : .move - } - return [] - } - - /// handle successful or unsuccessful drop - func outlineView( - _ outlineView: NSOutlineView, - acceptDrop info: NSDraggingInfo, - item: Any?, - childIndex index: Int - ) -> Bool { - guard let pasteboardItems = info.draggingPasteboard.readObjects(forClasses: [NSURL.self]) else { return false } - let fileItemURLS = pasteboardItems.compactMap { $0 as? URL } - - guard let fileItemDestination = item as? CEWorkspaceFile else { return false } - let destParentURL = fileItemDestination.url - - for fileItemURL in fileItemURLS { - let destURL = destParentURL.appendingPathComponent(fileItemURL.lastPathComponent) - // cancel dropping file item on self or in parent directory - if fileItemURL == destURL || fileItemURL == destParentURL { - return false - } - - // Needs to come before call to .removeItem or else race condition occurs - var srcFileItem: CEWorkspaceFile? = workspace?.workspaceFileManager?.getFile(fileItemURL.path) - // If srcFileItem is nil, fileItemUrl is an external file url. - if srcFileItem == nil { - srcFileItem = CEWorkspaceFile(url: URL(fileURLWithPath: fileItemURL.path)) - } - - guard let srcFileItem else { - return false - } - - if CEWorkspaceFile.fileManager.fileExists(atPath: destURL.path) { - let shouldReplace = replaceFileDialog(fileName: fileItemURL.lastPathComponent) - guard shouldReplace else { - return false - } - do { - try CEWorkspaceFile.fileManager.removeItem(at: destURL) - } catch { - fatalError(error.localizedDescription) - } - } - if info.draggingSourceOperationMask == .copy { - self.copyFile(file: srcFileItem, to: destURL) - } else { - self.moveFile(file: srcFileItem, to: destURL) - } - } - return true - } - - func replaceFileDialog(fileName: String) -> Bool { - let alert = NSAlert() - alert.messageText = """ - A file or folder with the name \(fileName) already exists in the destination folder. Do you want to replace it? - """ - alert.informativeText = "This action is irreversible!" - alert.alertStyle = .warning - alert.addButton(withTitle: "Replace") - alert.addButton(withTitle: "Cancel") - return alert.runModal() == .alertFirstButtonReturn - } -} - -// MARK: - NSOutlineViewDelegate -extension ProjectNavigatorViewController: NSOutlineViewDelegate { - func outlineView( - _ outlineView: NSOutlineView, - shouldShowCellExpansionFor tableColumn: NSTableColumn?, - item: Any - ) -> Bool { - true - } - - func outlineView(_ outlineView: NSOutlineView, shouldShowOutlineCellForItem item: Any) -> Bool { - true - } - - func outlineView(_ outlineView: NSOutlineView, viewFor tableColumn: NSTableColumn?, item: Any) -> NSView? { - guard let tableColumn else { return nil } - - let frameRect = NSRect(x: 0, y: 0, width: tableColumn.width, height: rowHeight) - - return ProjectNavigatorTableViewCell(frame: frameRect, item: item as? CEWorkspaceFile, delegate: self) - } - - func outlineViewSelectionDidChange(_ notification: Notification) { - guard let outlineView = notification.object as? NSOutlineView else { return } - - let selectedIndex = outlineView.selectedRow - - guard let item = outlineView.item(atRow: selectedIndex) as? CEWorkspaceFile else { return } - - if !item.isFolder && shouldSendSelectionUpdate { - DispatchQueue.main.async { - self.workspace?.editorManager.activeEditor.openTab(file: item, asTemporary: true) - } - } - } - - func outlineView(_ outlineView: NSOutlineView, heightOfRowByItem item: Any) -> CGFloat { - rowHeight // This can be changed to 20 to match Xcode's row height. - } - - func outlineViewItemDidExpand(_ notification: Notification) { - guard - let id = workspace?.editorManager.activeEditor.selectedTab?.file.id, - let item = workspace?.workspaceFileManager?.getFile(id, createIfNotFound: true) - else { return } - /// update outline selection only if the parent of selected item match with expanded item - guard item.parent === notification.userInfo?["NSObject"] as? CEWorkspaceFile else { return } - /// select active file under collapsed folder only if its parent is expanding - if outlineView.isItemExpanded(item.parent) { - updateSelection(itemID: item.id) - } - } - - func outlineViewItemDidCollapse(_ notification: Notification) {} - - func outlineView(_ outlineView: NSOutlineView, itemForPersistentObject object: Any) -> Any? { - guard let id = object as? CEWorkspaceFile.ID, - let item = workspace?.workspaceFileManager?.getFile(id, createIfNotFound: true) else { return nil } - return item - } - - func outlineView(_ outlineView: NSOutlineView, persistentObjectForItem item: Any?) -> Any? { - guard let item = item as? CEWorkspaceFile else { return nil } - return item.id - } - - /// Finds and selects an ``Item`` from an array of ``Item`` and their `children` based on the `id`. - /// - Parameters: - /// - id: the id of the item item - /// - collection: the array to search for - /// - forcesReveal: The boolean to indicates whether or not it should force to reveal the selected file. - private func select(by id: EditorTabID, forcesReveal: Bool) { - guard case .codeEditor(let path) = id, - let item = workspace?.workspaceFileManager?.getFile(path, createIfNotFound: true) else { - return - } - // If the user has set "Reveal file on selection change" to on or it is forced to reveal, - // we need to reveal the item before selecting the row. - if Settings.shared.preferences.general.revealFileOnFocusChange || forcesReveal { - reveal(item) - } - let row = outlineView.row(forItem: item) - if row == -1 { - outlineView.deselectRow(outlineView.selectedRow) - } - shouldSendSelectionUpdate = false - outlineView.selectRowIndexes(.init(integer: row), byExtendingSelection: false) - shouldSendSelectionUpdate = true - } - - /// Reveals the given `fileItem` in the outline view by expanding all the parent directories of the file. - /// If the file is not found, it will present an alert saying so. - /// - Parameter fileItem: The file to reveal. - public func reveal(_ fileItem: CEWorkspaceFile) { - if let parent = fileItem.parent { - expandParent(item: parent) - } - let row = outlineView.row(forItem: fileItem) - outlineView.selectRowIndexes(.init(integer: row), byExtendingSelection: false) - - if row < 0 { - let alert = NSAlert() - alert.messageText = NSLocalizedString( - "Could not find file", - comment: "Could not find file" - ) - alert.runModal() - return - } else { - let visibleRect = scrollView.contentView.visibleRect - let visibleRows = outlineView.rows(in: visibleRect) - guard !visibleRows.contains(row) else { - /// in case that the selected file is not fully visible (some parts are out of the visible rect), - /// `scrollRowToVisible(_:)` method brings the file where it can be fully visible. - outlineView.scrollRowToVisible(row) - return - } - let rowRect = outlineView.rect(ofRow: row) - let centerY = rowRect.midY - (visibleRect.height / 2) - let center = NSPoint(x: 0, y: centerY) - /// `scroll(_:)` method alone doesn't bring the selected file to the center in some cases. - /// calling `scrollRowToVisible(_:)` method before it makes the file reveal in the center more correctly. - outlineView.scrollRowToVisible(row) - outlineView.scroll(center) - } - } - - /// Method for recursively expanding a file's parent directories. - /// - Parameter item: - private func expandParent(item: CEWorkspaceFile) { - if let parent = item.parent as CEWorkspaceFile? { - expandParent(item: parent) - } - outlineView.expandItem(item) - } -} diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorChangesList.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorChangesList.swift index 857dedd238..3adf7b8354 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorChangesList.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorChangesList.swift @@ -37,7 +37,7 @@ struct SourceControlNavigatorChangesList: View { Group { Button("Open in New Tab") { DispatchQueue.main.async { - workspace.editorManager.openTab(item: file) + workspace.editorManager?.openTab(item: file) } } Button("Open in New Window") {} @@ -62,7 +62,7 @@ struct SourceControlNavigatorChangesList: View { selectedFiles.count == 1, let file = selection.first { DispatchQueue.main.async { - workspace.editorManager.openTab(item: file) + workspace.editorManager?.openTab(item: file) } } } @@ -72,7 +72,7 @@ struct SourceControlNavigatorChangesList: View { newSelection.count == 1, let file = newSelection.first { DispatchQueue.main.async { - workspace.editorManager.openTab(item: file) + workspace.editorManager?.openTab(item: file) } } } diff --git a/CodeEdit/Features/Settings/Pages/Keybindings/Models/KeybindingsSettings.swift b/CodeEdit/Features/Settings/Pages/Keybindings/Models/KeybindingsSettings.swift index 429d455528..883ac3a175 100644 --- a/CodeEdit/Features/Settings/Pages/Keybindings/Models/KeybindingsSettings.swift +++ b/CodeEdit/Features/Settings/Pages/Keybindings/Models/KeybindingsSettings.swift @@ -28,16 +28,6 @@ extension SettingsData { forKey: .keybindings ) ?? .init() appendNew() - - let mgr = CommandManager.shared - let wrap = CommandClosureWrapper.init(closure: { - print("testing closure") - }) - mgr.addCommand( - name: "Send test to console", - title: "Send test to console", id: "codeedit.test", command: wrap - ) - mgr.executeCommand("test") } /// Adds new keybindings if they were added to default_keybindings.json. diff --git a/CodeEdit/Features/Settings/Pages/TextEditingSettings/Models/TextEditingSettings.swift b/CodeEdit/Features/Settings/Pages/TextEditingSettings/Models/TextEditingSettings.swift index 019f1aeb23..2378099c8f 100644 --- a/CodeEdit/Features/Settings/Pages/TextEditingSettings/Models/TextEditingSettings.swift +++ b/CodeEdit/Features/Settings/Pages/TextEditingSettings/Models/TextEditingSettings.swift @@ -109,7 +109,7 @@ extension SettingsData { name: "Toggle Type-Over Completion", title: "Toggle Type-Over Completion", id: "prefs.text_editing.type_over_completion", - command: CommandClosureWrapper { + command: { Settings.shared.preferences.textEditing.enableTypeOverCompletion.toggle() } ) @@ -118,7 +118,7 @@ extension SettingsData { name: "Toggle Autocomplete Braces", title: "Toggle Autocomplete Braces", id: "prefs.text_editing.autocomplete_braces", - command: CommandClosureWrapper { + command: { Settings.shared.preferences.textEditing.autocompleteBraces.toggle() } ) @@ -127,7 +127,7 @@ extension SettingsData { name: "Toggle Word Wrap", title: "Toggle Word Wrap", id: "prefs.text_editing.wrap_lines_to_editor_width", - command: CommandClosureWrapper { + command: { Settings[\.textEditing].wrapLinesToEditorWidth.toggle() } ) diff --git a/CodeEdit/Features/StatusBar/Views/StatusBarItems/StatusBarToggleUtilityAreaButton.swift b/CodeEdit/Features/StatusBar/Views/StatusBarItems/StatusBarToggleUtilityAreaButton.swift index 16d525d814..1e5c2d15dd 100644 --- a/CodeEdit/Features/StatusBar/Views/StatusBarItems/StatusBarToggleUtilityAreaButton.swift +++ b/CodeEdit/Features/StatusBar/Views/StatusBarItems/StatusBarToggleUtilityAreaButton.swift @@ -29,7 +29,7 @@ internal struct StatusBarToggleUtilityAreaButton: View { name: "Toggle Utility Area", title: "Toggle Utility Area", id: "open.drawer", - command: CommandClosureWrapper.init(closure: utilityAreaViewModel.togglePanel) + command: { [weak utilityAreaViewModel] in utilityAreaViewModel?.togglePanel() } ) } } @@ -38,7 +38,7 @@ internal struct StatusBarToggleUtilityAreaButton: View { name: "Toggle Utility Area", title: "Toggle Utility Area", id: "open.drawer", - command: CommandClosureWrapper.init(closure: utilityAreaViewModel.togglePanel) + command: { [weak utilityAreaViewModel] in utilityAreaViewModel?.togglePanel() } ) } } diff --git a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalView.swift b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalView.swift index 867c8d43fd..63d29d92e8 100644 --- a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalView.swift +++ b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalView.swift @@ -86,29 +86,6 @@ struct UtilityAreaTerminalView: View { return utilityAreaViewModel.terminals.first(where: { $0.id == id }) ?? nil } - private func updateTerminal(_ id: UUID, title: String? = nil) { - let terminalIndex = utilityAreaViewModel.terminals.firstIndex(where: { $0.id == id }) - if terminalIndex != nil { - updateTerminalByReference(of: &utilityAreaViewModel.terminals[terminalIndex!], title: title) - } - } - - func updateTerminalByReference( - of terminal: inout UtilityAreaTerminal, - title: String? = nil - ) { - if let newTitle = title { - if !terminal.customTitle { - terminal.title = newTitle - } - terminal.terminalTitle = newTitle - } - } - - func handleTitleChange(id: UUID, title: String) { - updateTerminal(id, title: title) - } - /// Returns the `background` color of the selected theme private var backgroundColor: NSColor { if let selectedTheme = matchAppearance && darkAppearance @@ -134,10 +111,11 @@ struct UtilityAreaTerminalView: View { TerminalEmulatorView( url: terminal.url!, shellType: terminal.shell, - onTitleChange: { newTitle in + onTitleChange: { [weak terminal] newTitle in + guard let id = terminal?.id else { return } // This can be called whenever, even in a view update so it needs to be dispatched. - DispatchQueue.main.async { - handleTitleChange(id: terminal.id, title: newTitle) + DispatchQueue.main.async { [weak utilityAreaViewModel] in + utilityAreaViewModel?.updateTerminal(id, title: newTitle) } } ) diff --git a/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift b/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift index 671ccf3278..b713ed70e5 100644 --- a/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift +++ b/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift @@ -56,4 +56,21 @@ class UtilityAreaViewModel: ObservableObject { self.isCollapsed.toggle() } } + + /// Update a terminal's title. + /// - Parameters: + /// - id: The id of the terminal to update. + /// - title: The title to set. If left `nil`, will set the terminal's + /// ``UtilityAreaTerminal/customTitle`` to `false`. + func updateTerminal(_ id: UUID, title: String?) { + guard let terminal = terminals.first(where: { $0.id == id }) else { return } + if let newTitle = title { + if !terminal.customTitle { + terminal.title = newTitle + } + terminal.terminalTitle = newTitle + } else { + terminal.customTitle = false + } + } } diff --git a/CodeEdit/Features/WindowCommands/FileCommands.swift b/CodeEdit/Features/WindowCommands/FileCommands.swift index 03b0c199e6..d67514cdd0 100644 --- a/CodeEdit/Features/WindowCommands/FileCommands.swift +++ b/CodeEdit/Features/WindowCommands/FileCommands.swift @@ -44,7 +44,7 @@ struct FileCommands: Commands { if NSApp.target(forAction: #selector(CodeEditWindowController.closeCurrentTab(_:))) != nil { NSApp.sendAction(#selector(CodeEditWindowController.closeCurrentTab(_:)), to: nil, from: nil) } else { - NSApp.sendAction(#selector(NSWindow.close), to: nil, from: nil) + NSApp.sendAction(#selector(NSWindow.performClose(_:)), to: keyWindow, from: nil) } } .keyboardShortcut("w") @@ -57,19 +57,19 @@ struct FileCommands: Commands { from: nil ) } else { - NSApp.sendAction(#selector(NSWindow.close), to: nil, from: nil) + NSApp.sendAction(#selector(NSWindow.performClose(_:)), to: keyWindow, from: nil) } } .keyboardShortcut("w", modifiers: [.control, .shift, .command]) Button("Close Window") { - NSApp.sendAction(#selector(NSWindow.close), to: nil, from: nil) + NSApp.sendAction(#selector(NSWindow.performClose(_:)), to: keyWindow, from: nil) } .keyboardShortcut("w", modifiers: [.shift, .command]) Button("Close Workspace") { guard let keyWindow = NSApplication.shared.keyWindow else { return } - NSApp.sendAction(#selector(NSWindow.close), to: keyWindow, from: nil) + NSApp.sendAction(#selector(NSWindow.performClose(_:)), to: keyWindow, from: nil) } .keyboardShortcut("w", modifiers: [.control, .option, .command]) .disabled(!(NSApplication.shared.keyWindow?.windowController is CodeEditWindowController)) diff --git a/CodeEdit/Features/WindowCommands/ViewCommands.swift b/CodeEdit/Features/WindowCommands/ViewCommands.swift index 1326a93dcf..de989196a6 100644 --- a/CodeEdit/Features/WindowCommands/ViewCommands.swift +++ b/CodeEdit/Features/WindowCommands/ViewCommands.swift @@ -17,9 +17,9 @@ struct ViewCommands: Commands { @AppSettings(\.general.dimEditorsWithoutFocus) var dimEditorsWithoutFocus - @State private var windowController: CodeEditWindowController? +// @State private var windowController: CodeEditWindowController? - private let documentController: CodeEditDocumentController = CodeEditDocumentController() +// private let documentController: CodeEditDocumentController = CodeEditDocumentController() @FocusedBinding(\.navigationSplitViewVisibility) var navigationSplitViewVisibility @@ -34,36 +34,36 @@ struct ViewCommands: Commands { } .keyboardShortcut("p", modifiers: [.shift, .command]) - Menu("Font Size") { - Button("Increase") { - if editorFontSize < 288 { - editorFontSize += 1 - } - if terminalFontSize < 288 { - terminalFontSize += 1 - } - } - .keyboardShortcut("+") - - Button("Decrease") { - if editorFontSize > 1 { - editorFontSize -= 1 - } - if terminalFontSize > 1 { - terminalFontSize -= 1 - } - } - .keyboardShortcut("-") - - Divider() - - Button("Reset") { - editorFontSize = 12 - terminalFontSize = 12 - } - .keyboardShortcut("0", modifiers: [.command, .control]) - } - .disabled(windowController == nil) +// Menu("Font Size") { +// Button("Increase") { +// if editorFontSize < 288 { +// editorFontSize += 1 +// } +// if terminalFontSize < 288 { +// terminalFontSize += 1 +// } +// } +// .keyboardShortcut("+") +// +// Button("Decrease") { +// if editorFontSize > 1 { +// editorFontSize -= 1 +// } +// if terminalFontSize > 1 { +// terminalFontSize -= 1 +// } +// } +// .keyboardShortcut("-") +// +// Divider() +// +// Button("Reset") { +// editorFontSize = 12 +// terminalFontSize = 12 +// } +// .keyboardShortcut("0", modifiers: [.command, .control]) +// } +// .disabled(windowController == nil) Button("Customize Toolbar...") { @@ -72,17 +72,17 @@ struct ViewCommands: Commands { Divider() - HideCommands( - windowController: windowController ?? CodeEditWindowController( - window: nil, - workspace: nil, - taskNotificationHandler: TaskNotificationHandler() - ), - utilityAreaModel: windowController?.workspace?.utilityAreaModel ?? UtilityAreaViewModel() - ) - .onReceive(NSApp.publisher(for: \.keyWindow)) { window in - windowController = window?.windowController as? CodeEditWindowController - } +// HideCommands( +// windowController: windowController ?? CodeEditWindowController( +// window: nil, +// workspace: nil, +// taskNotificationHandler: TaskNotificationHandler() +// ), +// utilityAreaModel: windowController?.workspace?.utilityAreaModel ?? UtilityAreaViewModel() +// ) +// .onReceive(NSApp.publisher(for: \.keyWindow)) { window in +// windowController = window?.windowController as? CodeEditWindowController +// } Divider() @@ -94,60 +94,60 @@ struct ViewCommands: Commands { Divider() - if let model = windowController?.navigatorSidebarViewModel { - Divider() - NavigatorCommands(model: model) - } +// if let model = windowController?.navigatorSidebarViewModel { +// Divider() +// NavigatorCommands(model: model) +// } } } } -struct HideCommands: View { - @ObservedObject var windowController: CodeEditWindowController - @ObservedObject var utilityAreaModel: UtilityAreaViewModel - - var navigatorCollapsed: Bool { - windowController.navigatorCollapsed - } - - var inspectorCollapsed: Bool { - windowController.inspectorCollapsed - } - - var utilityAreaCollapsed: Bool { - utilityAreaModel.isCollapsed - } - - var toolbarCollapsed: Bool { - windowController.toolbarCollapsed - } - - var body: some View { - Button("\(navigatorCollapsed ? "Show" : "Hide") Navigator") { - windowController.toggleFirstPanel() - } - .disabled(windowController.window == nil) - .keyboardShortcut("0", modifiers: [.command]) - - Button("\(inspectorCollapsed ? "Show" : "Hide") Inspector") { - windowController.toggleLastPanel() - } - .disabled(windowController.window == nil) - .keyboardShortcut("i", modifiers: [.control, .command]) - - Button("\(utilityAreaCollapsed ? "Show" : "Hide") Utility Area") { - CommandManager.shared.executeCommand("open.drawer") - } - .disabled(windowController.window == nil) - .keyboardShortcut("y", modifiers: [.shift, .command]) - - Button("\(toolbarCollapsed ? "Show" : "Hide") Toolbar") { - windowController.toggleToolbar() - } - .disabled(windowController.window == nil) - .keyboardShortcut("t", modifiers: [.option, .command]) - } -} +//struct HideCommands: View { +// @ObservedObject var windowController: CodeEditWindowController +// @ObservedObject var utilityAreaModel: UtilityAreaViewModel +// +// var navigatorCollapsed: Bool { +// windowController.navigatorCollapsed +// } +// +// var inspectorCollapsed: Bool { +// windowController.inspectorCollapsed +// } +// +// var utilityAreaCollapsed: Bool { +// utilityAreaModel.isCollapsed +// } +// +// var toolbarCollapsed: Bool { +// windowController.toolbarCollapsed +// } +// +// var body: some View { +// Button("\(navigatorCollapsed ? "Show" : "Hide") Navigator") { +// windowController.toggleFirstPanel() +// } +// .disabled(windowController.window == nil) +// .keyboardShortcut("0", modifiers: [.command]) +// +// Button("\(inspectorCollapsed ? "Show" : "Hide") Inspector") { +// windowController.toggleLastPanel() +// } +// .disabled(windowController.window == nil) +// .keyboardShortcut("i", modifiers: [.control, .command]) +// +// Button("\(utilityAreaCollapsed ? "Show" : "Hide") Utility Area") { +// CommandManager.shared.executeCommand("open.drawer") +// } +// .disabled(windowController.window == nil) +// .keyboardShortcut("y", modifiers: [.shift, .command]) +// +// Button("\(toolbarCollapsed ? "Show" : "Hide") Toolbar") { +// windowController.toggleToolbar() +// } +// .disabled(windowController.window == nil) +// .keyboardShortcut("t", modifiers: [.option, .command]) +// } +//} extension ViewCommands { struct NavigatorCommands: View { diff --git a/CodeEdit/Utils/Environment/Env+Window.swift b/CodeEdit/Utils/Environment/Env+Window.swift index 5a18ba4d45..15b0c414c6 100644 --- a/CodeEdit/Utils/Environment/Env+Window.swift +++ b/CodeEdit/Utils/Environment/Env+Window.swift @@ -7,12 +7,17 @@ import SwiftUI +struct WindowBox { + weak var value: NSWindow? +} + struct NSWindowEnvironmentKey: EnvironmentKey { - static var defaultValue = NSWindow() + typealias Value = WindowBox + static var defaultValue = WindowBox(value: nil) } extension EnvironmentValues { - var window: NSWindowEnvironmentKey.Value { + var window: WindowBox { get { self[NSWindowEnvironmentKey.self] } set { self[NSWindowEnvironmentKey.self] = newValue } } diff --git a/CodeEdit/Features/Documents/String+AppearancesOfSubstring.swift b/CodeEdit/Utils/Extensions/String/String+AppearancesOfSubstring.swift similarity index 100% rename from CodeEdit/Features/Documents/String+AppearancesOfSubstring.swift rename to CodeEdit/Utils/Extensions/String/String+AppearancesOfSubstring.swift diff --git a/CodeEdit/Features/Documents/String+Character.swift b/CodeEdit/Utils/Extensions/String/String+Character.swift similarity index 100% rename from CodeEdit/Features/Documents/String+Character.swift rename to CodeEdit/Utils/Extensions/String/String+Character.swift diff --git a/CodeEdit/WindowObserver.swift b/CodeEdit/WindowObserver.swift index 17978b9435..530505f4c9 100644 --- a/CodeEdit/WindowObserver.swift +++ b/CodeEdit/WindowObserver.swift @@ -9,7 +9,7 @@ import SwiftUI struct WindowObserver: View { - var window: NSWindow + var window: WindowBox @ViewBuilder var content: Content diff --git a/CodeEdit/WorkspaceView.swift b/CodeEdit/WorkspaceView.swift index 8c4dcb35db..46f96b584a 100644 --- a/CodeEdit/WorkspaceView.swift +++ b/CodeEdit/WorkspaceView.swift @@ -8,8 +8,8 @@ import SwiftUI struct WorkspaceView: View { - @Environment(\.window) - private var window: NSWindow + @Environment(\.window.value) + private var window: NSWindow? @Environment(\.colorScheme) private var colorScheme From c9c31132b568742bee361d5ec15dee2d4a4039be Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Sun, 14 Jul 2024 10:43:11 -0500 Subject: [PATCH 2/9] Fix Hide Commands, Bump CESE --- CodeEdit.xcodeproj/project.pbxproj | 6 +- .../xcshareddata/swiftpm/Package.resolved | 6 +- .../CodeFileDocument.swift | 0 .../WindowControllerPropertyWrapper.swift | 66 +++++++ .../WindowCommands/ViewCommands.swift | 180 +++++++++--------- 5 files changed, 159 insertions(+), 99 deletions(-) rename CodeEdit/Features/Documents/{ => CodeFileDocument}/CodeFileDocument.swift (100%) create mode 100644 CodeEdit/Features/WindowCommands/Utils/WindowControllerPropertyWrapper.swift diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index 44374e70e4..42a52c7478 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -420,6 +420,7 @@ 6CC17B512C43311900834E2C /* ProjectNavigatorViewController+NSOutlineViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CC17B502C43311900834E2C /* ProjectNavigatorViewController+NSOutlineViewDataSource.swift */; }; 6CC17B532C43314000834E2C /* ProjectNavigatorViewController+NSOutlineViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CC17B522C43314000834E2C /* ProjectNavigatorViewController+NSOutlineViewDelegate.swift */; }; 6CC17B592C43F53700834E2C /* CodeEditSourceEditor in Frameworks */ = {isa = PBXBuildFile; productRef = 6CC17B582C43F53700834E2C /* CodeEditSourceEditor */; }; + 6CC17B5B2C44258700834E2C /* WindowControllerPropertyWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CC17B5A2C44258700834E2C /* WindowControllerPropertyWrapper.swift */; }; 6CC9E4B229B5669900C97388 /* Environment+ActiveEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CC9E4B129B5669900C97388 /* Environment+ActiveEditor.swift */; }; 6CD03B6A29FC773F001BD1D0 /* SettingsInjector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CD03B6929FC773F001BD1D0 /* SettingsInjector.swift */; }; 6CDA84AD284C1BA000C1CC3A /* EditorTabBarContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CDA84AC284C1BA000C1CC3A /* EditorTabBarContextMenu.swift */; }; @@ -1025,6 +1026,7 @@ 6CBE1CFA2B71DAA6003AC32E /* Loopable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Loopable.swift; sourceTree = ""; }; 6CC17B502C43311900834E2C /* ProjectNavigatorViewController+NSOutlineViewDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProjectNavigatorViewController+NSOutlineViewDataSource.swift"; sourceTree = ""; }; 6CC17B522C43314000834E2C /* ProjectNavigatorViewController+NSOutlineViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProjectNavigatorViewController+NSOutlineViewDelegate.swift"; sourceTree = ""; }; + 6CC17B5A2C44258700834E2C /* WindowControllerPropertyWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowControllerPropertyWrapper.swift; sourceTree = ""; }; 6CC9E4B129B5669900C97388 /* Environment+ActiveEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Environment+ActiveEditor.swift"; sourceTree = ""; }; 6CD03B6929FC773F001BD1D0 /* SettingsInjector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsInjector.swift; sourceTree = ""; }; 6CDA84AC284C1BA000C1CC3A /* EditorTabBarContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditorTabBarContextMenu.swift; sourceTree = ""; }; @@ -2693,6 +2695,7 @@ children = ( B66A4E4429C8E86D004573B4 /* CommandsFixes.swift */, 6C82D6BB29C00CD900495C54 /* FirstResponderPropertyWrapper.swift */, + 6CC17B5A2C44258700834E2C /* WindowControllerPropertyWrapper.swift */, ); path = Utils; sourceTree = ""; @@ -3656,6 +3659,7 @@ 6CED16E42A3E660D000EC962 /* String+Lines.swift in Sources */, 587B9E6B29301D8F00AC7927 /* GitLabAvatarURL.swift in Sources */, 58798233292E30B90085B254 /* FeedbackToolbar.swift in Sources */, + 6CC17B5B2C44258700834E2C /* WindowControllerPropertyWrapper.swift in Sources */, 587B9E6829301D8F00AC7927 /* GitLabAccountModel.swift in Sources */, 5878DAA7291AE76700DD95A3 /* OpenQuicklyViewModel.swift in Sources */, 6CFF967429BEBCC300182D6F /* FindCommands.swift in Sources */, @@ -5275,7 +5279,7 @@ repositoryURL = "https://github.com/CodeEditApp/CodeEditSourceEditor"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 0.7.3; + minimumVersion = 0.7.4; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index c6458df54c..6bc620d0a8 100644 --- a/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "59bb530acdf3bfbb14acc0701532467038359ce8bdcd8935b8d88825a447556c", + "originHash" : "a33fcca819dee4c816b1474e19017510b1d62b170c921187042e0675d3f4b0b3", "pins" : [ { "identity" : "anycodable", @@ -33,8 +33,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/CodeEditApp/CodeEditSourceEditor", "state" : { - "revision" : "cf85789d527d569e94edfd674c5ac8071b244dd9", - "version" : "0.7.3" + "revision" : "4e014f71d7be053ea8d05f6c0e45be268f9a0d64", + "version" : "0.7.4" } }, { diff --git a/CodeEdit/Features/Documents/CodeFileDocument.swift b/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift similarity index 100% rename from CodeEdit/Features/Documents/CodeFileDocument.swift rename to CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift diff --git a/CodeEdit/Features/WindowCommands/Utils/WindowControllerPropertyWrapper.swift b/CodeEdit/Features/WindowCommands/Utils/WindowControllerPropertyWrapper.swift new file mode 100644 index 0000000000..f5130a0340 --- /dev/null +++ b/CodeEdit/Features/WindowCommands/Utils/WindowControllerPropertyWrapper.swift @@ -0,0 +1,66 @@ +// +// WindowControllerPropertyWrapper.swift +// CodeEdit +// +// Created by Khan Winter on 7/14/24. +// + +import AppKit +import SwiftUI +import Combine + +/// Provides an auto-updating reference to ``CodeEditWindowController``. The value will update as the key window +/// changes, and does not keep a strong reference to the controller. +/// +/// Sample usage: +/// ```swift +/// struct WindowCommands: Commands { +/// @UpdatingWindowController var windowController +/// +/// var body: some Commands { +/// Button("Button that needs the window") { +/// print("Window exists") +/// } +/// .disabled(windowController == nil) +/// } +/// } +/// ``` +@propertyWrapper +struct UpdatingWindowController: DynamicProperty { + @StateObject var box = WindowControllerBox() + + var wrappedValue: CodeEditWindowController? { + box.controller + } + + class WindowControllerBox: ObservableObject { + public private(set) weak var controller: CodeEditWindowController? + + private var objectWillChangeCancellable: AnyCancellable? + private var utilityAreaCancellable: AnyCancellable? // ``ViewCommands`` needs this. + private var windowCancellable: AnyCancellable? + + init() { + windowCancellable = NSApp.publisher(for: \.keyWindow).sink { [weak self] window in + self?.setNewController(window?.windowController as? CodeEditWindowController) + } + } + + func setNewController(_ controller: CodeEditWindowController?) { + objectWillChangeCancellable?.cancel() + objectWillChangeCancellable = nil + utilityAreaCancellable?.cancel() + utilityAreaCancellable = nil + + self.controller = controller + + objectWillChangeCancellable = controller?.objectWillChange.sink { [weak self] in + self?.objectWillChange.send() + } + utilityAreaCancellable = controller?.workspace?.utilityAreaModel?.objectWillChange.sink { [weak self] in + self?.objectWillChange.send() + } + self.objectWillChange.send() + } + } +} diff --git a/CodeEdit/Features/WindowCommands/ViewCommands.swift b/CodeEdit/Features/WindowCommands/ViewCommands.swift index de989196a6..c11aff90d8 100644 --- a/CodeEdit/Features/WindowCommands/ViewCommands.swift +++ b/CodeEdit/Features/WindowCommands/ViewCommands.swift @@ -6,6 +6,7 @@ // import SwiftUI +import Combine struct ViewCommands: Commands { @AppSettings(\.textEditing.font.size) @@ -17,16 +18,14 @@ struct ViewCommands: Commands { @AppSettings(\.general.dimEditorsWithoutFocus) var dimEditorsWithoutFocus -// @State private var windowController: CodeEditWindowController? - -// private let documentController: CodeEditDocumentController = CodeEditDocumentController() - @FocusedBinding(\.navigationSplitViewVisibility) var navigationSplitViewVisibility @FocusedBinding(\.inspectorVisibility) var inspectorVisibility + @UpdatingWindowController var windowController: CodeEditWindowController? + var body: some Commands { CommandGroup(after: .toolbar) { Button("Show Command Palette") { @@ -34,36 +33,36 @@ struct ViewCommands: Commands { } .keyboardShortcut("p", modifiers: [.shift, .command]) -// Menu("Font Size") { -// Button("Increase") { -// if editorFontSize < 288 { -// editorFontSize += 1 -// } -// if terminalFontSize < 288 { -// terminalFontSize += 1 -// } -// } -// .keyboardShortcut("+") -// -// Button("Decrease") { -// if editorFontSize > 1 { -// editorFontSize -= 1 -// } -// if terminalFontSize > 1 { -// terminalFontSize -= 1 -// } -// } -// .keyboardShortcut("-") -// -// Divider() -// -// Button("Reset") { -// editorFontSize = 12 -// terminalFontSize = 12 -// } -// .keyboardShortcut("0", modifiers: [.command, .control]) -// } -// .disabled(windowController == nil) + Menu("Font Size") { + Button("Increase") { + if editorFontSize < 288 { + editorFontSize += 1 + } + if terminalFontSize < 288 { + terminalFontSize += 1 + } + } + .keyboardShortcut("+") + + Button("Decrease") { + if editorFontSize > 1 { + editorFontSize -= 1 + } + if terminalFontSize > 1 { + terminalFontSize -= 1 + } + } + .keyboardShortcut("-") + + Divider() + + Button("Reset") { + editorFontSize = 12 + terminalFontSize = 12 + } + .keyboardShortcut("0", modifiers: [.command, .control]) + } + .disabled(windowController == nil) Button("Customize Toolbar...") { @@ -72,17 +71,7 @@ struct ViewCommands: Commands { Divider() -// HideCommands( -// windowController: windowController ?? CodeEditWindowController( -// window: nil, -// workspace: nil, -// taskNotificationHandler: TaskNotificationHandler() -// ), -// utilityAreaModel: windowController?.workspace?.utilityAreaModel ?? UtilityAreaViewModel() -// ) -// .onReceive(NSApp.publisher(for: \.keyWindow)) { window in -// windowController = window?.windowController as? CodeEditWindowController -// } + HideCommands() Divider() @@ -94,60 +83,61 @@ struct ViewCommands: Commands { Divider() -// if let model = windowController?.navigatorSidebarViewModel { -// Divider() -// NavigatorCommands(model: model) -// } + if let model = windowController?.navigatorSidebarViewModel { + Divider() + NavigatorCommands(model: model) + } } } } -//struct HideCommands: View { -// @ObservedObject var windowController: CodeEditWindowController -// @ObservedObject var utilityAreaModel: UtilityAreaViewModel -// -// var navigatorCollapsed: Bool { -// windowController.navigatorCollapsed -// } -// -// var inspectorCollapsed: Bool { -// windowController.inspectorCollapsed -// } -// -// var utilityAreaCollapsed: Bool { -// utilityAreaModel.isCollapsed -// } -// -// var toolbarCollapsed: Bool { -// windowController.toolbarCollapsed -// } -// -// var body: some View { -// Button("\(navigatorCollapsed ? "Show" : "Hide") Navigator") { -// windowController.toggleFirstPanel() -// } -// .disabled(windowController.window == nil) -// .keyboardShortcut("0", modifiers: [.command]) -// -// Button("\(inspectorCollapsed ? "Show" : "Hide") Inspector") { -// windowController.toggleLastPanel() -// } -// .disabled(windowController.window == nil) -// .keyboardShortcut("i", modifiers: [.control, .command]) -// -// Button("\(utilityAreaCollapsed ? "Show" : "Hide") Utility Area") { -// CommandManager.shared.executeCommand("open.drawer") -// } -// .disabled(windowController.window == nil) -// .keyboardShortcut("y", modifiers: [.shift, .command]) -// -// Button("\(toolbarCollapsed ? "Show" : "Hide") Toolbar") { -// windowController.toggleToolbar() -// } -// .disabled(windowController.window == nil) -// .keyboardShortcut("t", modifiers: [.option, .command]) -// } -//} +extension ViewCommands { + struct HideCommands: View { + @UpdatingWindowController var windowController: CodeEditWindowController? + + var navigatorCollapsed: Bool { + windowController?.navigatorCollapsed ?? true + } + + var inspectorCollapsed: Bool { + windowController?.inspectorCollapsed ?? true + } + + var utilityAreaCollapsed: Bool { + windowController?.workspace?.utilityAreaModel?.isCollapsed ?? true + } + + var toolbarCollapsed: Bool { + windowController?.toolbarCollapsed ?? true + } + + var body: some View { + Button("\(navigatorCollapsed ? "Show" : "Hide") Navigator") { + windowController?.toggleFirstPanel() + } + .disabled(windowController == nil) + .keyboardShortcut("0", modifiers: [.command]) + + Button("\(inspectorCollapsed ? "Show" : "Hide") Inspector") { + windowController?.toggleLastPanel() + } + .disabled(windowController == nil) + .keyboardShortcut("i", modifiers: [.control, .command]) + + Button("\(utilityAreaCollapsed ? "Show" : "Hide") Utility Area") { + CommandManager.shared.executeCommand("open.drawer") + } + .disabled(windowController == nil) + .keyboardShortcut("y", modifiers: [.shift, .command]) + + Button("\(toolbarCollapsed ? "Show" : "Hide") Toolbar") { + windowController?.toggleToolbar() + } + .disabled(windowController == nil) + .keyboardShortcut("t", modifiers: [.option, .command]) + } + } +} extension ViewCommands { struct NavigatorCommands: View { From 5ee0da73b55663280e5b150cfaa4f35c1e984fff Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Sun, 14 Jul 2024 10:56:21 -0500 Subject: [PATCH 3/9] Fix Tests With Weak Variables --- .../Features/Documents/DocumentsUnitTests.swift | 3 ++- ...spaceDocument+SearchState+FindAndReplaceTests.swift | 10 ++++++++-- .../WorkspaceDocument+SearchState+IndexTests.swift | 9 +++++++-- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/CodeEditTests/Features/Documents/DocumentsUnitTests.swift b/CodeEditTests/Features/Documents/DocumentsUnitTests.swift index c116d7a191..5cdefd143c 100644 --- a/CodeEditTests/Features/Documents/DocumentsUnitTests.swift +++ b/CodeEditTests/Features/Documents/DocumentsUnitTests.swift @@ -14,6 +14,7 @@ final class DocumentsUnitTests: XCTestCase { private var hapticFeedbackPerformerMock: NSHapticFeedbackPerformerMock! private var navigatorViewModel: NavigatorSidebarViewModel! private var window: NSWindow! + private var workspace = WorkspaceDocument() // MARK: - Lifecycle @@ -23,7 +24,7 @@ final class DocumentsUnitTests: XCTestCase { navigatorViewModel = .init() window = NSWindow() splitViewController = .init( - workspace: WorkspaceDocument(), + workspace: workspace, navigatorViewModel: navigatorViewModel, windowRef: window, hapticPerformer: hapticFeedbackPerformerMock diff --git a/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindAndReplaceTests.swift b/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindAndReplaceTests.swift index 9e92c5a5a7..d358b09913 100644 --- a/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindAndReplaceTests.swift +++ b/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindAndReplaceTests.swift @@ -8,12 +8,16 @@ import XCTest @testable import CodeEdit +// swiftlint:disable:next type_body_length final class FindAndReplaceTests: XCTestCase { private var directory: URL! private var files: [CEWorkspaceFile] = [] private var mockWorkspace: WorkspaceDocument! private var searchState: WorkspaceDocument.SearchState! + private var folder1File: CEWorkspaceFile? + private var folder2File: CEWorkspaceFile? + // MARK: - Setup /// A mock WorkspaceDocument is created /// 3 mock files are added to the index @@ -35,7 +39,9 @@ final class FindAndReplaceTests: XCTestCase { // Add a few files let folder1 = directory.appending(path: "Folder 2") + folder1File = CEWorkspaceFile(url: folder1) let folder2 = directory.appending(path: "Longer Folder With Some 💯 Special Chars ⁉️") + folder2File = CEWorkspaceFile(url: folder2) try FileManager.default.createDirectory(at: folder1, withIntermediateDirectories: true) try FileManager.default.createDirectory(at: folder2, withIntermediateDirectories: true) @@ -55,8 +61,8 @@ final class FindAndReplaceTests: XCTestCase { files = fileURLs.map { CEWorkspaceFile(url: $0) } - files[1].parent = CEWorkspaceFile(url: folder1) - files[2].parent = CEWorkspaceFile(url: folder2) + files[1].parent = folder1File + files[2].parent = folder2File await mockWorkspace.searchState?.addProjectToIndex() diff --git a/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+IndexTests.swift b/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+IndexTests.swift index edc308e931..a0f1611339 100644 --- a/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+IndexTests.swift +++ b/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+IndexTests.swift @@ -14,6 +14,9 @@ final class WorkspaceDocumentIndexTests: XCTestCase { private var mockWorkspace: WorkspaceDocument! private var searchState: WorkspaceDocument.SearchState! + private var folder1File: CEWorkspaceFile? + private var folder2File: CEWorkspaceFile? + // MARK: - Setup /// A mock WorkspaceDocument is created /// 3 mock files are added to the index @@ -35,7 +38,9 @@ final class WorkspaceDocumentIndexTests: XCTestCase { // Add a few files let folder1 = directory.appending(path: "Folder 2") + folder1File = CEWorkspaceFile(url: folder1) let folder2 = directory.appending(path: "Longer Folder With Some 💯 Special Chars ⁉️") + folder2File = CEWorkspaceFile(url: folder2) try FileManager.default.createDirectory(at: folder1, withIntermediateDirectories: true) try FileManager.default.createDirectory(at: folder2, withIntermediateDirectories: true) @@ -55,8 +60,8 @@ final class WorkspaceDocumentIndexTests: XCTestCase { files = fileURLs.map { CEWorkspaceFile(url: $0) } - files[1].parent = CEWorkspaceFile(url: folder1) - files[2].parent = CEWorkspaceFile(url: folder2) + files[1].parent = folder1File + files[2].parent = folder2File await mockWorkspace.searchState?.addProjectToIndex() From 1786c9fdce66b9629df1a3db062c82212c485552 Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Sun, 14 Jul 2024 11:04:49 -0500 Subject: [PATCH 4/9] Update Cell Use New `textField` Variable --- .../OutlineView/ProjectNavigatorTableViewCell.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorTableViewCell.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorTableViewCell.swift index fe16e1cd90..bd64211705 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorTableViewCell.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorTableViewCell.swift @@ -29,7 +29,7 @@ final class ProjectNavigatorTableViewCell: FileSystemTableViewCell { delegate: OutlineTableViewCellDelegate? = nil ) { super.init(frame: frameRect, item: item, isEditable: isEditable) - self.label.setAccessibilityIdentifier("ProjectNavigatorTableViewCell-\(item?.name ?? "")") + self.textField?.setAccessibilityIdentifier("ProjectNavigatorTableViewCell-\(item?.name ?? "")") self.delegate = delegate } From 9b2216ce4c67c30d08939afb13672adc9d484af5 Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Sun, 14 Jul 2024 13:48:39 -0500 Subject: [PATCH 5/9] Remove Final Cycle, Remove Force Unwraps --- .../xcshareddata/xcschemes/CodeEdit.xcscheme | 12 ++++++++++++ .../CodeEditSplitViewController.swift | 18 ++++++++++++------ .../Features/WindowCommands/FileCommands.swift | 5 +---- 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/CodeEdit.xcodeproj/xcshareddata/xcschemes/CodeEdit.xcscheme b/CodeEdit.xcodeproj/xcshareddata/xcschemes/CodeEdit.xcscheme index 0fe6885358..36eff77016 100644 --- a/CodeEdit.xcodeproj/xcshareddata/xcschemes/CodeEdit.xcscheme +++ b/CodeEdit.xcodeproj/xcshareddata/xcschemes/CodeEdit.xcscheme @@ -138,6 +138,18 @@ ReferencedContainer = "container:CodeEdit.xcodeproj"> + + + + + + Date: Sun, 14 Jul 2024 13:48:58 -0500 Subject: [PATCH 6/9] Turn Off Allocation Logging --- .../xcshareddata/xcschemes/CodeEdit.xcscheme | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/CodeEdit.xcodeproj/xcshareddata/xcschemes/CodeEdit.xcscheme b/CodeEdit.xcodeproj/xcshareddata/xcschemes/CodeEdit.xcscheme index 36eff77016..0fe6885358 100644 --- a/CodeEdit.xcodeproj/xcshareddata/xcschemes/CodeEdit.xcscheme +++ b/CodeEdit.xcodeproj/xcshareddata/xcschemes/CodeEdit.xcscheme @@ -138,18 +138,6 @@ ReferencedContainer = "container:CodeEdit.xcodeproj"> - - - - - - Date: Sun, 14 Jul 2024 16:00:53 -0500 Subject: [PATCH 7/9] Finish Merge From Main --- ...ViewController+NSOutlineViewDelegate.swift | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift index 37215b3d71..1a76f99755 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift @@ -36,8 +36,10 @@ extension ProjectNavigatorViewController: NSOutlineViewDelegate { guard let item = outlineView.item(atRow: selectedIndex) as? CEWorkspaceFile else { return } if !item.isFolder && shouldSendSelectionUpdate { - DispatchQueue.main.async { - self.workspace?.editorManager?.activeEditor.openTab(file: item, asTemporary: true) + DispatchQueue.main.async { [weak self] in + self?.shouldSendSelectionUpdate = false + self?.workspace?.editorManager.activeEditor.openTab(file: item, asTemporary: true) + self?.shouldSendSelectionUpdate = true } } } @@ -47,12 +49,12 @@ extension ProjectNavigatorViewController: NSOutlineViewDelegate { } func outlineViewItemDidExpand(_ notification: Notification) { - guard - let id = workspace?.editorManager?.activeEditor.selectedTab?.file.id, - let item = workspace?.workspaceFileManager?.getFile(id, createIfNotFound: true) - else { return } - /// update outline selection only if the parent of selected item match with expanded item - guard item.parent === notification.userInfo?["NSObject"] as? CEWorkspaceFile else { return } + guard let id = workspace?.editorManager.activeEditor.selectedTab?.file.id, + let item = workspace?.workspaceFileManager?.getFile(id, createIfNotFound: true), + /// update outline selection only if the parent of selected item match with expanded item + item.parent === notification.userInfo?["NSObject"] as? CEWorkspaceFile else { + return + } /// select active file under collapsed folder only if its parent is expanding if outlineView.isItemExpanded(item.parent) { updateSelection(itemID: item.id) @@ -104,7 +106,9 @@ extension ProjectNavigatorViewController: NSOutlineViewDelegate { expandParent(item: parent) } let row = outlineView.row(forItem: fileItem) + shouldSendSelectionUpdate = false outlineView.selectRowIndexes(.init(integer: row), byExtendingSelection: false) + shouldSendSelectionUpdate = true if row < 0 { let alert = NSAlert() From 877c1a8a729d81fa4772038d42e9015ad8c2f722 Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Sun, 14 Jul 2024 16:13:10 -0500 Subject: [PATCH 8/9] Fix Build Error --- ...ProjectNavigatorViewController+NSOutlineViewDelegate.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift index 1a76f99755..52c8eeafcc 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift @@ -38,7 +38,7 @@ extension ProjectNavigatorViewController: NSOutlineViewDelegate { if !item.isFolder && shouldSendSelectionUpdate { DispatchQueue.main.async { [weak self] in self?.shouldSendSelectionUpdate = false - self?.workspace?.editorManager.activeEditor.openTab(file: item, asTemporary: true) + self?.workspace?.editorManager?.activeEditor.openTab(file: item, asTemporary: true) self?.shouldSendSelectionUpdate = true } } @@ -49,7 +49,7 @@ extension ProjectNavigatorViewController: NSOutlineViewDelegate { } func outlineViewItemDidExpand(_ notification: Notification) { - guard let id = workspace?.editorManager.activeEditor.selectedTab?.file.id, + guard let id = workspace?.editorManager?.activeEditor.selectedTab?.file.id, let item = workspace?.workspaceFileManager?.getFile(id, createIfNotFound: true), /// update outline selection only if the parent of selected item match with expanded item item.parent === notification.userInfo?["NSObject"] as? CEWorkspaceFile else { From 4caf1259e8ba18a8840be85352434308f7807c32 Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Wed, 17 Jul 2024 10:59:07 -0500 Subject: [PATCH 9/9] Finish Merge From Main, Address Comments --- CodeEdit.xcodeproj/project.pbxproj | 14 +- .../FindNavigatorListViewController.swift | 12 -- ...ctNavigatorViewController+DataSource.swift | 119 -------------- ...jectNavigatorViewController+Delegate.swift | 148 ------------------ ...ewController+NSOutlineViewDataSource.swift | 6 +- ...ViewController+NSOutlineViewDelegate.swift | 4 +- 6 files changed, 15 insertions(+), 288 deletions(-) delete mode 100644 CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+DataSource.swift delete mode 100644 CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+Delegate.swift diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index 9426f513cd..7243a08813 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -383,8 +383,6 @@ 6C5FDF7A29E6160000BC08C0 /* AppSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C5FDF7929E6160000BC08C0 /* AppSettings.swift */; }; 6C6362D42C3E321A0025570D /* Editor+History.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C6362D32C3E321A0025570D /* Editor+History.swift */; }; 6C66C31329D05CDC00DE9ED2 /* GRDB in Frameworks */ = {isa = PBXBuildFile; productRef = 6C66C31229D05CDC00DE9ED2 /* GRDB */; }; - 6C67413E2C44A28C00AABDF5 /* ProjectNavigatorViewController+DataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C67413D2C44A28C00AABDF5 /* ProjectNavigatorViewController+DataSource.swift */; }; - 6C6741402C44A2A200AABDF5 /* ProjectNavigatorViewController+Delegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C67413F2C44A2A200AABDF5 /* ProjectNavigatorViewController+Delegate.swift */; }; 6C6BD6EF29CD12E900235D17 /* ExtensionManagerWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C6BD6EE29CD12E900235D17 /* ExtensionManagerWindow.swift */; }; 6C6BD6F129CD13FA00235D17 /* ExtensionDiscovery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C6BD6F029CD13FA00235D17 /* ExtensionDiscovery.swift */; }; 6C6BD6F429CD142C00235D17 /* CollectionConcurrencyKit in Frameworks */ = {isa = PBXBuildFile; productRef = 6C6BD6F329CD142C00235D17 /* CollectionConcurrencyKit */; }; @@ -2600,6 +2598,15 @@ path = Models; sourceTree = ""; }; + 6C01F25D2C4820B600AA951B /* Recovered References */ = { + isa = PBXGroup; + children = ( + 6C67413D2C44A28C00AABDF5 /* ProjectNavigatorViewController+DataSource.swift */, + 6C67413F2C44A2A200AABDF5 /* ProjectNavigatorViewController+Delegate.swift */, + ); + name = "Recovered References"; + sourceTree = ""; + }; 6C092EDC2A53A63E00489202 /* Views */ = { isa = PBXGroup; children = ( @@ -2947,6 +2954,7 @@ 283BDCBC2972EEBD002AFF81 /* Package.resolved */, B658FB2D27DA9E0F00EA4DBD /* Products */, 5C403B8D27E20F8000788241 /* Frameworks */, + 6C01F25D2C4820B600AA951B /* Recovered References */, ); indentWidth = 4; sourceTree = ""; @@ -3739,7 +3747,6 @@ 58798237292E30B90085B254 /* FeedbackView.swift in Sources */, 587B9E9829301D8F00AC7927 /* GitCommit.swift in Sources */, 6C5228B529A868BD00AC48F6 /* Environment+ContentInsets.swift in Sources */, - 6C67413E2C44A28C00AABDF5 /* ProjectNavigatorViewController+DataSource.swift in Sources */, 587B9E9429301D8F00AC7927 /* BitBucketTokenConfiguration.swift in Sources */, 04BA7C222AE2D95E00584E1C /* GitClient+CommitHistory.swift in Sources */, 581BFB672926431000D251EC /* WelcomeWindowView.swift in Sources */, @@ -4094,7 +4101,6 @@ 77A01E302BB4270F00F0EA38 /* ProjectCEWorkspaceSettingsView.swift in Sources */, 669A50532C380C8E00304CD8 /* Collection+subscript_safe.swift in Sources */, 77A01E2C2BB425B200F0EA38 /* CEWorkspaceSettingsData+TasksSettings.swift in Sources */, - 6C6741402C44A2A200AABDF5 /* ProjectNavigatorViewController+Delegate.swift in Sources */, 5B241BF32B6DDBFF0016E616 /* IgnorePatternListItemView.swift in Sources */, 6CB52DC92AC8DC3E002E75B3 /* CEWorkspaceFileManager+FileManagement.swift in Sources */, 58F2EB0B292FB2B0004A9BDE /* AccountsSettings.swift in Sources */, diff --git a/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorResultList/FindNavigatorListViewController.swift b/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorResultList/FindNavigatorListViewController.swift index b10b795214..f469de2ee7 100644 --- a/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorResultList/FindNavigatorListViewController.swift +++ b/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorResultList/FindNavigatorListViewController.swift @@ -230,18 +230,6 @@ extension FindNavigatorListViewController: NSOutlineViewDelegate { let indexes = IndexSet(integersIn: 0.. String { - return "" - } } // MARK: - NSMenuDelegate diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+DataSource.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+DataSource.swift deleted file mode 100644 index 1437f9c877..0000000000 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+DataSource.swift +++ /dev/null @@ -1,119 +0,0 @@ -// -// ProjectNavigatorViewController+DataSource.swift -// CodeEdit -// -// Created by Khan Winter on 7/14/24. -// - -import AppKit - -extension ProjectNavigatorViewController: NSOutlineViewDataSource { - func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int { - if let item = item as? CEWorkspaceFile { - return item.isFolder ? workspace?.workspaceFileManager?.childrenOfFile(item)?.count ?? 0 : 0 - } - return content.count - } - - func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any { - if let item = item as? CEWorkspaceFile, - let children = workspace?.workspaceFileManager?.childrenOfFile(item) { - return children[index] - } - return content[index] - } - - func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: Any) -> Bool { - if let item = item as? CEWorkspaceFile { - return item.isFolder - } - return false - } - - /// write dragged file(s) to pasteboard - func outlineView(_ outlineView: NSOutlineView, pasteboardWriterForItem item: Any) -> NSPasteboardWriting? { - guard let fileItem = item as? CEWorkspaceFile else { return nil } - return fileItem.url as NSURL - } - - /// declare valid drop target - func outlineView( - _ outlineView: NSOutlineView, - validateDrop info: NSDraggingInfo, - proposedItem item: Any?, - proposedChildIndex index: Int - ) -> NSDragOperation { - guard let fileItem = item as? CEWorkspaceFile else { return [] } - // -1 index indicates that we are hovering over a row in outline view (folder or file) - if index == -1 { - if !fileItem.isFolder { - outlineView.setDropItem(fileItem.parent, dropChildIndex: index) - } - return info.draggingSourceOperationMask == .copy ? .copy : .move - } - return [] - } - - /// handle successful or unsuccessful drop - func outlineView( - _ outlineView: NSOutlineView, - acceptDrop info: NSDraggingInfo, - item: Any?, - childIndex index: Int - ) -> Bool { - guard let pasteboardItems = info.draggingPasteboard.readObjects(forClasses: [NSURL.self]) else { return false } - let fileItemURLS = pasteboardItems.compactMap { $0 as? URL } - - guard let fileItemDestination = item as? CEWorkspaceFile else { return false } - let destParentURL = fileItemDestination.url - - for fileItemURL in fileItemURLS { - let destURL = destParentURL.appendingPathComponent(fileItemURL.lastPathComponent) - // cancel dropping file item on self or in parent directory - if fileItemURL == destURL || fileItemURL == destParentURL { - return false - } - - // Needs to come before call to .removeItem or else race condition occurs - var srcFileItem: CEWorkspaceFile? = workspace?.workspaceFileManager?.getFile(fileItemURL.path) - // If srcFileItem is nil, fileItemUrl is an external file url. - if srcFileItem == nil { - srcFileItem = CEWorkspaceFile(url: URL(fileURLWithPath: fileItemURL.path)) - } - - guard let srcFileItem else { - return false - } - - if CEWorkspaceFile.fileManager.fileExists(atPath: destURL.path) { - let shouldReplace = replaceFileDialog(fileName: fileItemURL.lastPathComponent) - guard shouldReplace else { - return false - } - do { - try CEWorkspaceFile.fileManager.removeItem(at: destURL) - } catch { - fatalError(error.localizedDescription) - } - } - if info.draggingSourceOperationMask == .copy { - self.copyFile(file: srcFileItem, to: destURL) - } else { - self.moveFile(file: srcFileItem, to: destURL) - } - } - return true - } - - func replaceFileDialog(fileName: String) -> Bool { - let alert = NSAlert() - alert.messageText = """ - A file or folder with the name \(fileName) already exists in the destination folder. Do you want to replace it? - """ - alert.informativeText = "This action is irreversible!" - alert.alertStyle = .warning - alert.addButton(withTitle: "Replace") - alert.addButton(withTitle: "Cancel") - return alert.runModal() == .alertFirstButtonReturn - } -} diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+Delegate.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+Delegate.swift deleted file mode 100644 index 3389db81c6..0000000000 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+Delegate.swift +++ /dev/null @@ -1,148 +0,0 @@ -// -// ProjectNavigatorViewController+Delegate.swift -// CodeEdit -// -// Created by Khan Winter on 7/14/24. -// - -import AppKit - -extension ProjectNavigatorViewController: NSOutlineViewDelegate { - func outlineView( - _ outlineView: NSOutlineView, - shouldShowCellExpansionFor tableColumn: NSTableColumn?, - item: Any - ) -> Bool { - true - } - - func outlineView(_ outlineView: NSOutlineView, shouldShowOutlineCellForItem item: Any) -> Bool { - true - } - - func outlineView(_ outlineView: NSOutlineView, viewFor tableColumn: NSTableColumn?, item: Any) -> NSView? { - guard let tableColumn else { return nil } - - let frameRect = NSRect(x: 0, y: 0, width: tableColumn.width, height: rowHeight) - - return ProjectNavigatorTableViewCell(frame: frameRect, item: item as? CEWorkspaceFile, delegate: self) - } - - func outlineViewSelectionDidChange(_ notification: Notification) { - guard let outlineView = notification.object as? NSOutlineView else { return } - - let selectedIndex = outlineView.selectedRow - - guard let item = outlineView.item(atRow: selectedIndex) as? CEWorkspaceFile else { return } - - if !item.isFolder && shouldSendSelectionUpdate { - DispatchQueue.main.async { - self.shouldSendSelectionUpdate = false - self.workspace?.editorManager.activeEditor.openTab(file: item, asTemporary: true) - self.shouldSendSelectionUpdate = true - } - } - } - - func outlineView(_ outlineView: NSOutlineView, heightOfRowByItem item: Any) -> CGFloat { - rowHeight // This can be changed to 20 to match Xcode's row height. - } - - func outlineViewItemDidExpand(_ notification: Notification) { - guard let id = workspace?.editorManager.activeEditor.selectedTab?.file.id, - let item = workspace?.workspaceFileManager?.getFile(id, createIfNotFound: true), - /// update outline selection only if the parent of selected item match with expanded item - item.parent === notification.userInfo?["NSObject"] as? CEWorkspaceFile else { - return - } - /// select active file under collapsed folder only if its parent is expanding - if outlineView.isItemExpanded(item.parent) { - updateSelection(itemID: item.id) - } - } - - func outlineViewItemDidCollapse(_ notification: Notification) {} - - func outlineView(_ outlineView: NSOutlineView, itemForPersistentObject object: Any) -> Any? { - guard let id = object as? CEWorkspaceFile.ID, - let item = workspace?.workspaceFileManager?.getFile(id, createIfNotFound: true) else { return nil } - return item - } - - func outlineView(_ outlineView: NSOutlineView, persistentObjectForItem item: Any?) -> Any? { - guard let item = item as? CEWorkspaceFile else { return nil } - return item.id - } - - /// Finds and selects an ``Item`` from an array of ``Item`` and their `children` based on the `id`. - /// - Parameters: - /// - id: the id of the item item - /// - collection: the array to search for - /// - forcesReveal: The boolean to indicates whether or not it should force to reveal the selected file. - func select(by id: EditorTabID, forcesReveal: Bool) { - guard case .codeEditor(let path) = id, - let item = workspace?.workspaceFileManager?.getFile(path, createIfNotFound: true) else { - return - } - // If the user has set "Reveal file on selection change" to on or it is forced to reveal, - // we need to reveal the item before selecting the row. - if Settings.shared.preferences.general.revealFileOnFocusChange || forcesReveal { - reveal(item) - } - let row = outlineView.row(forItem: item) - if row == -1 { - outlineView.deselectRow(outlineView.selectedRow) - } - shouldSendSelectionUpdate = false - outlineView.selectRowIndexes(.init(integer: row), byExtendingSelection: false) - shouldSendSelectionUpdate = true - } - - /// Reveals the given `fileItem` in the outline view by expanding all the parent directories of the file. - /// If the file is not found, it will present an alert saying so. - /// - Parameter fileItem: The file to reveal. - public func reveal(_ fileItem: CEWorkspaceFile) { - if let parent = fileItem.parent { - expandParent(item: parent) - } - let row = outlineView.row(forItem: fileItem) - shouldSendSelectionUpdate = false - outlineView.selectRowIndexes(.init(integer: row), byExtendingSelection: false) - shouldSendSelectionUpdate = true - - if row < 0 { - let alert = NSAlert() - alert.messageText = NSLocalizedString( - "Could not find file", - comment: "Could not find file" - ) - alert.runModal() - return - } else { - let visibleRect = scrollView.contentView.visibleRect - let visibleRows = outlineView.rows(in: visibleRect) - guard !visibleRows.contains(row) else { - /// in case that the selected file is not fully visible (some parts are out of the visible rect), - /// `scrollRowToVisible(_:)` method brings the file where it can be fully visible. - outlineView.scrollRowToVisible(row) - return - } - let rowRect = outlineView.rect(ofRow: row) - let centerY = rowRect.midY - (visibleRect.height / 2) - let center = NSPoint(x: 0, y: centerY) - /// `scroll(_:)` method alone doesn't bring the selected file to the center in some cases. - /// calling `scrollRowToVisible(_:)` method before it makes the file reveal in the center more correctly. - outlineView.scrollRowToVisible(row) - outlineView.scroll(center) - } - } - - /// Method for recursively expanding a file's parent directories. - /// - Parameter item: - private func expandParent(item: CEWorkspaceFile) { - if let parent = item.parent as CEWorkspaceFile? { - expandParent(item: parent) - } - outlineView.expandItem(item) - } -} diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDataSource.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDataSource.swift index b2782a3e7f..2b7fc81d77 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDataSource.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDataSource.swift @@ -30,13 +30,13 @@ extension ProjectNavigatorViewController: NSOutlineViewDataSource { return false } - /// write dragged file(s) to pasteboard + /// Write dragged file(s) to pasteboard func outlineView(_ outlineView: NSOutlineView, pasteboardWriterForItem item: Any) -> NSPasteboardWriting? { guard let fileItem = item as? CEWorkspaceFile else { return nil } return fileItem.url as NSURL } - /// declare valid drop target + /// Declare valid drop target func outlineView( _ outlineView: NSOutlineView, validateDrop info: NSDraggingInfo, @@ -54,7 +54,7 @@ extension ProjectNavigatorViewController: NSOutlineViewDataSource { return [] } - /// handle successful or unsuccessful drop + /// Handle successful or unsuccessful drop func outlineView( _ outlineView: NSOutlineView, acceptDrop info: NSDraggingInfo, diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift index 52c8eeafcc..53d9298a6c 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift @@ -146,8 +146,8 @@ extension ProjectNavigatorViewController: NSOutlineViewDelegate { outlineView.expandItem(item) } - // swiftlint:disable:next function_parameter_count - func outlineView( + /// Adds a tooltip to the file row. + func outlineView( // swiftlint:disable:this function_parameter_count _ outlineView: NSOutlineView, toolTipFor cell: NSCell, rect: NSRectPointer,