From a4a76a20043fb23136bc6515afe9969aaac31b74 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sun, 19 Mar 2023 03:19:08 +0100 Subject: [PATCH 1/4] =?UTF-8?q?=F0=9F=A7=B9=20Refactor=20of=20the=20Worksp?= =?UTF-8?q?aceClient=20(#1157)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CodeEdit.xcodeproj/project.pbxproj | 100 ++-- .../CEWorkspace/Models/CEWorkspaceFile.swift | 483 ++++++++++++++++++ .../Models/CEWorkspaceFileIcon.swift} | 0 .../Models/CEWorkspaceFileManager.swift | 257 ++++++++++ .../Views/ToolbarBranchPicker.swift | 10 +- .../CodeEditWindowController.swift | 2 +- .../Views/WorkspaceCodeFileView.swift | 6 +- .../WorkspaceDocument+Listeners.swift | 5 +- .../Documents/WorkspaceDocument+Search.swift | 2 +- .../Documents/WorkspaceDocument.swift | 28 +- .../ExtensionsStore/Models/Plugin.swift | 2 +- .../FindNavigatorListViewController.swift | 1 - .../NavigatorSidebarToolbarBottom.swift | 22 +- .../OutlineView/OutlineMenu.swift | 9 +- .../OutlineView/OutlineTableViewCell.swift | 22 +- .../OutlineView/OutlineView.swift | 4 +- .../OutlineView/OutlineViewController.swift | 59 ++- ...troller+OutlineTableViewCellDelegate.swift | 6 +- .../ProjectNavigatorView.swift | 4 +- .../PathBar/Views/PathBarComponent.swift | 12 +- .../Features/PathBar/Views/PathBarMenu.swift | 22 +- .../Features/PathBar/Views/PathBarView.swift | 14 +- .../ViewModels/QuickOpenViewModel.swift | 4 +- .../QuickOpen/Views/QuickOpenItem.swift | 4 +- .../Views/QuickOpenPreviewView.swift | 4 +- .../QuickOpen/Views/QuickOpenView.swift | 6 +- .../Search/Model/SearchResultMatchModel.swift | 4 +- .../Search/Model/SearchResultModel.swift | 6 +- .../StatusBarDrawer/StatusBarDrawer.swift | 2 +- .../Tabs/Models/TabBarItemRepresentable.swift | 2 +- .../Features/Tabs/Models/TabManager.swift | 10 +- .../Features/Tabs/TabGroup/TabGroup.swift | 4 +- .../Features/Tabs/TabGroup/TabGroupData.swift | 23 +- .../Tabs/Views/TabBarContextMenu.swift | 12 +- .../Features/Tabs/Views/TabBarItemView.swift | 16 +- CodeEdit/Features/Tabs/Views/TabBarView.swift | 16 +- .../Array/Array+CEWorkspaceFile.swift | 43 ++ .../Utils/WorkspaceClient/Interface.swift | 39 -- CodeEdit/Utils/WorkspaceClient/Live.swift | 178 ------- CodeEdit/Utils/WorkspaceClient/Mocks.swift | 18 - .../Model/FileItem+Array.swift | 29 -- .../WorkspaceClient/Model/FileItem.swift | 338 ------------ CodeEdit/WorkspaceView.swift | 2 +- 43 files changed, 1017 insertions(+), 813 deletions(-) create mode 100644 CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile.swift rename CodeEdit/{Utils/WorkspaceClient/Model/FileIcon.swift => Features/CEWorkspace/Models/CEWorkspaceFileIcon.swift} (100%) create mode 100644 CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager.swift create mode 100644 CodeEdit/Utils/Extensions/Array/Array+CEWorkspaceFile.swift delete mode 100644 CodeEdit/Utils/WorkspaceClient/Interface.swift delete mode 100644 CodeEdit/Utils/WorkspaceClient/Live.swift delete mode 100644 CodeEdit/Utils/WorkspaceClient/Mocks.swift delete mode 100644 CodeEdit/Utils/WorkspaceClient/Model/FileItem+Array.swift delete mode 100644 CodeEdit/Utils/WorkspaceClient/Model/FileItem.swift diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index 4c3996deed..86022cb047 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -57,7 +57,6 @@ 285FEC7027FE4B9800E57D53 /* OutlineTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 285FEC6F27FE4B9800E57D53 /* OutlineTableViewCell.swift */; }; 286471AB27ED51FD0039369D /* ProjectNavigatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 286471AA27ED51FD0039369D /* ProjectNavigatorView.swift */; }; 287776E927E34BC700D46668 /* TabBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 287776E827E34BC700D46668 /* TabBarView.swift */; }; - 287776EF27E3515300D46668 /* TabBarItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 287776EE27E3515300D46668 /* TabBarItemView.swift */; }; 2897E1C72979A29200741E32 /* OffsettableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2897E1C62979A29200741E32 /* OffsettableScrollView.swift */; }; 28A51001281673530087B0CC /* codeedit-xcode-dark.json in Resources */ = {isa = PBXBuildFile; fileRef = 28A50FFF281673530087B0CC /* codeedit-xcode-dark.json */; }; 28A51002281673530087B0CC /* codeedit-xcode-light.json in Resources */ = {isa = PBXBuildFile; fileRef = 28A51000281673530087B0CC /* codeedit-xcode-light.json */; }; @@ -95,6 +94,7 @@ 583E529729361B39001AB554 /* testEffectViewDark.1.png in Resources */ = {isa = PBXBuildFile; fileRef = 583E528329361B39001AB554 /* testEffectViewDark.1.png */; }; 583E529829361B39001AB554 /* testBranchPickerLight.1.png in Resources */ = {isa = PBXBuildFile; fileRef = 583E528429361B39001AB554 /* testBranchPickerLight.1.png */; }; 583E529C29361BAB001AB554 /* SnapshotTesting in Frameworks */ = {isa = PBXBuildFile; productRef = 583E529B29361BAB001AB554 /* SnapshotTesting */; }; + 58710159298EB80000951BA4 /* CEWorkspaceFileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58710158298EB80000951BA4 /* CEWorkspaceFileManager.swift */; }; 5878DA82291863F900DD95A3 /* AcknowledgementsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5878DA81291863F900DD95A3 /* AcknowledgementsView.swift */; }; 5878DA842918642000DD95A3 /* ParsePackagesResolved.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5878DA832918642000DD95A3 /* ParsePackagesResolved.swift */; }; 5878DA872918642F00DD95A3 /* AcknowledgementsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5878DA862918642F00DD95A3 /* AcknowledgementsViewModel.swift */; }; @@ -212,12 +212,7 @@ 587B9E9829301D8F00AC7927 /* GitCommit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587B9E5329301D8F00AC7927 /* GitCommit.swift */; }; 587B9E9929301D8F00AC7927 /* GitChangedFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587B9E5429301D8F00AC7927 /* GitChangedFile.swift */; }; 587B9E9A29301D8F00AC7927 /* GitType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587B9E5529301D8F00AC7927 /* GitType.swift */; }; - 587D9B732933BF5700BF7490 /* FileIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587D9B6D2933BF5700BF7490 /* FileIcon.swift */; }; - 587D9B742933BF5700BF7490 /* FileItem+Array.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587D9B6E2933BF5700BF7490 /* FileItem+Array.swift */; }; - 587D9B752933BF5700BF7490 /* FileItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587D9B6F2933BF5700BF7490 /* FileItem.swift */; }; - 587D9B762933BF5700BF7490 /* Mocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587D9B702933BF5700BF7490 /* Mocks.swift */; }; - 587D9B772933BF5700BF7490 /* Live.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587D9B712933BF5700BF7490 /* Live.swift */; }; - 587D9B782933BF5700BF7490 /* Interface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587D9B722933BF5700BF7490 /* Interface.swift */; }; + 587FB99029C1246400B519DD /* TabBarItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587FB98F29C1246400B519DD /* TabBarItemView.swift */; }; 58822524292C280D00E83CDE /* StatusBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58822509292C280D00E83CDE /* StatusBarView.swift */; }; 58822525292C280D00E83CDE /* StatusBarMenuLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5882250B292C280D00E83CDE /* StatusBarMenuLabel.swift */; }; 58822526292C280D00E83CDE /* StatusBarBreakpointButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5882250C292C280D00E83CDE /* StatusBarBreakpointButton.swift */; }; @@ -235,6 +230,9 @@ 58822532292C280D00E83CDE /* StatusBarViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5882251C292C280D00E83CDE /* StatusBarViewModel.swift */; }; 58822533292C280D00E83CDE /* StatusBarTabType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5882251D292C280D00E83CDE /* StatusBarTabType.swift */; }; 58822534292C280D00E83CDE /* CursorLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5882251E292C280D00E83CDE /* CursorLocation.swift */; }; + 588847632992A2A200996D95 /* CEWorkspaceFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 588847622992A2A200996D95 /* CEWorkspaceFile.swift */; }; + 588847692992ABCA00996D95 /* Array+CEWorkspaceFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 588847682992ABCA00996D95 /* Array+CEWorkspaceFile.swift */; }; + 58A2E40C29C3975D005CB615 /* CEWorkspaceFileIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A2E40629C3975D005CB615 /* CEWorkspaceFileIcon.swift */; }; 58A5DF7D2931787A00D1BD5D /* ShellClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A5DF7C2931787A00D1BD5D /* ShellClient.swift */; }; 58A5DF8029325B5A00D1BD5D /* GitClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A5DF7F29325B5A00D1BD5D /* GitClient.swift */; }; 58A5DFA229339F6400D1BD5D /* KeybindingManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A5DF9E29339F6400D1BD5D /* KeybindingManager.swift */; }; @@ -473,7 +471,6 @@ 286471AA27ED51FD0039369D /* ProjectNavigatorView.swift */ = {isa = PBXFileReference; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = ProjectNavigatorView.swift; sourceTree = ""; tabWidth = 4; }; 287776E627E3413200D46668 /* NavigatorSidebarView.swift */ = {isa = PBXFileReference; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = NavigatorSidebarView.swift; sourceTree = ""; tabWidth = 4; }; 287776E827E34BC700D46668 /* TabBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarView.swift; sourceTree = ""; }; - 287776EE27E3515300D46668 /* TabBarItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarItemView.swift; sourceTree = ""; }; 2897E1C62979A29200741E32 /* OffsettableScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OffsettableScrollView.swift; sourceTree = ""; }; 28A50FFF281673530087B0CC /* codeedit-xcode-dark.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "codeedit-xcode-dark.json"; sourceTree = ""; }; 28A51000281673530087B0CC /* codeedit-xcode-light.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "codeedit-xcode-light.json"; sourceTree = ""; }; @@ -512,6 +509,7 @@ 583E528329361B39001AB554 /* testEffectViewDark.1.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = testEffectViewDark.1.png; sourceTree = ""; }; 583E528429361B39001AB554 /* testBranchPickerLight.1.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = testBranchPickerLight.1.png; sourceTree = ""; }; 583E52A129361BFD001AB554 /* CodeEditUITests-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "CodeEditUITests-Bridging-Header.h"; sourceTree = ""; }; + 58710158298EB80000951BA4 /* CEWorkspaceFileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CEWorkspaceFileManager.swift; sourceTree = ""; }; 5878DA81291863F900DD95A3 /* AcknowledgementsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AcknowledgementsView.swift; sourceTree = ""; }; 5878DA832918642000DD95A3 /* ParsePackagesResolved.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParsePackagesResolved.swift; sourceTree = ""; }; 5878DA862918642F00DD95A3 /* AcknowledgementsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AcknowledgementsViewModel.swift; sourceTree = ""; }; @@ -626,12 +624,7 @@ 587B9E5329301D8F00AC7927 /* GitCommit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GitCommit.swift; sourceTree = ""; }; 587B9E5429301D8F00AC7927 /* GitChangedFile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GitChangedFile.swift; sourceTree = ""; }; 587B9E5529301D8F00AC7927 /* GitType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GitType.swift; sourceTree = ""; }; - 587D9B6D2933BF5700BF7490 /* FileIcon.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileIcon.swift; sourceTree = ""; }; - 587D9B6E2933BF5700BF7490 /* FileItem+Array.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "FileItem+Array.swift"; sourceTree = ""; }; - 587D9B6F2933BF5700BF7490 /* FileItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileItem.swift; sourceTree = ""; }; - 587D9B702933BF5700BF7490 /* Mocks.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Mocks.swift; sourceTree = ""; }; - 587D9B712933BF5700BF7490 /* Live.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Live.swift; sourceTree = ""; }; - 587D9B722933BF5700BF7490 /* Interface.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Interface.swift; sourceTree = ""; }; + 587FB98F29C1246400B519DD /* TabBarItemView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabBarItemView.swift; sourceTree = ""; }; 58822509292C280D00E83CDE /* StatusBarView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusBarView.swift; sourceTree = ""; }; 5882250B292C280D00E83CDE /* StatusBarMenuLabel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusBarMenuLabel.swift; sourceTree = ""; }; 5882250C292C280D00E83CDE /* StatusBarBreakpointButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusBarBreakpointButton.swift; sourceTree = ""; }; @@ -649,7 +642,10 @@ 5882251C292C280D00E83CDE /* StatusBarViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusBarViewModel.swift; sourceTree = ""; }; 5882251D292C280D00E83CDE /* StatusBarTabType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusBarTabType.swift; sourceTree = ""; }; 5882251E292C280D00E83CDE /* CursorLocation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CursorLocation.swift; sourceTree = ""; }; + 588847622992A2A200996D95 /* CEWorkspaceFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CEWorkspaceFile.swift; sourceTree = ""; }; + 588847682992ABCA00996D95 /* Array+CEWorkspaceFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+CEWorkspaceFile.swift"; sourceTree = ""; }; 589F3E342936185400E1A4DA /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Platforms/MacOSX.platform/Developer/Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; }; + 58A2E40629C3975D005CB615 /* CEWorkspaceFileIcon.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CEWorkspaceFileIcon.swift; sourceTree = ""; }; 58A5DF7C2931787A00D1BD5D /* ShellClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShellClient.swift; sourceTree = ""; }; 58A5DF7F29325B5A00D1BD5D /* GitClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GitClient.swift; sourceTree = ""; }; 58A5DF9E29339F6400D1BD5D /* KeybindingManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeybindingManager.swift; sourceTree = ""; }; @@ -1113,13 +1109,13 @@ children = ( 582213EE2918345500EFE361 /* About */, 5878DA7D291862BC00DD95A3 /* Acknowledgements */, - 284DC84B2978B5EB00BF2770 /* Contributors */, 58F2EA9E292FB2B0004A9BDE /* AppPreferences */, - 5878DAAB291D627C00DD95A3 /* PathBar */, + 588847642992A30900996D95 /* CEWorkspace */, 587B9D50292FC27A00AC7927 /* CodeEditExtension */, 587B9D7529300ABD00AC7927 /* CodeEditUI */, 58798244292E78D80085B254 /* CodeFile */, 58FD7603291EA1CB0051D6E4 /* Commands */, + 284DC84B2978B5EB00BF2770 /* Contributors */, 043C321227E31FE8006AE443 /* Documents */, 58798256292EC4080085B254 /* ExtensionsStore */, 58798228292E30B90085B254 /* Feedback */, @@ -1128,6 +1124,7 @@ 58A5DF9D29339F6400D1BD5D /* Keybindings */, 58798274292ECFD40085B254 /* LSP */, 287776EA27E350A100D46668 /* NavigatorSidebar */, + 5878DAAB291D627C00DD95A3 /* PathBar */, 5878DAA0291AE76700DD95A3 /* QuickOpen */, 58798210292D92370085B254 /* Search */, 6C147C4729A329E50089B630 /* SplitView */, @@ -1752,27 +1749,6 @@ path = Models; sourceTree = ""; }; - 587D9B6B2933BF5700BF7490 /* WorkspaceClient */ = { - isa = PBXGroup; - children = ( - 587D9B6C2933BF5700BF7490 /* Model */, - 587D9B702933BF5700BF7490 /* Mocks.swift */, - 587D9B712933BF5700BF7490 /* Live.swift */, - 587D9B722933BF5700BF7490 /* Interface.swift */, - ); - path = WorkspaceClient; - sourceTree = ""; - }; - 587D9B6C2933BF5700BF7490 /* Model */ = { - isa = PBXGroup; - children = ( - 587D9B6D2933BF5700BF7490 /* FileIcon.swift */, - 587D9B6E2933BF5700BF7490 /* FileItem+Array.swift */, - 587D9B6F2933BF5700BF7490 /* FileItem.swift */, - ); - path = Model; - sourceTree = ""; - }; 588224FF292C280D00E83CDE /* StatusBar */ = { isa = PBXGroup; children = ( @@ -1844,6 +1820,40 @@ path = ViewModels; sourceTree = ""; }; + 588847642992A30900996D95 /* CEWorkspace */ = { + isa = PBXGroup; + children = ( + 588847652992A35800996D95 /* Models */, + 588847662992A36100996D95 /* Views */, + ); + path = CEWorkspace; + sourceTree = ""; + }; + 588847652992A35800996D95 /* Models */ = { + isa = PBXGroup; + children = ( + 588847622992A2A200996D95 /* CEWorkspaceFile.swift */, + 58A2E40629C3975D005CB615 /* CEWorkspaceFileIcon.swift */, + 58710158298EB80000951BA4 /* CEWorkspaceFileManager.swift */, + ); + path = Models; + sourceTree = ""; + }; + 588847662992A36100996D95 /* Views */ = { + isa = PBXGroup; + children = ( + ); + path = Views; + sourceTree = ""; + }; + 588847672992AAB800996D95 /* Array */ = { + isa = PBXGroup; + children = ( + 588847682992ABCA00996D95 /* Array+CEWorkspaceFile.swift */, + ); + path = Array; + sourceTree = ""; + }; 58A5DF7B2931784D00D1BD5D /* Models */ = { isa = PBXGroup; children = ( @@ -1867,11 +1877,11 @@ isa = PBXGroup; children = ( 287776E827E34BC700D46668 /* TabBarView.swift */, - 287776EE27E3515300D46668 /* TabBarItemView.swift */, B6C6A429297716A500A3D28F /* TabBarItemCloseButton.swift */, 6CDA84AC284C1BA000C1CC3A /* TabBarContextMenu.swift */, B6C6A42D29771A8D00A3D28F /* TabBarItemButtonStyle.swift */, DE6F77862813625500D00A76 /* TabBarDivider.swift */, + 587FB98F29C1246400B519DD /* TabBarItemView.swift */, DE6405A52817734700881FDF /* TabBarNative.swift */, DE513F51281B672D002260B9 /* TabBarAccessory.swift */, DE513F53281DE5D0002260B9 /* TabBarXcode.swift */, @@ -1898,7 +1908,6 @@ 58D01C8F293167DC00C5B6B4 /* KeyChain */, 5831E3C92933E83400D5A6D2 /* Protocols */, 5875680E29316BDC00C965A3 /* ShellClient */, - 587D9B6B2933BF5700BF7490 /* WorkspaceClient */, ); path = Utils; sourceTree = ""; @@ -1907,6 +1916,7 @@ isa = PBXGroup; children = ( 6C82D6C429C0129E00495C54 /* NSApplication */, + 588847672992AAB800996D95 /* Array */, 6CBD1BC42978DE3E006639D5 /* Text */, 5831E3D02934036D00D5A6D2 /* NSTableView */, 5831E3CA2933E86F00D5A6D2 /* View */, @@ -2713,6 +2723,7 @@ 587B9D9F29300ABD00AC7927 /* SegmentedControl.swift in Sources */, 6C7256D729A3D7D000C2D3E0 /* SplitViewControllerView.swift in Sources */, 58FD7609291EA1CB0051D6E4 /* CommandPaletteView.swift in Sources */, + 58A2E40C29C3975D005CB615 /* CEWorkspaceFileIcon.swift in Sources */, 58F2EAFC292FB2B0004A9BDE /* GitAccountItemView.swift in Sources */, 587B9E8F29301D8F00AC7927 /* BitBucketUserRouter.swift in Sources */, 58F2EB03292FB2B0004A9BDE /* Documentation.docc in Sources */, @@ -2814,12 +2825,12 @@ 581BFB692926431000D251EC /* WelcomeActionView.swift in Sources */, 20D839AE280E0CA700B27357 /* HistoryPopoverView.swift in Sources */, 58F2EAFD292FB2B0004A9BDE /* GitHubLoginView.swift in Sources */, - 587D9B782933BF5700BF7490 /* Interface.swift in Sources */, 6CFF967A29BEBD2400182D6F /* ViewCommands.swift in Sources */, 2072FA1E280D891500C7F8D4 /* FileLocation.swift in Sources */, 587B9E6729301D8F00AC7927 /* GitLabEventData.swift in Sources */, 5882252F292C280D00E83CDE /* StatusBarClearButton.swift in Sources */, 58F2EB04292FB2B0004A9BDE /* SourceControlPreferences.swift in Sources */, + 58710159298EB80000951BA4 /* CEWorkspaceFileManager.swift in Sources */, 582213F0291834A500EFE361 /* AboutView.swift in Sources */, 58F2EB12292FB2B0004A9BDE /* PreferencesPlaceholderView.swift in Sources */, 6CC9E4B229B5669900C97388 /* Environment+ActiveTabGroup.swift in Sources */, @@ -2839,11 +2850,11 @@ 58F2EAF8292FB2B0004A9BDE /* AccountListItemView.swift in Sources */, 587B9E7429301D8F00AC7927 /* URL+URLParameters.swift in Sources */, 581BFB6B2926431000D251EC /* RecentProjectItem.swift in Sources */, + 587FB99029C1246400B519DD /* TabBarItemView.swift in Sources */, 6C5AB9D729C1496E003B5F96 /* SceneID.swift in Sources */, 587B9DA429300ABD00AC7927 /* OverlayPanel.swift in Sources */, 58F2EAEE292FB2B0004A9BDE /* TextEditingPreferencesView.swift in Sources */, 58D01C95293167DC00C5B6B4 /* Bundle+Info.swift in Sources */, - 587D9B762933BF5700BF7490 /* Mocks.swift in Sources */, B6C6A42A297716A500A3D28F /* TabBarItemCloseButton.swift in Sources */, 58A5DF7D2931787A00D1BD5D /* ShellClient.swift in Sources */, 5879821A292D92370085B254 /* SearchResultModel.swift in Sources */, @@ -2851,7 +2862,6 @@ 0485EB1F27E7458B00138301 /* WorkspaceCodeFileView.swift in Sources */, 58D01C94293167DC00C5B6B4 /* Color+HEX.swift in Sources */, 58798251292E78D80085B254 /* OtherFileView.swift in Sources */, - 587D9B752933BF5700BF7490 /* FileItem.swift in Sources */, 587B9E7929301D8F00AC7927 /* GitHubIssueRouter.swift in Sources */, 587B9E8029301D8F00AC7927 /* GitHubConfiguration.swift in Sources */, 58822524292C280D00E83CDE /* StatusBarView.swift in Sources */, @@ -2883,7 +2893,6 @@ 58F2EAF1292FB2B0004A9BDE /* TerminalThemeView.swift in Sources */, 04C3254B27FF23B000C8DA2D /* ExtensionNavigatorData.swift in Sources */, 58798261292EC4080085B254 /* ExtensionsManager.swift in Sources */, - 287776EF27E3515300D46668 /* TabBarItemView.swift in Sources */, 2806E9022979588B000040F4 /* Contributor.swift in Sources */, 58D01C98293167DC00C5B6B4 /* String+RemoveOccurrences.swift in Sources */, 5878DAA8291AE76700DD95A3 /* QuickOpenItem.swift in Sources */, @@ -2897,8 +2906,6 @@ 5879821B292D92370085B254 /* SearchResultMatchModel.swift in Sources */, 587B9E5929301D8F00AC7927 /* GitCheckoutBranchView+CheckoutBranch.swift in Sources */, 58F2EB09292FB2B0004A9BDE /* TerminalPreferences.swift in Sources */, - 587D9B742933BF5700BF7490 /* FileItem+Array.swift in Sources */, - 587D9B772933BF5700BF7490 /* Live.swift in Sources */, 6C48D8F42972DB1A00D6D205 /* Env+Window.swift in Sources */, 2072FA18280D871200C7F8D4 /* TextEncoding.swift in Sources */, 58F2EAF3292FB2B0004A9BDE /* ThemePreviewIcon.swift in Sources */, @@ -2909,6 +2916,7 @@ 587B9E7629301D8F00AC7927 /* GitTime.swift in Sources */, 2072FA16280D83A500C7F8D4 /* FileTypeList.swift in Sources */, 587B9E5D29301D8F00AC7927 /* GitLabUserRouter.swift in Sources */, + 588847692992ABCA00996D95 /* Array+CEWorkspaceFile.swift in Sources */, 04C3254F2800AA4700C8DA2D /* ExtensionInstallationView.swift in Sources */, 58822530292C280D00E83CDE /* FilterTextField.swift in Sources */, 6C82D6B929BFE34900495C54 /* HelpCommands.swift in Sources */, @@ -2975,7 +2983,7 @@ 58798263292EC4080085B254 /* DownloadedPlugin.swift in Sources */, 587B9E6029301D8F00AC7927 /* GitLabOAuthRouter.swift in Sources */, 6C05A8AF284D0CA3007F4EAA /* WorkspaceDocument+Listeners.swift in Sources */, - 587D9B732933BF5700BF7490 /* FileIcon.swift in Sources */, + 588847632992A2A200996D95 /* CEWorkspaceFile.swift in Sources */, 58F2EAF9292FB2B0004A9BDE /* AccountSelectionDialog.swift in Sources */, 6C2C155D29B4F4E500EA60A5 /* SplitViewReader.swift in Sources */, 58F2EAE8292FB2B0004A9BDE /* TerminalPreferencesView.swift in Sources */, diff --git a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile.swift b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile.swift new file mode 100644 index 0000000000..29d0d5bf2f --- /dev/null +++ b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile.swift @@ -0,0 +1,483 @@ +// +// FileItem.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom on 07/02/2023. +// + +import Foundation +import SwiftUI +import UniformTypeIdentifiers + +enum FileItemCodingKeys: String, CodingKey { + case id + case url + case children + case changeType +} + +/// An object containing all necessary information and actions for a specific file in the workspace +final class CEWorkspaceFile: Codable, Comparable, Hashable, Identifiable, TabBarItemRepresentable { + + /// The id of the ``FileSystemClient/FileSystemClient/FileItem``. + /// + /// This is equal to `url.relativePath` + var id: String { url.relativePath } + + /// Returns the file name (e.g.: `Package.swift`) + var name: String { url.lastPathComponent } + + /// Returns the extension of the file or an empty string if no extension is present. + var type: FileIcon.FileType { .init(rawValue: url.pathExtension) ?? .txt } + + /// Returns the URL of the ``FileSystemClient/FileSystemClient/FileItem`` + var url: URL + + /// Return the icon of the file as `Image` + var icon: Image { Image(systemName: systemImage) } + + /// Returns the children of the current ``FileSystemClient/FileSystemClient/FileItem``. + /// + /// If the current ``FileSystemClient/FileSystemClient/FileItem`` is a file this will be `nil`. + /// If it is an empty folder this will be an empty array. + var children: [CEWorkspaceFile]? + + /// Returns a parent ``FileSystemClient/FileSystemClient/FileItem``. + /// + /// If the item already is the top-level ``FileSystemClient/FileSystemClient/FileItem`` this returns `nil`. + var parent: CEWorkspaceFile? + + /// Returns the `id` in ``TabBarItemID`` enum form + var tabID: TabBarItemID { .codeEditor(id) } + + var fileDocument: CodeFileDocument? + + var fileIdentifier = UUID().uuidString + + var watcher: DispatchSourceFileSystemObject? + var watcherCode: ((CEWorkspaceFile) -> Void)? + + /// Returns the Git status of a file as ``GitType`` + var gitStatus: GitType? + + /// Returns a boolean that is true if ``children`` is not `nil` + var isFolder: Bool { url.hasDirectoryPath } + + /// Returns a boolean that is true if the file item is the root folder of the workspace. + var isRoot: Bool { parent == nil } + + /// Returns a boolean that is true if the file item actually exists in the file system + var doesExist: Bool { CEWorkspaceFile.fileManger.fileExists(atPath: self.url.path) } + + /// Returns a string describing a SFSymbol for the current ``FileSystemClient/FileSystemClient/FileItem`` + /// + /// Use it like this + /// ```swift + /// Image(systemName: item.systemImage) + /// ``` + var systemImage: String { + if let children = children { + // item is a folder + return folderIcon(children) + } else { + // item is a file + return FileIcon.fileIcon(fileType: type) + } + } + + var debugFileHeirachy: String { childrenDescription(tabCount: 0) } + + init( + url: URL, + children: [CEWorkspaceFile]? = nil, + changeType: GitType? = nil + ) { + self.url = url + self.children = children + self.gitStatus = changeType + } + + required init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: FileItemCodingKeys.self) + url = try values.decode(URL.self, forKey: .url) + children = try values.decode([CEWorkspaceFile]?.self, forKey: .children) + gitStatus = try values.decode(GitType.self, forKey: .changeType) + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: FileItemCodingKeys.self) + try container.encode(id, forKey: .id) + try container.encode(url, forKey: .url) + try container.encode(children, forKey: .children) + try container.encode(gitStatus, forKey: .changeType) + } + + func activateWatcher() -> Bool { + guard let watcherCode else { return false } + + let descriptor = open(self.url.path, O_EVTONLY) + guard descriptor > 0 else { return false } + + // create the source + let source = DispatchSource.makeFileSystemObjectSource( + fileDescriptor: descriptor, + eventMask: .write, + queue: DispatchQueue.global() + ) + + if descriptor > 2000 { + print("Watcher \(descriptor) used up on \(url.path)") + } + + source.setEventHandler { watcherCode(self) } + source.setCancelHandler { close(descriptor) } + source.resume() + self.watcher = source + + // TODO: reindex the current item, because the files in the item may have changed + // since the initial load on startup. + return true + } + + func childrenDescription(tabCount: Int) -> String { + var myDetails = "\(String(repeating: "| ", count: max(tabCount - 1, 0)))\(tabCount != 0 ? "╰--" : "")" + myDetails += "\(url.path)" + if !self.isFolder { // if im a file, just return the url + return myDetails + } else { // if im a folder, return the url and its children's details + var childDetails = "\(myDetails)" + for child in children ?? [] { + childDetails += "\n\(child.childrenDescription(tabCount: tabCount + 1))" + } + return childDetails + } + } + + /// Returns a string describing a SFSymbol for folders + /// + /// If it is the top-level folder this will return `"square.dashed.inset.filled"`. + /// If it is a `.codeedit` folder this will return `"folder.fill.badge.gearshape"`. + /// If it has children this will return `"folder.fill"` otherwise `"folder"`. + private func folderIcon(_ children: [CEWorkspaceFile]) -> String { + if self.parent == nil { + return "square.dashed.inset.filled" + } + if self.name == ".codeedit" { + return "folder.fill.badge.gearshape" + } + return children.isEmpty ? "folder" : "folder.fill" + } + + /// Returns the file name with optional extension (e.g.: `Package.swift`) + func fileName(typeHidden: Bool) -> String { + typeHidden ? url.deletingPathExtension().lastPathComponent : name + } + + /// Return the file's UTType + var contentType: UTType? { + try? url.resourceValues(forKeys: [.contentTypeKey]).contentType + } + + /// Returns a `Color` for a specific `fileType` + /// + /// If not specified otherwise this will return `Color.accentColor` + var iconColor: Color { + FileIcon.iconColor(fileType: type) + } + + // MARK: Statics + /// The default `FileManager` instance + static let fileManger = FileManager.default + + // MARK: Intents + /// Allows the user to view the file or folder in the finder application + func showInFinder() { + NSWorkspace.shared.activateFileViewerSelecting([url]) + } + + /// Allows the user to launch the file or folder as it would be in finder + func openWithExternalEditor() { + NSWorkspace.shared.open(url) + } + + /// Flattens the children of ``self`` recursively with depth. + /// - Parameters: + /// - depth: An int that indicates the how deep the tree files need to be flattened + /// - ignoringFolders: A boolean on whether to ignore files that are Folders + /// - Returns: An array of flattened `CEWorkspaceFiles` + func flattenedChildren(withDepth depth: Int, ignoringFolders: Bool) -> [CEWorkspaceFile] { + guard depth > 0 else { return [] } + guard self.isFolder else { return [self] } + var childItems: [CEWorkspaceFile] = ignoringFolders ? [] : [self] + children?.forEach { child in + childItems.append(contentsOf: child.flattenedChildren( + withDepth: depth - 1, + ignoringFolders: ignoringFolders + )) + } + return childItems + } + + /// Returns a list of `CEWorkspaceFiles` that are sibilings of ``self``. + /// The `height` parameter lets the function navigate up the folder hierarchy to + /// select a starting point from which it should start flettening the items. + /// - Parameters: + /// - height: `Int` that tells where to start in the hierarchy + /// - ignoringFolders: Wether the sibling folders should be flattened + /// - Returns: A list of `FileSystemItems` + func flattenedSiblings(withHeight height: Int, ignoringFolders: Bool) -> [CEWorkspaceFile] { + let topMostParent = self.getParent(withHeight: height) + return topMostParent.flattenedChildren(withDepth: height, ignoringFolders: ignoringFolders) + } + + /// Using the current instance of `FileSystemItem` it will walk back up the Workspace file hiarchy + /// the amount of times specified with the `withHeight` parameter. + /// - Parameter height: The amount of times you want to up a folder. + /// - Returns: The found `FileSystemItem` object, This should always be a folder. + private func getParent(withHeight height: Int) -> CEWorkspaceFile { + var topmostParent = self + for _ in 0.. Int { + var count = 0 + guard self.isFolder else { return 0 } + for child in self.children ?? [] { + var isIgnored: Bool = false + for ignoredString in ignoredStrings where child.name.hasPrefix(ignoredString) { + isIgnored = true // can use regex later + } + + if isIgnored { + continue + } + + guard !searchString.isEmpty else { count += 1; continue } + if child.isFolder { + count += child.appearanceWithinChildrenOf(searchString: searchString) > 0 ? 1 : 0 + } else { + count += child.name.lowercased().contains(searchString.lowercased()) ? 1 : 0 + } + } + return count + } + + /// Function that returns an array of the children + /// that contain the `searchString` in their path or their subitems' paths. + /// Similar to `appearanceWithinChildrenOf(searchString: String)` + /// Returns `[]` if the item is not a folder. + /// - Parameter searchString: The string + /// - Parameter ignoredStrings: The prefixes to ignore if they prefix file names + /// - Returns: The children that match the conditiions + func childrenSatisfying(searchString: String, ignoredStrings: [String] = [".", "~"]) -> [CEWorkspaceFile] { + var satisfyingChildren: [CEWorkspaceFile] = [] + guard self.isFolder else { return [] } + for child in self.children ?? [] { + var isIgnored: Bool = false + for ignoredString in ignoredStrings where child.name.hasPrefix(ignoredString) { + isIgnored = true // can use regex later + } + + if isIgnored { + continue + } + + guard !searchString.isEmpty else { satisfyingChildren.append(child); continue } + if child.isFolder { + if child.appearanceWithinChildrenOf(searchString: searchString) > 0 { + satisfyingChildren.append(child) + } + } else { + if child.name.lowercased().contains(searchString.lowercased()) { + satisfyingChildren.append(child) + } + } + } + return satisfyingChildren + } + + /// This function allows creation of folders in the main directory or sub-folders + /// - Parameter folderName: The name of the new folder + func addFolder(folderName: String) { + // Check if folder, if it is create folder under self, else create on same level. + var folderUrl = (self.isFolder ? + self.url.appendingPathComponent(folderName) : + self.url.deletingLastPathComponent().appendingPathComponent(folderName)) + + // If a file/folder with the same name exists, add a number to the end. + var fileNumber = 0 + while CEWorkspaceFile.fileManger.fileExists(atPath: folderUrl.path) { + fileNumber += 1 + folderUrl = folderUrl.deletingLastPathComponent().appendingPathComponent("\(folderName)\(fileNumber)") + } + + // Create the folder + do { + try CEWorkspaceFile.fileManger.createDirectory( + at: folderUrl, + withIntermediateDirectories: true, + attributes: [:] + ) + } catch { + fatalError(error.localizedDescription) + } + } + + /// This function allows creating files in the selected folder or project main directory + /// - Parameter fileName: The name of the new file + func addFile(fileName: String) { + // check the folder for other files, and see what the most common file extension is + var fileExtensions: [String: Int] = ["": 0] + + for child in (self.isFolder ? + self.flattenedSiblings(withHeight: 2, ignoringFolders: true) : + parent?.flattenedSiblings(withHeight: 2, ignoringFolders: true)) ?? [] where !child.isFolder { + // if the file extension was present before, add it now + let childFileName = child.fileName(typeHidden: false) + if let index = childFileName.lastIndex(of: ".") { + let childFileExtension = ".\(childFileName.suffix(from: index).dropFirst())" + fileExtensions[childFileExtension] = (fileExtensions[childFileExtension] ?? 0) + 1 + } else { + fileExtensions[""] = (fileExtensions[""] ?? 0) + 1 + } + } + + var largestValue = 0 + var idealExtension = "" + for (extName, count) in fileExtensions where count > largestValue { + idealExtension = extName + largestValue = count + } + + var fileUrl = nearestFolder.appendingPathComponent("\(fileName)\(idealExtension)") + // If a file/folder with the same name exists, add a number to the end. + var fileNumber = 0 + while CEWorkspaceFile.fileManger.fileExists(atPath: fileUrl.path) { + fileNumber += 1 + fileUrl = fileUrl.deletingLastPathComponent() + .appendingPathComponent("\(fileName)\(fileNumber)\(idealExtension)") + } + + // Create the file + CEWorkspaceFile.fileManger.createFile( + atPath: fileUrl.path, + contents: nil, + attributes: [FileAttributeKey.creationDate: Date()] + ) + } + + /// Nearest folder refers to the parent directory if this is a non-folder item, or itself if the item is a folder. + var nearestFolder: URL { + (self.isFolder ? + self.url : + self.url.deletingLastPathComponent()) + } + + /// This function deletes the item or folder from the current project + func delete() { + // This function also has to account for how the + // - file system can change outside of the editor + let deleteConfirmation = NSAlert() + let message = "\(String(describing: self.fileName))\(self.isFolder ? " and its children" : "")" + deleteConfirmation.messageText = "Do you want to move \(message) to the bin?" + deleteConfirmation.alertStyle = .critical + deleteConfirmation.addButton(withTitle: "Delete") + deleteConfirmation.buttons.last?.hasDestructiveAction = true + deleteConfirmation.addButton(withTitle: "Cancel") + if deleteConfirmation.runModal() == .alertFirstButtonReturn { // "Delete" button + if CEWorkspaceFile.fileManger.fileExists(atPath: self.url.path) { + do { + try CEWorkspaceFile.fileManger.removeItem(at: self.url) + } catch { + fatalError(error.localizedDescription) + } + } + } + } + + /// This function duplicates the item or folder + func duplicate() { + // If a file/folder with the same name exists, add "copy" to the end + var fileUrl = self.url + while CEWorkspaceFile.fileManger.fileExists(atPath: fileUrl.path) { + let previousName = fileUrl.lastPathComponent + let fileExtension = fileUrl.pathExtension.isEmpty ? "" : ".\(fileUrl.pathExtension)" + let fileName = fileExtension.isEmpty ? previousName : + previousName.replacingOccurrences(of: ".\(fileExtension)", with: "") + fileUrl = fileUrl.deletingLastPathComponent().appendingPathComponent("\(fileName) copy\(fileExtension)") + } +// Log.info("Duplicating file to \(fileUrl)") + + if CEWorkspaceFile.fileManger.fileExists(atPath: self.url.path) { + do { + try CEWorkspaceFile.fileManger.copyItem(at: self.url, to: fileUrl) + } catch { +// Log.error("Error at \(self.url.path) to \(fileUrl.path)") + fatalError(error.localizedDescription) + } + } + } + + /// This function moves the item or folder if possible + func move(to newLocation: URL) { + guard !CEWorkspaceFile.fileManger.fileExists(atPath: newLocation.path) else { return } + createMissingParentDirectory(for: newLocation.deletingLastPathComponent()) + + do { +// Log.info("Moving file \(self.url.debugDescription) to \(newLocation.debugDescription)") + try CEWorkspaceFile.fileManger.moveItem(at: self.url, to: newLocation) + } catch { fatalError(error.localizedDescription) } + + // This function recursively creates missing directories if the file is moved to a directory that does not exist + func createMissingParentDirectory(for url: URL, createSelf: Bool = true) { + // if the folder's parent folder doesn't exist, create it. + if !CEWorkspaceFile.fileManger.fileExists(atPath: url.deletingLastPathComponent().path) { + createMissingParentDirectory(for: url.deletingLastPathComponent()) + } + // if the folder doesn't exist and the function was ordered to create it, create it. + if createSelf && !CEWorkspaceFile.fileManger.fileExists(atPath: url.path) { +// Log.info("Creating folder \(url.debugDescription)") + // Create the folder + do { + try CEWorkspaceFile.fileManger.createDirectory( + at: url, + withIntermediateDirectories: true, + attributes: [:] + ) + } catch { + fatalError(error.localizedDescription) + } + } + } + } + + // MARK: Comparable + + static func == (lhs: CEWorkspaceFile, rhs: CEWorkspaceFile) -> Bool { + lhs.id == rhs.id + } + + static func < (lhs: CEWorkspaceFile, rhs: CEWorkspaceFile) -> Bool { + lhs.url.lastPathComponent < rhs.url.lastPathComponent + } + + // MARK: Hashable + + func hash(into hasher: inout Hasher) { + hasher.combine(fileIdentifier) + hasher.combine(id) + } + +} diff --git a/CodeEdit/Utils/WorkspaceClient/Model/FileIcon.swift b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileIcon.swift similarity index 100% rename from CodeEdit/Utils/WorkspaceClient/Model/FileIcon.swift rename to CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileIcon.swift diff --git a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager.swift b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager.swift new file mode 100644 index 0000000000..153827160f --- /dev/null +++ b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager.swift @@ -0,0 +1,257 @@ +// +// FileSystemClient.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom on 04/02/2023. +// + +import Combine +import Foundation + +/// This class is used to load the files of the machine into a CodeEdit workspace. +final class CEWorkspaceFileManager { + enum FileSystemClientError: Error { + case fileNotExist + } + + private var subject = CurrentValueSubject<[CEWorkspaceFile], Never>([]) + private var isRunning = false + private var anotherInstanceRan = 0 + + private(set) var fileManager = FileManager.default + private(set) var ignoredFilesAndFolders: [String] + private(set) var flattenedFileItems: [String: CEWorkspaceFile] + + var onRefresh: () -> Void = {} + var getFiles: AnyPublisher<[CEWorkspaceFile], Never> = + CurrentValueSubject<[CEWorkspaceFile], Never>([]).eraseToAnyPublisher() + + let folderUrl: URL + let workspaceItem: CEWorkspaceFile + + init(folderUrl: URL, ignoredFilesAndFolders: [String]) { + self.folderUrl = folderUrl + self.ignoredFilesAndFolders = ignoredFilesAndFolders + + self.workspaceItem = CEWorkspaceFile(url: folderUrl, children: []) + self.flattenedFileItems = [workspaceItem.id: workspaceItem] + + self.setup() + } + + private func setup() { + // initial load + var workspaceFiles: [CEWorkspaceFile] + do { + workspaceFiles = try loadFiles(fromUrl: self.folderUrl) + } catch { + fatalError("Failed to loadFiles") + } + + // workspace fileItem + let workspaceFile = CEWorkspaceFile(url: self.folderUrl, children: workspaceFiles) + flattenedFileItems[workspaceFile.id] = workspaceFile + workspaceFiles.forEach { item in + item.parent = workspaceFile + } + + // By using `CurrentValueSubject` we can define a starting value. + // The value passed during init it's going to be send as soon as the + // consumer subscribes to the publisher. + let subject = CurrentValueSubject<[CEWorkspaceFile], Never>(workspaceFiles) + + self.getFiles = subject + .handleEvents(receiveCancel: { + for item in self.flattenedFileItems.values { + item.watcher?.cancel() + item.watcher = nil + } + }) + .receive(on: RunLoop.main) + .eraseToAnyPublisher() + + workspaceFile.watcherCode = { sourceFileItem in + self.reloadFromWatcher(sourceFileItem: sourceFileItem) + } + reloadFromWatcher(sourceFileItem: workspaceFile) + } + + /// Recursive loading of files into `FileItem`s + /// - Parameter url: The URL of the directory to load the items of + /// - Returns: `[FileItem]` representing the contents of the directory + private func loadFiles(fromUrl url: URL) throws -> [CEWorkspaceFile] { + let directoryContents = try fileManager.contentsOfDirectory( + at: url.resolvingSymlinksInPath(), + includingPropertiesForKeys: nil + ) + var items: [CEWorkspaceFile] = [] + + for itemURL in directoryContents { + guard !ignoredFilesAndFolders.contains(itemURL.lastPathComponent) else { continue } + + var isDir: ObjCBool = false + + if fileManager.fileExists(atPath: itemURL.path, isDirectory: &isDir) { + var subItems: [CEWorkspaceFile]? + + if isDir.boolValue { + // Recursively fetch subdirectories and files if the path points to a directory + subItems = try loadFiles(fromUrl: itemURL) + } + + let newFileItem = CEWorkspaceFile( + url: itemURL, + children: subItems?.sortItems(foldersOnTop: true) + ) + + // note: watcher code will be applied after the workspaceItem is created + newFileItem.watcherCode = { sourceFileItem in + self.reloadFromWatcher(sourceFileItem: sourceFileItem) + } + subItems?.forEach { $0.parent = newFileItem } + items.append(newFileItem) + flattenedFileItems[newFileItem.id] = newFileItem + } + } + + return items + } + + /// A function that, given a file's path, returns a `FileItem` if it exists + /// within the scope of the `FileSystemClient`. + /// - Parameter id: The file's full path + /// - Returns: The file item corresponding to the file + func getFileItem(_ id: String) throws -> CEWorkspaceFile { + guard let item = flattenedFileItems[id] else { + throw FileSystemClientError.fileNotExist + } + + return item + } + + /// Usually run when the owner of the `FileSystemClient` doesn't need it anymore. + /// This de-inits most functions in the `FileSystemClient`, so that in case it isn't de-init'd it does not use up + /// significant amounts of RAM. + func cleanUp() { + stopListeningToDirectory() + workspaceItem.children = [] + flattenedFileItems = [workspaceItem.id: workspaceItem] + print("Cleaned up watchers and file items") + } + + // run by dispatchsource watchers. Multiple instances may be concurrent, + // so we need to be careful to avoid EXC_BAD_ACCESS errors. + /// This is a function run by `DispatchSource` file watchers. Due to the nature of watchers, multiple + /// instances may be running concurrently, so the function prevents more than one instance of it from + /// running the main code body. + /// - Parameter sourceFileItem: The `FileItem` corresponding to the file that triggered the `DispatchSource` + func reloadFromWatcher(sourceFileItem: CEWorkspaceFile) { + // Something has changed inside the directory + // We should reload the files. + guard !isRunning else { // this runs when a file change is detected but is already running + anotherInstanceRan += 1 + return + } + isRunning = true + + // inital reload of files + _ = try? rebuildFiles(fromItem: sourceFileItem) + + // re-reload if another instance tried to run while this instance was running + // TODO: optimise + while anotherInstanceRan > 0 { + let somethingChanged = try? rebuildFiles(fromItem: workspaceItem) + anotherInstanceRan = !(somethingChanged ?? false) ? 0 : anotherInstanceRan - 1 + } + + subject.send(workspaceItem.children ?? []) + isRunning = false + anotherInstanceRan = 0 + + // reload data in outline view controller through the main thread + DispatchQueue.main.async { + self.onRefresh() + } + } + + /// A function to kill the watcher of a specific directory, or all directories. + /// - Parameter directory: The directory to stop watching, or nil to stop watching everything. + func stopListeningToDirectory(directory: URL? = nil) { + if directory != nil { + flattenedFileItems[directory!.relativePath]?.watcher?.cancel() + } else { + for item in flattenedFileItems.values { + item.watcher?.cancel() + item.watcher = nil + } + } + } + + /// Recursive function similar to `loadFiles`, but creates or deletes children of the + /// `FileItem` so that they are accurate with the file system, instead of creating an + /// entirely new `FileItem`, to prevent the `OutlineView` from going crazy with folding. + /// - Parameter fileItem: The `FileItem` to correct the children of + @discardableResult + func rebuildFiles(fromItem fileItem: CEWorkspaceFile) throws -> Bool { + var didChangeSomething = false + + // get the actual directory children + let directoryContentsUrls = try fileManager.contentsOfDirectory( + at: fileItem.url.resolvingSymlinksInPath(), + includingPropertiesForKeys: nil + ) + + // test for deleted children, and remove them from the index + for oldContent in fileItem.children ?? [] where !directoryContentsUrls.contains(oldContent.url) { + if let removeAt = fileItem.children?.firstIndex(of: oldContent) { + fileItem.children?[removeAt].watcher?.cancel() + fileItem.children?.remove(at: removeAt) + flattenedFileItems.removeValue(forKey: oldContent.id) + didChangeSomething = true + } + } + + // test for new children, and index them using loadFiles + for newContent in directoryContentsUrls { + guard !ignoredFilesAndFolders.contains(newContent.lastPathComponent) else { continue } + + // if the child has already been indexed, continue to the next item. + guard !(fileItem.children?.map({ $0.url }).contains(newContent) ?? false) else { continue } + + var isDir: ObjCBool = false + if fileManager.fileExists(atPath: newContent.path, isDirectory: &isDir) { + var subItems: [CEWorkspaceFile]? + + if isDir.boolValue { subItems = try loadFiles(fromUrl: newContent) } + + let newFileItem = CEWorkspaceFile( + url: newContent, + children: subItems?.sortItems(foldersOnTop: true) + ) + + newFileItem.watcherCode = { sourceFileItem in + self.reloadFromWatcher(sourceFileItem: sourceFileItem) + } + + subItems?.forEach { $0.parent = newFileItem } + + newFileItem.parent = fileItem + flattenedFileItems[newFileItem.id] = newFileItem + fileItem.children?.append(newFileItem) + didChangeSomething = true + } + } + + fileItem.children = fileItem.children?.sortItems(foldersOnTop: true) + fileItem.children?.forEach({ + if $0.isFolder { + let childChanged = try? rebuildFiles(fromItem: $0) + didChangeSomething = (childChanged ?? false) ? true : didChangeSomething + } + flattenedFileItems[$0.id] = $0 + }) + + return didChangeSomething + } + +} diff --git a/CodeEdit/Features/CodeEditUI/Views/ToolbarBranchPicker.swift b/CodeEdit/Features/CodeEditUI/Views/ToolbarBranchPicker.swift index fee253e052..d984466572 100644 --- a/CodeEdit/Features/CodeEditUI/Views/ToolbarBranchPicker.swift +++ b/CodeEdit/Features/CodeEditUI/Views/ToolbarBranchPicker.swift @@ -10,7 +10,7 @@ import CodeEditSymbols /// A view that pops up a branch picker. struct ToolbarBranchPicker: View { - private var workspace: WorkspaceClient? + private var workspaceFileManager: CEWorkspaceFileManager? private var gitClient: GitClient? @Environment(\.controlActiveState) @@ -30,10 +30,10 @@ struct ToolbarBranchPicker: View { /// - Parameter workspace: An instance of the current `WorkspaceClient` init( shellClient: ShellClient, - workspace: WorkspaceClient? + workspaceFileManager: CEWorkspaceFileManager? ) { - self.workspace = workspace - if let folderURL = workspace?.folderURL() { + self.workspaceFileManager = workspaceFileManager + if let folderURL = workspaceFileManager?.folderUrl { self.gitClient = GitClient(directoryURL: folderURL, shellClient: shellClient) } self._currentBranch = State(initialValue: try? gitClient?.getCurrentBranchName()) @@ -94,7 +94,7 @@ struct ToolbarBranchPicker: View { } private var title: String { - workspace?.folderURL()?.lastPathComponent ?? "Empty" + workspaceFileManager?.folderUrl.lastPathComponent ?? "Empty" } // MARK: Popover View diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift index ebee724d85..b9a9a0903a 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift @@ -209,7 +209,7 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate { let view = NSHostingView( rootView: ToolbarBranchPicker( shellClient: currentWorld.shellClient, - workspace: workspace?.workspaceClient + workspaceFileManager: workspace?.workspaceFileManager ) ) toolbarItem.view = view diff --git a/CodeEdit/Features/Documents/Views/WorkspaceCodeFileView.swift b/CodeEdit/Features/Documents/Views/WorkspaceCodeFileView.swift index b9df358624..f40d2ca112 100644 --- a/CodeEdit/Features/Documents/Views/WorkspaceCodeFileView.swift +++ b/CodeEdit/Features/Documents/Views/WorkspaceCodeFileView.swift @@ -16,7 +16,7 @@ struct WorkspaceCodeFileView: View { @EnvironmentObject private var tabgroup: TabGroupData - var file: WorkspaceClient.FileItem + var file: CEWorkspaceFile @StateObject private var prefs: AppPreferencesModel = .shared @@ -37,7 +37,7 @@ struct WorkspaceCodeFileView: View { Spacer() VStack(spacing: 10) { ProgressView() - Text("Opening \(file.fileName)...") + Text("Opening \(file.name)...") } Spacer() } @@ -46,7 +46,7 @@ struct WorkspaceCodeFileView: View { @ViewBuilder private func otherFileView( _ otherFile: CodeFileDocument, - for item: WorkspaceClient.FileItem + for item: CEWorkspaceFile ) -> some View { VStack(spacing: 0) { diff --git a/CodeEdit/Features/Documents/WorkspaceDocument+Listeners.swift b/CodeEdit/Features/Documents/WorkspaceDocument+Listeners.swift index 2b4601f176..8d79dca9ff 100644 --- a/CodeEdit/Features/Documents/WorkspaceDocument+Listeners.swift +++ b/CodeEdit/Features/Documents/WorkspaceDocument+Listeners.swift @@ -9,9 +9,12 @@ import Foundation import Combine class WorkspaceNotificationModel: ObservableObject { + + @Published + var highlightedFileItem: CEWorkspaceFile? + init() { highlightedFileItem = nil } - @Published var highlightedFileItem: WorkspaceClient.FileItem? } diff --git a/CodeEdit/Features/Documents/WorkspaceDocument+Search.swift b/CodeEdit/Features/Documents/WorkspaceDocument+Search.swift index 853f08b16b..b3dc53b6ee 100644 --- a/CodeEdit/Features/Documents/WorkspaceDocument+Search.swift +++ b/CodeEdit/Features/Documents/WorkspaceDocument+Search.swift @@ -74,7 +74,7 @@ extension WorkspaceDocument { // - Lazily load strings using `FileHandle.AsyncBytes` // https://developer.apple.com/documentation/foundation/filehandle/3766681-bytes filePaths.map { url in - WorkspaceClient.FileItem(url: url, children: nil) + CEWorkspaceFile(url: url, children: nil) }.forEach { fileItem in guard let data = try? Data(contentsOf: fileItem.url), let string = String(data: data, encoding: .utf8) else { return } diff --git a/CodeEdit/Features/Documents/WorkspaceDocument.swift b/CodeEdit/Features/Documents/WorkspaceDocument.swift index 1fea6babb8..8b85596fa3 100644 --- a/CodeEdit/Features/Documents/WorkspaceDocument.swift +++ b/CodeEdit/Features/Documents/WorkspaceDocument.swift @@ -12,13 +12,19 @@ import Combine import CodeEditKit @objc(WorkspaceDocument) final class WorkspaceDocument: NSDocument, ObservableObject, NSToolbarDelegate { - var workspaceClient: WorkspaceClient? - var extensionNavigatorData = ExtensionNavigatorData() + @Published + var sortFoldersOnTop: Bool = true + + @Published + var fileItems: [CEWorkspaceFile] = [] - @Published var sortFoldersOnTop: Bool = true + @Published + var targets: [Target] = [] - @Published var fileItems: [WorkspaceClient.FileItem] = [] + var workspaceFileManager: CEWorkspaceFileManager? + + var extensionNavigatorData = ExtensionNavigatorData() var tabManager = TabManager() @@ -48,8 +54,6 @@ import CodeEditKit private let activeTabStateName: String = "\(String(describing: WorkspaceDocument.self))-ActiveTab" private var openedTabsFromState = false - @Published var targets: [Target] = [] - deinit { cancellables.forEach { $0.cancel() } NotificationCenter.default.removeObserver(self) @@ -116,9 +120,13 @@ import CodeEditKit // MARK: Set Up Workspace private func initWorkspaceState(_ url: URL) throws { - self.workspaceClient = try .default( - fileManager: .default, - folderURL: url, +// self.workspaceClient = try .default( +// fileManager: .default, +// folderURL: url, +// ignoredFilesAndFolders: ignoredFilesAndDirectory +// ) + self.workspaceFileManager = .init( + folderUrl: url, ignoredFilesAndFolders: ignoredFilesAndDirectory ) self.searchState = .init(self) @@ -130,7 +138,7 @@ import CodeEditKit try initWorkspaceState(url) // Initialize Workspace - workspaceClient? + workspaceFileManager? .getFiles .sink { [weak self] files in guard let self = self else { return } diff --git a/CodeEdit/Features/ExtensionsStore/Models/Plugin.swift b/CodeEdit/Features/ExtensionsStore/Models/Plugin.swift index 5b8ec6b565..6ced0b145c 100644 --- a/CodeEdit/Features/ExtensionsStore/Models/Plugin.swift +++ b/CodeEdit/Features/ExtensionsStore/Models/Plugin.swift @@ -14,7 +14,7 @@ struct Plugin: Codable, Identifiable, Hashable, TabBarItemRepresentable { .extensionInstallation(self.id) } - var title: String { + var name: String { self.manifest.displayName } diff --git a/CodeEdit/Features/NavigatorSidebar/FindNavigator/FindNavigatorResultList/FindNavigatorListViewController.swift b/CodeEdit/Features/NavigatorSidebar/FindNavigator/FindNavigatorResultList/FindNavigatorListViewController.swift index 6668c7b4f6..e34ee570d6 100644 --- a/CodeEdit/Features/NavigatorSidebar/FindNavigator/FindNavigatorResultList/FindNavigatorListViewController.swift +++ b/CodeEdit/Features/NavigatorSidebar/FindNavigator/FindNavigatorResultList/FindNavigatorListViewController.swift @@ -12,7 +12,6 @@ final class FindNavigatorListViewController: NSViewController { public var workspace: WorkspaceDocument public var selectedItem: Any? - typealias FileItem = WorkspaceClient.FileItem private var searchId: UUID? private var searchItems: [SearchResultModel] = [] private var scrollView: NSScrollView! diff --git a/CodeEdit/Features/NavigatorSidebar/NavigatorSidebarToolbarBottom.swift b/CodeEdit/Features/NavigatorSidebar/NavigatorSidebarToolbarBottom.swift index 8c698c3a49..9a83d6bea1 100644 --- a/CodeEdit/Features/NavigatorSidebar/NavigatorSidebarToolbarBottom.swift +++ b/CodeEdit/Features/NavigatorSidebar/NavigatorSidebarToolbarBottom.swift @@ -31,22 +31,18 @@ struct NavigatorSidebarToolbarBottom: View { private var addNewFileButton: some View { Menu { Button("Add File") { - guard let folderURL = workspace.workspaceClient?.folderURL() else { return } - guard let root = try? workspace.workspaceClient?.getFileItem(folderURL.path) else { return } - let newFile = root.addFile(fileName: "untitled") // TODO: use currently selected file instead of root - - DispatchQueue.main.async { - guard let newFileItem = try? workspace.workspaceClient?.getFileItem(newFile) else { - return - } - workspace.tabManager.openTab(item: newFileItem) - } + guard let folderURL = workspace.workspaceFileManager?.folderUrl, + let root = try? workspace.workspaceFileManager?.getFileItem(folderURL.path) else { return } + // TODO: use currently selected file instead of root + root.addFile(fileName: "untitled") } Button("Add Folder") { - guard let folderURL = workspace.workspaceClient?.folderURL() else { return } - guard let root = try? workspace.workspaceClient?.getFileItem(folderURL.path) else { return } - root.addFolder(folderName: "untitled") // TODO: use currently selected file instead of root + guard let folderURL = workspace.workspaceFileManager?.folderUrl, + let root = try? workspace.workspaceFileManager?.getFileItem(folderURL.path) else { return } + + // TODO: use currently selected file instead of root + root.addFolder(folderName: "untitled") } } label: { Image(systemName: "plus") diff --git a/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/OutlineMenu.swift b/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/OutlineMenu.swift index 1bd682d820..412f4abab6 100644 --- a/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/OutlineMenu.swift +++ b/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/OutlineMenu.swift @@ -10,10 +10,9 @@ import UniformTypeIdentifiers /// A subclass of `NSMenu` implementing the contextual menu for the project navigator final class OutlineMenu: NSMenu { - typealias Item = WorkspaceClient.FileItem /// The item to show the contextual menu for - var item: Item? + var item: CEWorkspaceFile? /// The workspace, for opening the item var workspace: WorkspaceDocument? @@ -61,7 +60,7 @@ final class OutlineMenu: NSMenu { let rename = menuItem("Rename", action: #selector(renameFile)) let delete = menuItem("Delete", action: - item.url != workspace?.workspaceClient?.folderURL() + item.url != workspace?.workspaceFileManager?.folderUrl ? #selector(delete) : nil) let duplicate = menuItem("Duplicate \(item.isFolder ? "Folder" : "File")", action: #selector(duplicate)) @@ -102,7 +101,7 @@ final class OutlineMenu: NSMenu { } /// Submenu for **Open As** menu item. - private func openAsMenu(item: Item) -> NSMenu { + private func openAsMenu(item: CEWorkspaceFile) -> NSMenu { let openAsMenu = NSMenu(title: "Open As") func getMenusItems() -> ([NSMenuItem], [NSMenuItem]) { // Use UTType to distinguish between bundle file and user-browsable directory @@ -154,7 +153,7 @@ final class OutlineMenu: NSMenu { } /// Submenu for **Source Control** menu item. - private func sourceControlMenu(item: Item) -> NSMenu { + private func sourceControlMenu(item: CEWorkspaceFile) -> NSMenu { let sourceControlMenu = NSMenu(title: "Source Control") sourceControlMenu.addItem(withTitle: "Commit \"\(item.fileName)\"...", action: nil, keyEquivalent: "") sourceControlMenu.addItem(.separator()) diff --git a/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/OutlineTableViewCell.swift b/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/OutlineTableViewCell.swift index 06cbd880c0..478cf66137 100644 --- a/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/OutlineTableViewCell.swift +++ b/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/OutlineTableViewCell.swift @@ -8,8 +8,8 @@ import SwiftUI protocol OutlineTableViewCellDelegate: AnyObject { - func moveFile(file: WorkspaceClient.FileItem, to destination: URL) - func copyFile(file: WorkspaceClient.FileItem, to destination: URL) + func moveFile(file: CEWorkspaceFile, to destination: URL) + func copyFile(file: CEWorkspaceFile, to destination: URL) } /// A `NSTableCellView` showing an ``icon`` and a ``label`` @@ -17,7 +17,7 @@ final class OutlineTableViewCell: NSTableCellView { var label: NSTextField! var icon: NSImageView! - private var fileItem: WorkspaceClient.FileItem! + private var fileItem: CEWorkspaceFile! private var delegate: OutlineTableViewCellDelegate? private let prefs = AppPreferencesModel.shared.preferences.general @@ -29,7 +29,7 @@ final class OutlineTableViewCell: NSTableCellView { /// - item: The file item the cell represents. /// - isEditable: Set to true if the user should be able to edit the file name. init( - frame frameRect: NSRect, item: WorkspaceClient.FileItem?, + frame frameRect: NSRect, item: CEWorkspaceFile?, isEditable: Bool = true, delegate: OutlineTableViewCellDelegate? = nil ) { @@ -115,23 +115,23 @@ final class OutlineTableViewCell: NSTableCellView { /// Generates a string based on user's file name preferences. /// - Parameter item: The FileItem to generate the name for. /// - Returns: A `String` with the name to display. - private func label(for item: WorkspaceClient.FileItem) -> String { + private func label(for item: CEWorkspaceFile) -> String { switch prefs.fileExtensionsVisibility { case .hideAll: return item.fileName(typeHidden: true) case .showAll: return item.fileName(typeHidden: false) case .showOnly: - return item.fileName(typeHidden: !prefs.shownFileExtensions.extensions.contains(item.fileType.rawValue)) + return item.fileName(typeHidden: !prefs.shownFileExtensions.extensions.contains(item.type.rawValue)) case .hideOnly: - return item.fileName(typeHidden: prefs.hiddenFileExtensions.extensions.contains(item.fileType.rawValue)) + return item.fileName(typeHidden: prefs.hiddenFileExtensions.extensions.contains(item.type.rawValue)) } } /// Get the appropriate color for the items icon depending on the users preferences. /// - Parameter item: The `FileItem` to get the color for /// - Returns: A `NSColor` for the given `FileItem`. - private func color(for item: WorkspaceClient.FileItem) -> NSColor { + private func color(for item: CEWorkspaceFile) -> NSColor { return prefs.fileIconStyle == .color ? item.children == nil ? NSColor(item.iconColor) : NSColor(named: "FolderBlue")! : .secondaryLabelColor @@ -155,15 +155,15 @@ extension OutlineTableViewCell: NSTextFieldDelegate { .appendingPathComponent(label?.stringValue ?? "") delegate?.moveFile(file: fileItem, to: destinationURL) } else { - label?.stringValue = fileItem.fileName + label?.stringValue = fileItem.name } } func validateFileName(for newName: String) -> Bool { - guard newName != fileItem.fileName else { return true } + guard newName != fileItem.name else { return true } guard newName != "" && newName.isValidFilename && - !WorkspaceClient.FileItem.fileManger.fileExists(atPath: + !CEWorkspaceFile.fileManger.fileExists(atPath: fileItem.url.deletingLastPathComponent().appendingPathComponent(newName).path) else { return false } diff --git a/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/OutlineView.swift b/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/OutlineView.swift index 606b41e620..90a31828e0 100644 --- a/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/OutlineView.swift +++ b/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/OutlineView.swift @@ -19,7 +19,7 @@ struct OutlineView: NSViewControllerRepresentable { // This is mainly just used to trigger a view update. @Binding - var selection: WorkspaceClient.FileItem? + var selection: CEWorkspaceFile? typealias NSViewControllerType = OutlineViewController @@ -54,7 +54,7 @@ struct OutlineView: NSViewControllerRepresentable { listener = workspace.listenerModel.$highlightedFileItem .sink(receiveValue: { [weak self] fileItem in - guard let fileItem = fileItem else { + guard let fileItem else { return } self?.controller?.reveal(fileItem) diff --git a/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/OutlineViewController.swift b/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/OutlineViewController.swift index 2bce580bd5..6462e08360 100644 --- a/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/OutlineViewController.swift +++ b/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/OutlineViewController.swift @@ -13,18 +13,16 @@ import SwiftUI /// currently open project. final class OutlineViewController: NSViewController { - typealias Item = WorkspaceClient.FileItem - var scrollView: NSScrollView! var outlineView: NSOutlineView! /// Gets the folder structure /// /// Also creates a top level item "root" which represents the projects root directory and automatically expands it. - private var content: [Item] { - guard let folderURL = workspace?.workspaceClient?.folderURL() else { return [] } + private var content: [CEWorkspaceFile] { + guard let folderURL = workspace?.workspaceFileManager?.folderUrl else { return [] } let children = workspace?.fileItems.sortItems(foldersOnTop: true) - guard let root = try? workspace?.workspaceClient?.getFileItem(folderURL.path) else { return [] } + guard let root = try? workspace?.workspaceFileManager?.getFileItem(folderURL.path) else { return [] } root.children = children return [root] } @@ -58,7 +56,7 @@ final class OutlineViewController: NSViewController { self.outlineView.dataSource = self self.outlineView.delegate = self self.outlineView.autosaveExpandedItems = true - self.outlineView.autosaveName = workspace?.workspaceClient?.folderURL()?.path ?? "" + self.outlineView.autosaveName = workspace?.workspaceFileManager?.folderUrl.path ?? "" self.outlineView.headerView = nil self.outlineView.menu = OutlineMenu(sender: self.outlineView) self.outlineView.menu?.delegate = self @@ -75,7 +73,8 @@ final class OutlineViewController: NSViewController { self.scrollView.contentView.automaticallyAdjustsContentInsets = false self.scrollView.contentView.contentInsets = .init(top: 10, left: 0, bottom: 0, right: 0) - WorkspaceClient.onRefresh = self.outlineView.reloadData + // TODO: Kai needs to replace this with his implementation of the sidebar +// WorkspaceClient.onRefresh = self.outlineView.reloadData outlineView.expandItem(outlineView.item(atRow: 0)) } @@ -102,7 +101,7 @@ final class OutlineViewController: NSViewController { /// Expand or collapse the folder on double click @objc private func onItemDoubleClicked() { - guard let item = outlineView.item(atRow: outlineView.clickedRow) as? Item else { return } + guard let item = outlineView.item(atRow: outlineView.clickedRow) as? CEWorkspaceFile else { return } if item.children != nil { if outlineView.isItemExpanded(item) { @@ -118,7 +117,7 @@ final class OutlineViewController: NSViewController { /// Get the appropriate color for the items icon depending on the users preferences. /// - Parameter item: The `FileItem` to get the color for /// - Returns: A `NSColor` for the given `FileItem`. - private func color(for item: Item) -> NSColor { + private func color(for item: CEWorkspaceFile) -> NSColor { if item.children == nil && iconColor == .color { return NSColor(item.iconColor) } else { @@ -132,14 +131,14 @@ final class OutlineViewController: NSViewController { extension OutlineViewController: NSOutlineViewDataSource { func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int { - if let item = item as? Item { + if let item = item as? CEWorkspaceFile { return item.children?.count ?? 0 } return content.count } func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any { - if let item = item as? Item, + if let item = item as? CEWorkspaceFile, let children = item.children { return children[index] } @@ -147,7 +146,7 @@ extension OutlineViewController: NSOutlineViewDataSource { } func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: Any) -> Bool { - if let item = item as? Item { + if let item = item as? CEWorkspaceFile { return item.children != nil } return false @@ -155,7 +154,7 @@ extension OutlineViewController: NSOutlineViewDataSource { /// write dragged file(s) to pasteboard func outlineView(_ outlineView: NSOutlineView, pasteboardWriterForItem item: Any) -> NSPasteboardWriting? { - guard let fileItem = item as? Item else { return nil } + guard let fileItem = item as? CEWorkspaceFile else { return nil } return fileItem.url as NSURL } @@ -166,7 +165,7 @@ extension OutlineViewController: NSOutlineViewDataSource { proposedItem item: Any?, proposedChildIndex index: Int ) -> NSDragOperation { - guard let fileItem = item as? Item else { return [] } + 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 { @@ -187,7 +186,7 @@ extension OutlineViewController: NSOutlineViewDataSource { guard let pasteboardItems = info.draggingPasteboard.readObjects(forClasses: [NSURL.self]) else { return false } let fileItemURLS = pasteboardItems.compactMap { $0 as? URL } - guard let fileItemDestination = item as? Item else { return false } + guard let fileItemDestination = item as? CEWorkspaceFile else { return false } let destParentURL = fileItemDestination.url for fileItemURL in fileItemURLS { @@ -198,23 +197,23 @@ extension OutlineViewController: NSOutlineViewDataSource { } // Needs to come before call to .removeItem or else race condition occurs - var srcFileItem: WorkspaceClient.FileItem? = try? workspace?.workspaceClient?.getFileItem(fileItemURL.path) + var srcFileItem: CEWorkspaceFile? = try? workspace?.workspaceFileManager?.getFileItem(fileItemURL.path) // If srcFileItem is nil, fileItemUrl is an external file url. if srcFileItem == nil { - srcFileItem = WorkspaceClient.FileItem(url: URL(fileURLWithPath: fileItemURL.path)) + srcFileItem = CEWorkspaceFile(url: URL(fileURLWithPath: fileItemURL.path)) } guard let srcFileItem else { return false } - if WorkspaceClient.FileItem.fileManger.fileExists(atPath: destURL.path) { + if CEWorkspaceFile.fileManger.fileExists(atPath: destURL.path) { let shouldReplace = replaceFileDialog(fileName: fileItemURL.lastPathComponent) guard shouldReplace else { return false } do { - try WorkspaceClient.FileItem.fileManger.removeItem(at: destURL) + try CEWorkspaceFile.fileManger.removeItem(at: destURL) } catch { fatalError(error.localizedDescription) } @@ -261,7 +260,7 @@ extension OutlineViewController: NSOutlineViewDelegate { let frameRect = NSRect(x: 0, y: 0, width: tableColumn.width, height: rowHeight) - return OutlineTableViewCell(frame: frameRect, item: item as? Item, delegate: self) + return OutlineTableViewCell(frame: frameRect, item: item as? CEWorkspaceFile, delegate: self) } func outlineViewSelectionDidChange(_ notification: Notification) { @@ -271,7 +270,7 @@ extension OutlineViewController: NSOutlineViewDelegate { let selectedIndex = outlineView.selectedRow - guard let item = outlineView.item(atRow: selectedIndex) as? Item else { return } + guard let item = outlineView.item(atRow: selectedIndex) as? CEWorkspaceFile else { return } if item.children == nil && shouldSendSelectionUpdate { workspace?.tabManager.activeTabGroup.openTab(item: item, asTemporary: true) @@ -287,13 +286,13 @@ extension OutlineViewController: NSOutlineViewDelegate { func outlineViewItemDidCollapse(_ notification: Notification) {} func outlineView(_ outlineView: NSOutlineView, itemForPersistentObject object: Any) -> Any? { - guard let id = object as? Item.ID, - let item = try? workspace?.workspaceClient?.getFileItem(id) else { return nil } + guard let id = object as? CEWorkspaceFile.ID, + let item = try? workspace?.workspaceFileManager?.getFileItem(id) else { return nil } return item } func outlineView(_ outlineView: NSOutlineView, persistentObjectForItem item: Any?) -> Any? { - guard let item = item as? Item else { return nil } + guard let item = item as? CEWorkspaceFile else { return nil } return item.id } @@ -301,12 +300,12 @@ extension OutlineViewController: NSOutlineViewDelegate { /// - Parameters: /// - id: the id of the item item /// - collection: the array to search for - private func select(by id: TabBarItemID, from collection: [Item]) { + private func select(by id: TabBarItemID, from collection: [CEWorkspaceFile]) { // If the user has set "Reveal file on selection change" to on, we need to reveal the item before // selecting the row. if AppPreferencesModel.shared.preferences.general.revealFileOnFocusChange, case let .codeEditor(id) = id, - let fileItem = try? workspace?.workspaceClient?.getFileItem(id as Item.ID) as? Item { + let fileItem = try? workspace?.workspaceFileManager?.getFileItem(id as CEWorkspaceFile.ID) as? CEWorkspaceFile { reveal(fileItem) } @@ -328,7 +327,7 @@ extension OutlineViewController: NSOutlineViewDelegate { /// 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: Item) { + public func reveal(_ fileItem: CEWorkspaceFile) { if let parent = fileItem.parent { expandParent(item: parent) } @@ -350,8 +349,8 @@ extension OutlineViewController: NSOutlineViewDelegate { /// Method for recursively expanding a file's parent directories. /// - Parameter item: - private func expandParent(item: Item) { - if let parent = item.parent as Item? { + private func expandParent(item: CEWorkspaceFile) { + if let parent = item.parent as CEWorkspaceFile? { expandParent(item: parent) } outlineView.expandItem(item) @@ -373,7 +372,7 @@ extension OutlineViewController: NSMenuDelegate { if row == -1 { menu.item = nil } else { - if let item = outlineView.item(atRow: row) as? Item { + if let item = outlineView.item(atRow: row) as? CEWorkspaceFile { menu.item = item menu.workspace = workspace } else { diff --git a/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/OutlintViewController+OutlineTableViewCellDelegate.swift b/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/OutlintViewController+OutlineTableViewCellDelegate.swift index 9fc423f3f5..911067010e 100644 --- a/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/OutlintViewController+OutlineTableViewCellDelegate.swift +++ b/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/OutlintViewController+OutlineTableViewCellDelegate.swift @@ -10,7 +10,7 @@ import Foundation // MARK: - OutlineTableViewCellDelegate extension OutlineViewController: OutlineTableViewCellDelegate { - func moveFile(file: Item, to destination: URL) { + func moveFile(file: CEWorkspaceFile, to destination: URL) { if !file.isFolder { workspace?.tabManager.tabGroups.closeAllTabs(of: file) } @@ -20,7 +20,7 @@ extension OutlineViewController: OutlineTableViewCellDelegate { } } - func copyFile(file: WorkspaceClient.FileItem, to destination: URL) { - file.duplicate(to: destination) + func copyFile(file: CEWorkspaceFile, to destination: URL) { + file.duplicate() } } diff --git a/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/ProjectNavigatorView.swift b/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/ProjectNavigatorView.swift index 3466bc7913..9e8791a3de 100644 --- a/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/ProjectNavigatorView.swift +++ b/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/ProjectNavigatorView.swift @@ -16,9 +16,11 @@ import SwiftUI /// struct ProjectNavigatorView: View { - @EnvironmentObject var tabManager: TabManager + @EnvironmentObject + var tabManager: TabManager var body: some View { OutlineView(selection: $tabManager.activeTabGroup.selected) } + } diff --git a/CodeEdit/Features/PathBar/Views/PathBarComponent.swift b/CodeEdit/Features/PathBar/Views/PathBarComponent.swift index bbe3cd0afa..f1b2a1c079 100644 --- a/CodeEdit/Features/PathBar/Views/PathBarComponent.swift +++ b/CodeEdit/Features/PathBar/Views/PathBarComponent.swift @@ -10,8 +10,8 @@ import Combine struct PathBarComponent: View { - private let fileItem: WorkspaceClient.FileItem - private let tappedOpenFile: (WorkspaceClient.FileItem) -> Void + private let fileItem: CEWorkspaceFile + private let tappedOpenFile: (CEWorkspaceFile) -> Void @Environment(\.colorScheme) var colorScheme @@ -26,18 +26,18 @@ struct PathBarComponent: View { var position: NSPoint? @State - var selection: WorkspaceClient.FileItem + var selection: CEWorkspaceFile init( - fileItem: WorkspaceClient.FileItem, - tappedOpenFile: @escaping (WorkspaceClient.FileItem) -> Void + fileItem: CEWorkspaceFile, + tappedOpenFile: @escaping (CEWorkspaceFile) -> Void ) { self.fileItem = fileItem self._selection = .init(wrappedValue: fileItem) self.tappedOpenFile = tappedOpenFile } - var siblings: [WorkspaceClient.FileItem] { + var siblings: [CEWorkspaceFile] { if let siblings = fileItem.parent?.children?.sortItems(foldersOnTop: true), !siblings.isEmpty { return siblings } else { diff --git a/CodeEdit/Features/PathBar/Views/PathBarMenu.swift b/CodeEdit/Features/PathBar/Views/PathBarMenu.swift index 474901c895..76da34bba6 100644 --- a/CodeEdit/Features/PathBar/Views/PathBarMenu.swift +++ b/CodeEdit/Features/PathBar/Views/PathBarMenu.swift @@ -8,12 +8,12 @@ import AppKit final class PathBarMenu: NSMenu, NSMenuDelegate { - private let fileItems: [WorkspaceClient.FileItem] - private let tappedOpenFile: (WorkspaceClient.FileItem) -> Void + private let fileItems: [CEWorkspaceFile] + private let tappedOpenFile: (CEWorkspaceFile) -> Void init( - fileItems: [WorkspaceClient.FileItem], - tappedOpenFile: @escaping (WorkspaceClient.FileItem) -> Void + fileItems: [CEWorkspaceFile], + tappedOpenFile: @escaping (CEWorkspaceFile) -> Void ) { self.fileItems = fileItems self.tappedOpenFile = tappedOpenFile @@ -38,13 +38,13 @@ final class PathBarMenu: NSMenu, NSMenuDelegate { /// Only when menu item is highlighted then generate its submenu func menu(_: NSMenu, willHighlight item: NSMenuItem?) { if let highlightedItem = item, let submenuItems = highlightedItem.submenu?.items, submenuItems.isEmpty { - if let highlightedFileItem = highlightedItem.representedObject as? WorkspaceClient.FileItem { + if let highlightedFileItem = highlightedItem.representedObject as? CEWorkspaceFile { highlightedItem.submenu = generateSubmenu(highlightedFileItem) } } } - private func generateSubmenu(_ fileItem: WorkspaceClient.FileItem) -> PathBarMenu? { + private func generateSubmenu(_ fileItem: CEWorkspaceFile) -> PathBarMenu? { if let children = fileItem.children { let menu = PathBarMenu( fileItems: children, @@ -57,16 +57,16 @@ final class PathBarMenu: NSMenu, NSMenuDelegate { } final class PathBarMenuItem: NSMenuItem { - private let fileItem: WorkspaceClient.FileItem - private let tappedOpenFile: (WorkspaceClient.FileItem) -> Void + private let fileItem: CEWorkspaceFile + private let tappedOpenFile: (CEWorkspaceFile) -> Void init( - fileItem: WorkspaceClient.FileItem, - tappedOpenFile: @escaping (WorkspaceClient.FileItem) -> Void + fileItem: CEWorkspaceFile, + tappedOpenFile: @escaping (CEWorkspaceFile) -> Void ) { self.fileItem = fileItem self.tappedOpenFile = tappedOpenFile - super.init(title: fileItem.fileName, action: #selector(openFile), keyEquivalent: "") + super.init(title: fileItem.name, action: #selector(openFile), keyEquivalent: "") var icon = fileItem.systemImage var color = NSColor(fileItem.iconColor) diff --git a/CodeEdit/Features/PathBar/Views/PathBarView.swift b/CodeEdit/Features/PathBar/Views/PathBarView.swift index 6702da4ab7..0576c9a02c 100644 --- a/CodeEdit/Features/PathBar/Views/PathBarView.swift +++ b/CodeEdit/Features/PathBar/Views/PathBarView.swift @@ -9,8 +9,8 @@ import SwiftUI struct PathBarView: View { - private let file: WorkspaceClient.FileItem - private let tappedOpenFile: (WorkspaceClient.FileItem) -> Void + private let file: CEWorkspaceFile + private let tappedOpenFile: (CEWorkspaceFile) -> Void @Environment(\.colorScheme) private var colorScheme @@ -24,16 +24,16 @@ struct PathBarView: View { static let height = 27.0 init( - file: WorkspaceClient.FileItem, - tappedOpenFile: @escaping (WorkspaceClient.FileItem) -> Void + file: CEWorkspaceFile, + tappedOpenFile: @escaping (CEWorkspaceFile) -> Void ) { self.file = file self.tappedOpenFile = tappedOpenFile } - var fileItems: [WorkspaceClient.FileItem] { - var treePath: [WorkspaceClient.FileItem] = [] - var currentFile: WorkspaceClient.FileItem? = file + var fileItems: [CEWorkspaceFile] { + var treePath: [CEWorkspaceFile] = [] + var currentFile: CEWorkspaceFile? = file while let currentFileLoop = currentFile { treePath.insert(currentFileLoop, at: 0) diff --git a/CodeEdit/Features/QuickOpen/ViewModels/QuickOpenViewModel.swift b/CodeEdit/Features/QuickOpen/ViewModels/QuickOpenViewModel.swift index d0fc13e6b7..3915000f8d 100644 --- a/CodeEdit/Features/QuickOpen/ViewModels/QuickOpenViewModel.swift +++ b/CodeEdit/Features/QuickOpen/ViewModels/QuickOpenViewModel.swift @@ -14,7 +14,7 @@ final class QuickOpenViewModel: ObservableObject { var openQuicklyQuery: String = "" @Published - var openQuicklyFiles: [WorkspaceClient.FileItem] = [] + var openQuicklyFiles: [CEWorkspaceFile] = [] @Published var isShowingOpenQuicklyFiles: Bool = false @@ -56,7 +56,7 @@ final class QuickOpenViewModel: ObservableObject { return false } }.map { url in - WorkspaceClient.FileItem(url: url, children: nil) + CEWorkspaceFile(url: url, children: nil) } DispatchQueue.main.async { self.openQuicklyFiles = files diff --git a/CodeEdit/Features/QuickOpen/Views/QuickOpenItem.swift b/CodeEdit/Features/QuickOpen/Views/QuickOpenItem.swift index bf2843afe1..f75ab89b50 100644 --- a/CodeEdit/Features/QuickOpen/Views/QuickOpenItem.swift +++ b/CodeEdit/Features/QuickOpen/Views/QuickOpenItem.swift @@ -10,11 +10,11 @@ import SwiftUI struct QuickOpenItem: View { private let baseDirectory: URL - private let fileItem: WorkspaceClient.FileItem + private let fileItem: CEWorkspaceFile init( baseDirectory: URL, - fileItem: WorkspaceClient.FileItem + fileItem: CEWorkspaceFile ) { self.baseDirectory = baseDirectory self.fileItem = fileItem diff --git a/CodeEdit/Features/QuickOpen/Views/QuickOpenPreviewView.swift b/CodeEdit/Features/QuickOpen/Views/QuickOpenPreviewView.swift index 6de0a18a3c..e2303d902f 100644 --- a/CodeEdit/Features/QuickOpen/Views/QuickOpenPreviewView.swift +++ b/CodeEdit/Features/QuickOpen/Views/QuickOpenPreviewView.swift @@ -10,7 +10,7 @@ import SwiftUI struct QuickOpenPreviewView: View { private let queue = DispatchQueue(label: "austincondiff.CodeEdit.quickOpen.preview") - private let item: WorkspaceClient.FileItem + private let item: CEWorkspaceFile @State private var content: String = "" @@ -22,7 +22,7 @@ struct QuickOpenPreviewView: View { private var error: String? init( - item: WorkspaceClient.FileItem + item: CEWorkspaceFile ) { self.item = item } diff --git a/CodeEdit/Features/QuickOpen/Views/QuickOpenView.swift b/CodeEdit/Features/QuickOpen/Views/QuickOpenView.swift index 40138b445f..839a46aeea 100644 --- a/CodeEdit/Features/QuickOpen/Views/QuickOpenView.swift +++ b/CodeEdit/Features/QuickOpen/Views/QuickOpenView.swift @@ -10,18 +10,18 @@ import SwiftUI struct QuickOpenView: View { private let onClose: () -> Void - private let openFile: (WorkspaceClient.FileItem) -> Void + private let openFile: (CEWorkspaceFile) -> Void @ObservedObject private var state: QuickOpenViewModel @State - private var selectedItem: WorkspaceClient.FileItem? + private var selectedItem: CEWorkspaceFile? init( state: QuickOpenViewModel, onClose: @escaping () -> Void, - openFile: @escaping (WorkspaceClient.FileItem) -> Void + openFile: @escaping (CEWorkspaceFile) -> Void ) { self.state = state self.onClose = onClose diff --git a/CodeEdit/Features/Search/Model/SearchResultMatchModel.swift b/CodeEdit/Features/Search/Model/SearchResultMatchModel.swift index fb89fa29b4..b582e64713 100644 --- a/CodeEdit/Features/Search/Model/SearchResultMatchModel.swift +++ b/CodeEdit/Features/Search/Model/SearchResultMatchModel.swift @@ -12,7 +12,7 @@ import Cocoa class SearchResultMatchModel: Hashable, Identifiable { init( lineNumber: Int, - file: WorkspaceClient.FileItem, + file: CEWorkspaceFile, lineContent: String, keywordRange: Range ) { @@ -24,7 +24,7 @@ class SearchResultMatchModel: Hashable, Identifiable { } var id: UUID - var file: WorkspaceClient.FileItem + var file: CEWorkspaceFile var lineNumber: Int var lineContent: String var keywordRange: Range diff --git a/CodeEdit/Features/Search/Model/SearchResultModel.swift b/CodeEdit/Features/Search/Model/SearchResultModel.swift index 4c42d435d0..84dc43251a 100644 --- a/CodeEdit/Features/Search/Model/SearchResultModel.swift +++ b/CodeEdit/Features/Search/Model/SearchResultModel.swift @@ -9,11 +9,12 @@ import Foundation /// A struct for holding information about a file and any matches it may have for a search query. class SearchResultModel: Hashable { - var file: WorkspaceClient.FileItem + + var file: CEWorkspaceFile var lineMatches: [SearchResultMatchModel] init( - file: WorkspaceClient.FileItem, + file: CEWorkspaceFile, lineMatches: [SearchResultMatchModel] = [] ) { self.file = file @@ -29,4 +30,5 @@ class SearchResultModel: Hashable { hasher.combine(file) hasher.combine(lineMatches) } + } diff --git a/CodeEdit/Features/StatusBar/Views/StatusBarDrawer/StatusBarDrawer.swift b/CodeEdit/Features/StatusBar/Views/StatusBarDrawer/StatusBarDrawer.swift index c2add2fd03..3652948179 100644 --- a/CodeEdit/Features/StatusBar/Views/StatusBarDrawer/StatusBarDrawer.swift +++ b/CodeEdit/Features/StatusBar/Views/StatusBarDrawer/StatusBarDrawer.swift @@ -18,7 +18,7 @@ struct StatusBarDrawer: View { private var searchText = "" var body: some View { - if let url = workspace.workspaceClient?.folderURL() { + if let url = workspace.workspaceFileManager?.folderUrl { VStack(spacing: 0) { TerminalEmulatorView(url: url) HStack(alignment: .center, spacing: 10) { diff --git a/CodeEdit/Features/Tabs/Models/TabBarItemRepresentable.swift b/CodeEdit/Features/Tabs/Models/TabBarItemRepresentable.swift index cfbe232412..4c7ca8a08c 100644 --- a/CodeEdit/Features/Tabs/Models/TabBarItemRepresentable.swift +++ b/CodeEdit/Features/Tabs/Models/TabBarItemRepresentable.swift @@ -12,7 +12,7 @@ protocol TabBarItemRepresentable { /// Unique tab identifier var tabID: TabBarItemID { get } /// String to be shown as tab's title - var title: String { get } + var name: String { get } /// Image to be shown as tab's icon var icon: Image { get } /// Color of the tab's icon diff --git a/CodeEdit/Features/Tabs/Models/TabManager.swift b/CodeEdit/Features/Tabs/Models/TabManager.swift index 8ff36c76ac..964b4ed6e3 100644 --- a/CodeEdit/Features/Tabs/Models/TabManager.swift +++ b/CodeEdit/Features/Tabs/Models/TabManager.swift @@ -11,10 +11,12 @@ import DequeModule class TabManager: ObservableObject { /// Collection of all the tabgroups. - @Published var tabGroups: TabGroup + @Published + var tabGroups: TabGroup /// The TabGroup with active focus. - @Published var activeTabGroup: TabGroupData { + @Published + var activeTabGroup: TabGroupData { didSet { activeTabGroupHistory.prepend { [weak oldValue] in oldValue } } @@ -23,7 +25,7 @@ class TabManager: ObservableObject { /// History of last-used tab groups. var activeTabGroupHistory: Deque<() -> TabGroupData?> = [] - var fileDocuments: [WorkspaceClient.FileItem: CodeFileDocument] = [:] + var fileDocuments: [CEWorkspaceFile: CodeFileDocument] = [:] init() { let tab = TabGroupData() @@ -43,7 +45,7 @@ class TabManager: ObservableObject { /// - Parameters: /// - item: The tab to open. /// - tabgroup: The tabgroup to add the tab to. If nil, it is added to the active tab group. - func openTab(item: WorkspaceClient.FileItem, in tabgroup: TabGroupData? = nil) { + func openTab(item: CEWorkspaceFile, in tabgroup: TabGroupData? = nil) { let tabgroup = tabgroup ?? activeTabGroup tabgroup.openTab(item: item) } diff --git a/CodeEdit/Features/Tabs/TabGroup/TabGroup.swift b/CodeEdit/Features/Tabs/TabGroup/TabGroup.swift index 1e005ec089..25b627017d 100644 --- a/CodeEdit/Features/Tabs/TabGroup/TabGroup.swift +++ b/CodeEdit/Features/Tabs/TabGroup/TabGroup.swift @@ -14,7 +14,7 @@ enum TabGroup { /// Closes all tabs which present the given file /// - Parameter file: a file. - func closeAllTabs(of file: WorkspaceClient.FileItem) { + func closeAllTabs(of file: CEWorkspaceFile) { switch self { case .one(let tabGroupData): tabGroupData.tabs.remove(file) @@ -45,7 +45,7 @@ enum TabGroup { } /// Forms a set of all files currently represented by tabs. - func gatherOpenFiles() -> Set { + func gatherOpenFiles() -> Set { switch self { case .one(let tabGroupData): return Set(tabGroupData.tabs) diff --git a/CodeEdit/Features/Tabs/TabGroup/TabGroupData.swift b/CodeEdit/Features/Tabs/TabGroup/TabGroupData.swift index f4cebbdd83..7ffaea9649 100644 --- a/CodeEdit/Features/Tabs/TabGroup/TabGroupData.swift +++ b/CodeEdit/Features/Tabs/TabGroup/TabGroupData.swift @@ -10,10 +10,11 @@ import OrderedCollections import DequeModule final class TabGroupData: ObservableObject, Identifiable { - typealias Tab = WorkspaceClient.FileItem + typealias Tab = CEWorkspaceFile /// Set of open tabs. - @Published var tabs: OrderedSet = [] { + @Published + var tabs: OrderedSet = [] { didSet { let change = tabs.symmetricDifference(oldValue) @@ -32,12 +33,10 @@ final class TabGroupData: ObservableObject, Identifiable { } } } - - /// History of tab switching. - @Published var history: Deque = [] - + /// The current offset in the history list. - @Published var historyOffset: Int = 0 { + @Published + var historyOffset: Int = 0 { didSet { let tab = history[historyOffset] @@ -52,10 +51,16 @@ final class TabGroupData: ObservableObject, Identifiable { } } + /// History of tab switching. + @Published + var history: Deque = [] + /// Currently selected tab. - @Published var selected: Tab? + @Published + var selected: Tab? - @Published var temporaryTab: Tab? + @Published + var temporaryTab: Tab? let id = UUID() diff --git a/CodeEdit/Features/Tabs/Views/TabBarContextMenu.swift b/CodeEdit/Features/Tabs/Views/TabBarContextMenu.swift index f1ff9d074c..64362e6302 100644 --- a/CodeEdit/Features/Tabs/Views/TabBarContextMenu.swift +++ b/CodeEdit/Features/Tabs/Views/TabBarContextMenu.swift @@ -9,14 +9,14 @@ import Foundation import SwiftUI extension View { - func tabBarContextMenu(item: WorkspaceClient.FileItem, isTemporary: Bool) -> some View { + func tabBarContextMenu(item: CEWorkspaceFile, isTemporary: Bool) -> some View { modifier(TabBarContextMenu(item: item, isTemporary: isTemporary)) } } struct TabBarContextMenu: ViewModifier { init( - item: WorkspaceClient.FileItem, + item: CEWorkspaceFile, isTemporary: Bool ) { self.item = item @@ -31,7 +31,7 @@ struct TabBarContextMenu: ViewModifier { @Environment(\.splitEditor) var splitEditor - private var item: WorkspaceClient.FileItem + private var item: CEWorkspaceFile private var isTemporary: Bool // swiftlint:disable:next function_body_length @@ -131,7 +131,7 @@ struct TabBarContextMenu: ViewModifier { /// Copies the absolute path of the given `FileItem` /// - Parameter item: The `FileItem` to use. - private func copyPath(item: WorkspaceClient.FileItem) { + private func copyPath(item: CEWorkspaceFile) { NSPasteboard.general.clearContents() NSPasteboard.general.setString(item.url.standardizedFileURL.path, forType: .string) } @@ -145,8 +145,8 @@ struct TabBarContextMenu: ViewModifier { /// Copies the relative path from the workspace folder to the given file item to the pasteboard. /// - Parameter item: The `FileItem` to use. - private func copyRelativePath(item: WorkspaceClient.FileItem) { - guard let rootPath = workspace.workspaceClient?.folderURL() else { + private func copyRelativePath(item: CEWorkspaceFile) { + guard let rootPath = workspace.workspaceFileManager?.folderUrl else { return } // Calculate the relative path diff --git a/CodeEdit/Features/Tabs/Views/TabBarItemView.swift b/CodeEdit/Features/Tabs/Views/TabBarItemView.swift index 30bdc1bca7..b114974ad7 100644 --- a/CodeEdit/Features/Tabs/Views/TabBarItemView.swift +++ b/CodeEdit/Features/Tabs/Views/TabBarItemView.swift @@ -9,8 +9,6 @@ import SwiftUI struct TabBarItemView: View { - typealias Item = WorkspaceClient.FileItem - @Environment(\.colorScheme) private var colorScheme @@ -58,9 +56,9 @@ struct TabBarItemView: View { /// The id associating with the tab that is currently being dragged. /// /// When `nil`, then there is no tab being dragged. - private var draggingTabId: Item.ID? + private var draggingTabId: CEWorkspaceFile.ID? - private var onDragTabId: Item.ID? + private var onDragTabId: CEWorkspaceFile.ID? @Binding private var closeButtonGestureActive: Bool @@ -71,7 +69,7 @@ struct TabBarItemView: View { /// The item associated with the current tab. /// /// You can get tab-related information from here, like `label`, `icon`, etc. - private var item: Item + private var item: CEWorkspaceFile var index: Int @@ -120,10 +118,10 @@ struct TabBarItemView: View { init( expectedWidth: CGFloat, - item: Item, + item: CEWorkspaceFile, index: Int, - draggingTabId: Item.ID?, - onDragTabId: Item.ID?, + draggingTabId: CEWorkspaceFile.ID?, + onDragTabId: CEWorkspaceFile.ID?, closeButtonGestureActive: Binding ) { self.expectedWidth = expectedWidth @@ -155,7 +153,7 @@ struct TabBarItemView: View { : .secondary ) .frame(width: 12, height: 12) - Text(item.fileName) + Text(item.name) .font( isTemporary ? .system(size: 11.0).italic() diff --git a/CodeEdit/Features/Tabs/Views/TabBarView.swift b/CodeEdit/Features/Tabs/Views/TabBarView.swift index 13cd478c8e..4147829baf 100644 --- a/CodeEdit/Features/Tabs/Views/TabBarView.swift +++ b/CodeEdit/Features/Tabs/Views/TabBarView.swift @@ -14,14 +14,18 @@ import SwiftUI // - TODO: TabBarItemView drop-outside event handler. struct TabBarView: View { - @Environment(\.modifierKeys) var modifierKeys - - typealias TabID = WorkspaceClient.FileItem.ID + typealias TabID = CEWorkspaceFile.ID /// The height of tab bar. /// I am not making it a private variable because it may need to be used in outside views. static let height = 28.0 + @Environment(\.modifierKeys) + var modifierKeys + + @Environment(\.splitEditor) + var splitEditor + @Environment(\.colorScheme) private var colorScheme @@ -38,8 +42,6 @@ struct TabBarView: View { @EnvironmentObject private var tabgroup: TabGroupData - @Environment(\.splitEditor) var splitEditor - /// The app preference. @StateObject private var prefs: AppPreferencesModel = .shared @@ -473,7 +475,7 @@ struct TabBarView: View { } label: { HStack { tab.icon - Text(tab.fileName) + Text(tab.name) } } } @@ -502,7 +504,7 @@ struct TabBarView: View { } label: { HStack { tab.icon - Text(tab.fileName) + Text(tab.name) } } } diff --git a/CodeEdit/Utils/Extensions/Array/Array+CEWorkspaceFile.swift b/CodeEdit/Utils/Extensions/Array/Array+CEWorkspaceFile.swift new file mode 100644 index 0000000000..5bd9a2e203 --- /dev/null +++ b/CodeEdit/Utils/Extensions/Array/Array+CEWorkspaceFile.swift @@ -0,0 +1,43 @@ +// +// Array+FileSystem.FileItem.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom on 07/02/2023. +// + +import Foundation + +extension Array where Element == CEWorkspaceFile { + + /// Sorts the elements in alphabetical order. + /// - Parameter foldersOnTop: if set to `true` folders will always be on top of files. + /// - Returns: A sorted array of ``FileSystemClient/FileSystemClient/FileItem`` + func sortItems(foldersOnTop: Bool) -> Self { + var alphabetically = sorted { $0.name < $1.name } + + if foldersOnTop { + var foldersOnTop = alphabetically.filter { $0.children != nil } + alphabetically.removeAll { $0.children != nil } + + foldersOnTop.append(contentsOf: alphabetically) + + return foldersOnTop + } else { + return alphabetically + } + } + +} + +extension Array where Element: Hashable { + + /// Checks the difference between two given items. + /// - Parameter other: Other element + /// - Returns: symmetricDifference + func difference(from other: [Element]) -> [Element] { + let thisSet = Set(self) + let otherSet = Set(other) + return Array(thisSet.symmetricDifference(otherSet)) + } + +} diff --git a/CodeEdit/Utils/WorkspaceClient/Interface.swift b/CodeEdit/Utils/WorkspaceClient/Interface.swift deleted file mode 100644 index e10b098dc1..0000000000 --- a/CodeEdit/Utils/WorkspaceClient/Interface.swift +++ /dev/null @@ -1,39 +0,0 @@ -// -// Interface.swift -// CodeEditModules/WorkspaceClient -// -// Created by Marco Carnevali on 16/03/22. -// - -import Combine -import Foundation - -// TODO: DOCS (Marco Carnevali) -struct WorkspaceClient { - - var folderURL: () -> URL? - - var getFiles: AnyPublisher<[FileItem], Never> - - var getFileItem: (_ id: String) throws -> FileItem - - /// callback function that is run when a change is detected in the file system. - /// This usually contains a `reloadData` function. - static var onRefresh: () -> Void = {} - - // For some strange reason, swiftlint thinks this is wrong? - init( - folderURL: @escaping () -> URL?, - getFiles: AnyPublisher<[FileItem], Never>, - getFileItem: @escaping (_ id: String) throws -> FileItem - ) { - self.folderURL = folderURL - self.getFiles = getFiles - self.getFileItem = getFileItem - } - // swiftlint:enable vertical_parameter_alignment - - enum WorkspaceClientError: Error { - case fileNotExist - } -} diff --git a/CodeEdit/Utils/WorkspaceClient/Live.swift b/CodeEdit/Utils/WorkspaceClient/Live.swift deleted file mode 100644 index 0bc35b6aa6..0000000000 --- a/CodeEdit/Utils/WorkspaceClient/Live.swift +++ /dev/null @@ -1,178 +0,0 @@ -// -// Live.swift -// CodeEditModules/WorkspaceClient -// -// Created by Marco Carnevali on 16/03/22. -// - -import Combine -import Foundation - -// TODO: DOCS (Marco Carnevali) -extension WorkspaceClient { - // swiftlint:disable:next function_body_length - static func `default`( - fileManager: FileManager, - folderURL: URL, - ignoredFilesAndFolders: [String] - ) throws -> Self { - var flattenedFileItems: [String: FileItem] = [:] - - // Recursive loading of files into `FileItem`s - // - Parameter url: The URL of the directory to load the items of - // - Returns: `[FileItem]` representing the contents of the directory - func loadFiles(fromURL url: URL) throws -> [FileItem] { - let directoryContents = try fileManager.contentsOfDirectory( - at: url.resolvingSymlinksInPath(), - includingPropertiesForKeys: nil - ) - var items: [FileItem] = [] - - for itemURL in directoryContents { - // Skip file if it is in ignore list - guard !ignoredFilesAndFolders.contains(itemURL.lastPathComponent) else { continue } - - var isDir: ObjCBool = false - - if fileManager.fileExists(atPath: itemURL.path, isDirectory: &isDir) { - var subItems: [FileItem]? - - if isDir.boolValue { - // Recursively fetch subdirectories and files if the path points to a directory - subItems = try loadFiles(fromURL: itemURL) - } - - let newFileItem = FileItem(url: itemURL, children: subItems?.sortItems(foldersOnTop: true)) - subItems?.forEach { $0.parent = newFileItem } - items.append(newFileItem) - flattenedFileItems[newFileItem.id] = newFileItem - } - } - - return items - } - - // initial load - let fileItems = try loadFiles(fromURL: folderURL) - // workspace fileItem - let workspaceItem = FileItem(url: folderURL, children: fileItems) - flattenedFileItems[workspaceItem.id] = workspaceItem - fileItems.forEach { item in - item.parent = workspaceItem - } - - // By using `CurrentValueSubject` we can define a starting value. - // The value passed during init it's going to be send as soon as the - // consumer subscribes to the publisher. - let subject = CurrentValueSubject<[FileItem], Never>(fileItems) - - var isRunning: Bool = false - var anotherInstanceRan: Int = 0 - - // Recursive function similar to `loadFiles`, but creates or deletes children of the - // `FileItem` so that they are accurate with the file system, instead of creating an - // entirely new `FileItem`, to prevent the `OutlineView` from going crazy with folding. - // - Parameter fileItem: The `FileItem` to correct the children of - func rebuildFiles(fromItem fileItem: FileItem) throws -> Bool { - var didChangeSomething = false - - // get the actual directory children - let directoryContentsUrls = try fileManager.contentsOfDirectory( - at: fileItem.url.resolvingSymlinksInPath(), - includingPropertiesForKeys: nil - ) - - // test for deleted children, and remove them from the index - for oldContent in fileItem.children ?? [] where !directoryContentsUrls.contains(oldContent.url) { - if let removeAt = fileItem.children?.firstIndex(of: oldContent) { - fileItem.children?.remove(at: removeAt) - flattenedFileItems.removeValue(forKey: oldContent.id) - didChangeSomething = true - } - } - - // test for new children, and index them using loadFiles - for newContent in directoryContentsUrls { - guard !ignoredFilesAndFolders.contains(newContent.lastPathComponent) else { continue } - - var childExists = false - fileItem.children?.forEach({ childExists = $0.url == newContent ? true : childExists }) - if childExists { - continue - } - - var isDir: ObjCBool = false - if fileManager.fileExists(atPath: newContent.path, isDirectory: &isDir) { - var subItems: [FileItem]? - - if isDir.boolValue { subItems = try loadFiles(fromURL: newContent) } - - let newFileItem = FileItem(url: newContent, children: subItems?.sortItems(foldersOnTop: true)) - subItems?.forEach { $0.parent = newFileItem } - newFileItem.parent = fileItem - flattenedFileItems[newFileItem.id] = newFileItem - fileItem.children?.append(newFileItem) - didChangeSomething = true - } - } - - fileItem.children = fileItem.children?.sortItems(foldersOnTop: true) - fileItem.children?.forEach({ - if $0.isFolder { - let childChanged = try? rebuildFiles(fromItem: $0) - didChangeSomething = (childChanged ?? false) ? true : didChangeSomething - } - flattenedFileItems[$0.id] = $0 - }) - - return didChangeSomething - } - - FileItem.watcherCode = { - // Something has changed inside the directory - // We should reload the files. - guard !isRunning else { // this runs when a file change is detected but is already running - anotherInstanceRan += 1 - return - } - isRunning = true - flattenedFileItems = [workspaceItem.id: workspaceItem] - _ = try? rebuildFiles(fromItem: workspaceItem) - while anotherInstanceRan > 0 { // TODO: optimise - let somethingChanged = try? rebuildFiles(fromItem: workspaceItem) - anotherInstanceRan = !(somethingChanged ?? false) ? 0 : anotherInstanceRan - 1 - } - subject.send(workspaceItem.children ?? []) - isRunning = false - anotherInstanceRan = 0 - // reload data in outline view controller through the main thread - DispatchQueue.main.async { onRefresh() } - } - - func stopListeningToDirectory(directory: URL? = nil) { - if directory != nil { - flattenedFileItems[directory!.relativePath]?.watcher?.cancel() - } else { - for item in flattenedFileItems.values { - item.watcher?.cancel() - } - } - } - - return Self( - folderURL: { folderURL }, - getFiles: subject - .handleEvents(receiveCancel: { - stopListeningToDirectory() - }) - .receive(on: RunLoop.main) - .eraseToAnyPublisher(), - getFileItem: { id in - guard let item = flattenedFileItems[id] else { - throw WorkspaceClientError.fileNotExist - } - return item - } - ) - } -} diff --git a/CodeEdit/Utils/WorkspaceClient/Mocks.swift b/CodeEdit/Utils/WorkspaceClient/Mocks.swift deleted file mode 100644 index 14b6906016..0000000000 --- a/CodeEdit/Utils/WorkspaceClient/Mocks.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// Mocks.swift -// CodeEditModules/WorkspaceClient -// -// Created by Marco Carnevali on 16/03/22. -// - -import Combine -import Foundation - -// TODO: DOCS (Marco Carnevali) -extension WorkspaceClient { - static var empty = Self( - folderURL: { nil }, - getFiles: CurrentValueSubject<[FileItem], Never>([]).eraseToAnyPublisher(), - getFileItem: { _ in throw WorkspaceClientError.fileNotExist } - ) -} diff --git a/CodeEdit/Utils/WorkspaceClient/Model/FileItem+Array.swift b/CodeEdit/Utils/WorkspaceClient/Model/FileItem+Array.swift deleted file mode 100644 index c6a0893384..0000000000 --- a/CodeEdit/Utils/WorkspaceClient/Model/FileItem+Array.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// FileItem+Array.swift -// CodeEditModules/WorkspaceClient -// -// Created by Lukas Pistrol on 17.03.22. -// - -import Foundation - -extension Array where Element == WorkspaceClient.FileItem { - - /// Sorts the elements in alphabetical order. - /// - Parameter foldersOnTop: if set to `true` folders will always be on top of files. - /// - Returns: A sorted array of ``WorkspaceClient/WorkspaceClient/FileItem`` - func sortItems(foldersOnTop: Bool) -> Self { - var alphabetically = sorted { $0.fileName < $1.fileName } - - if foldersOnTop { - var foldersOnTop = alphabetically.filter { $0.children != nil } - alphabetically.removeAll { $0.children != nil } - - foldersOnTop.append(contentsOf: alphabetically) - - return foldersOnTop - } else { - return alphabetically - } - } -} diff --git a/CodeEdit/Utils/WorkspaceClient/Model/FileItem.swift b/CodeEdit/Utils/WorkspaceClient/Model/FileItem.swift deleted file mode 100644 index 7ad7f7e742..0000000000 --- a/CodeEdit/Utils/WorkspaceClient/Model/FileItem.swift +++ /dev/null @@ -1,338 +0,0 @@ -// -// FileItem.swift -// CodeEditModules/WorkspaceClient -// -// Created by Marco Carnevali on 16/03/22. -// - -import Foundation -import SwiftUI -import UniformTypeIdentifiers - -extension WorkspaceClient { - enum FileItemCodingKeys: String, CodingKey { - case id - case url - case children - } - - /// An object containing all necessary information and actions for a specific file in the workspace - final class FileItem: Identifiable, Codable, TabBarItemRepresentable { - var tabID: TabBarItemID { - .codeEditor(id) - } - - var title: String { - url.lastPathComponent - } - - var icon: Image { - Image(systemName: systemImage) - } - - typealias ID = String - - private let uuid: UUID - - var watcher: DispatchSourceFileSystemObject? - static var watcherCode: () -> Void = {} - - func activateWatcher() -> Bool { - let descriptor = open(self.url.path, O_EVTONLY) - guard descriptor > 0 else { return false } - let source = DispatchSource.makeFileSystemObjectSource( - fileDescriptor: descriptor, - eventMask: .write, - queue: DispatchQueue.global() - ) - source.setEventHandler { FileItem.watcherCode() } - source.setCancelHandler { close(descriptor) } - source.resume() - self.watcher = source - return true - } - - init( - url: URL, - children: [FileItem]? = nil - ) { - self.url = url - self.children = children - id = url.relativePath - uuid = UUID() - } - - required init(from decoder: Decoder) throws { - let values = try decoder.container(keyedBy: FileItemCodingKeys.self) - id = try values.decode(String.self, forKey: .id) - url = try values.decode(URL.self, forKey: .url) - children = try values.decode([FileItem]?.self, forKey: .children) - uuid = UUID() - } - - func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: FileItemCodingKeys.self) - try container.encode(id, forKey: .id) - try container.encode(url, forKey: .url) - try container.encode(children, forKey: .children) - } - - /// The id of the ``WorkspaceClient/FileItem``. - /// - /// This is equal to `url.relativePath` - var id: ID - - /// Returns the URL of the ``WorkspaceClient/FileItem`` - var url: URL - - /// Returns the children of the current ``WorkspaceClient/FileItem``. - /// - /// If the current ``WorkspaceClient/FileItem`` is a file this will be `nil`. - /// If it is an empty folder this will be an empty array. - var children: [FileItem]? - - /// Returns a parent ``WorkspaceClient/FileItem``. - /// - /// If the item already is the top-level ``WorkspaceClient/FileItem`` this returns `nil`. - var parent: FileItem? - - /// A boolean that is true if ``children`` is not `nil` - var isFolder: Bool { - children != nil - } - - /// A boolean that is true if the file item is the root folder of the workspace. - var isRoot: Bool { - parent == nil - } - - /// Returns a string describing a SFSymbol for the current ``WorkspaceClient/FileItem`` - /// - /// Use it like this - /// ```swift - /// Image(systemName: item.systemImage) - /// ``` - var systemImage: String { - switch children { - case nil: - return FileIcon.fileIcon(fileType: fileType) - case .some where parent == nil: - return "folder.fill.badge.gearshape" - case let .some(children): - if self.watcher == nil && !self.activateWatcher() { - return "questionmark.folder" - } - return folderIcon(children) - } - } - - /// Returns the file name (e.g.: `Package.swift`) - var fileName: String { - url.lastPathComponent - } - - /// Returns the extension of the file or an empty string if no extension is present. - var fileType: FileIcon.FileType { - .init(rawValue: url.pathExtension) ?? .txt - } - - /// Returns a string describing a SFSymbol for folders - /// - /// If it is the top-level folder this will return `"square.dashed.inset.filled"`. - /// If it is a `.codeedit` folder this will return `"folder.fill.badge.gearshape"`. - /// If it has children this will return `"folder.fill"` otherwise `"folder"`. - private func folderIcon(_ children: [FileItem]) -> String { - if self.parent == nil { - return "square.dashed.inset.filled" - } - if self.fileName == ".codeedit" { - return "folder.fill.badge.gearshape" - } - return children.isEmpty ? "folder" : "folder.fill" - } - - /// Returns the file name with optional extension (e.g.: `Package.swift`) - func fileName(typeHidden: Bool) -> String { - typeHidden ? url.deletingPathExtension().lastPathComponent : fileName - } - - /// Return the file's UTType - var contentType: UTType? { - try? url.resourceValues(forKeys: [.contentTypeKey]).contentType - } - - /// Returns a `Color` for a specific `fileType` - /// - /// If not specified otherwise this will return `Color.accentColor` - var iconColor: Color { - FileIcon.iconColor(fileType: fileType) - } - - var fileDocument: CodeFileDocument? - - // MARK: Statics - - /// The default `FileManager` instance - static let fileManger = FileManager.default - - // MARK: Intents - - /// Allows the user to view the file or folder in the finder application - func showInFinder() { - NSWorkspace.shared.activateFileViewerSelecting([url]) - } - - /// Allows the user to launch the file or folder as it would be in finder - func openWithExternalEditor() { - NSWorkspace.shared.open(url) - } - - /// This function allows creation of folders in the main directory or sub-folders - /// - Parameter folderName: The name of the new folder - func addFolder(folderName: String) { - // check if folder, if it is create folder under self, else create on same level. - var folderUrl = (self.isFolder ? - self.url.appendingPathComponent(folderName) : - self.url.deletingLastPathComponent().appendingPathComponent(folderName)) - - // if a file/folder with the same name exists, add a number to the end. - var fileNumber = 0 - while FileItem.fileManger.fileExists(atPath: folderUrl.path) { - fileNumber += 1 - folderUrl = folderUrl.deletingLastPathComponent().appendingPathComponent("\(folderName)\(fileNumber)") - } - - // create the folder - do { - try FileItem.fileManger.createDirectory( - at: folderUrl, - withIntermediateDirectories: true, - attributes: [:] - ) - } catch { - fatalError(error.localizedDescription) - } - } - - /// This function allows creating files in the selected folder or project main directory - /// - Parameter fileName: The name of the new file - @discardableResult - func addFile(fileName: String) -> String { - // check if folder, if it is create file under self - var fileUrl = (self.isFolder ? - self.url.appendingPathComponent(fileName) : - self.url.deletingLastPathComponent().appendingPathComponent(fileName)) - - // if a file/folder with the same name exists, add a number to the end. - var fileNumber = 0 - while FileItem.fileManger.fileExists(atPath: fileUrl.path) { - fileNumber += 1 - fileUrl = fileUrl.deletingLastPathComponent().appendingPathComponent("\(fileName)\(fileNumber)") - } - - // create the file - FileItem.fileManger.createFile( - atPath: fileUrl.path, - contents: nil, - attributes: [FileAttributeKey.creationDate: Date()] - ) - - return fileUrl.path - } - - /// This function deletes the item or folder from the current project - func delete() { - // this function also has to account for how the - // file system can change outside of the editor - - let deleteConfirmation = NSAlert() - let message = "\(self.fileName)\(self.isFolder ? " and its children" :"")" - deleteConfirmation.messageText = "Are you sure you want to move \(message) to the Trash?" - deleteConfirmation.alertStyle = .critical - deleteConfirmation.addButton(withTitle: "Delete") - deleteConfirmation.buttons.last?.hasDestructiveAction = true - deleteConfirmation.addButton(withTitle: "Cancel") - if deleteConfirmation.runModal() == .alertFirstButtonReturn { // "Delete" button - if FileItem.fileManger.fileExists(atPath: self.url.path) { - do { - try FileItem.fileManger.removeItem(at: self.url) - } catch { - fatalError(error.localizedDescription) - } - } - } - } - - /// This function duplicates the item or folder - func duplicate(to destination: URL? = nil) { - var fileUrl = destination == nil ? self.url : destination! - // if a file/folder with the same name exists, add "copy" to the end - while FileItem.fileManger.fileExists(atPath: fileUrl.path) { - let previousName = fileUrl.deletingPathExtension().lastPathComponent - let filextension = fileUrl.pathExtension - let duplicateName = "\(previousName)-copy" - - fileUrl = fileUrl.deletingLastPathComponent() - fileUrl.appendPathComponent(duplicateName) - fileUrl.appendPathExtension(filextension) - } - - if FileItem.fileManger.fileExists(atPath: self.url.path) { - do { - try FileItem.fileManger.copyItem(at: self.url, to: fileUrl) - } catch { - print("Error at \(self.url.path) to \(fileUrl.path)") - fatalError(error.localizedDescription) - } - } - } - - /// This function moves the item or folder if possible - func move(to newLocation: URL) { - guard !FileItem.fileManger.fileExists(atPath: newLocation.path) else { return } - do { - try FileItem.fileManger.moveItem(at: self.url, to: newLocation) - self.url = newLocation - } catch { - let errorCode = (error as NSError).code - let errorAlert = NSAlert() - errorAlert.messageText = """ - The operation can’t be completed because an unexpected error occurred (error code \(String(errorCode))). - """ - errorAlert.alertStyle = .critical - errorAlert.addButton(withTitle: "OK") - errorAlert.runModal() - } - } - } -} - -// MARK: Hashable - -extension WorkspaceClient.FileItem: Hashable { - func hash(into hasher: inout Hasher) { - hasher.combine(uuid) - } -} - -// MARK: Comparable - -extension WorkspaceClient.FileItem: Comparable { - static func == (lhs: WorkspaceClient.FileItem, rhs: WorkspaceClient.FileItem) -> Bool { - lhs.id == rhs.id - } - - static func < (lhs: WorkspaceClient.FileItem, rhs: WorkspaceClient.FileItem) -> Bool { - lhs.url.lastPathComponent < rhs.url.lastPathComponent - } -} - -extension Array where Element: Hashable { - - // TODO: DOCS (Marco Carnevali) - func difference(from other: [Element]) -> [Element] { - let thisSet = Set(self) - let otherSet = Set(other) - return Array(thisSet.symmetricDifference(otherSet)) - } -} diff --git a/CodeEdit/WorkspaceView.swift b/CodeEdit/WorkspaceView.swift index 328ac36ed6..17508bece2 100644 --- a/CodeEdit/WorkspaceView.swift +++ b/CodeEdit/WorkspaceView.swift @@ -40,7 +40,7 @@ struct WorkspaceView: View { var focusedEditor: TabGroupData? var body: some View { - if workspace.workspaceClient != nil { + if workspace.workspaceFileManager != nil { VStack { SplitViewReader { proxy in SplitView(axis: .vertical) { From 13ebefebe6bca65e5d470c642d0dd371de11acf7 Mon Sep 17 00:00:00 2001 From: KaiTheRedNinja <88234730+KaiTheRedNinja@users.noreply.github.com> Date: Wed, 26 Apr 2023 22:21:05 +0800 Subject: [PATCH 2/4] Transfer project navigator (#1208) * Transfer project navigator bottom * Transfer TableViewCell abstractions * Transfer over project manager outline view * Get proj navigator sidebar working again * Rename OutlineView -> ProjectNavigatorOutlineView for clarity * Remove debug statements * Fix incorrect comment headers * Change old AE assets to new CE assets * Add vscode-style toolbar * Disable vertical sidebar by default * Make deletion messages more descriptive * Fix table view cell alignments --- CodeEdit.xcodeproj/project.pbxproj | 76 ++++--- .../CEWorkspace/Models/CEWorkspaceFile.swift | 14 +- .../Models/CEWorkspaceFileManager.swift | 5 +- .../Views/ToolbarBranchPicker.swift | 2 +- .../Documents/WorkspaceDocument.swift | 34 +-- .../FindNavigatorListViewController.swift | 2 +- ...op.swift => NavigatorSidebarToolbar.swift} | 94 ++++++--- .../NavigatorSidebarView.swift | 28 ++- .../OutlineView/FileSystemTableViewCell.swift | 149 +++++++++++++ .../OutlineView/StandardTableViewCell.swift | 198 ++++++++++++++++++ .../OutlineView/TextTableViewCell.swift | 81 +++++++ .../OutlineView/OutlineTableViewCell.swift | 180 ---------------- ...eMenu.swift => ProjectNavigatorMenu.swift} | 4 +- ...wift => ProjectNavigatorOutlineView.swift} | 15 +- .../ProjectNavigatorTableViewCell.swift | 64 ++++++ ...roller+OutlineTableViewCellDelegate.swift} | 2 +- ...t => ProjectNavigatorViewController.swift} | 29 +-- .../ProjectNavigatorToolbarBottom.swift} | 54 ++++- .../ProjectNavigatorView.swift | 2 +- 19 files changed, 723 insertions(+), 310 deletions(-) rename CodeEdit/Features/NavigatorSidebar/{NavigatorSidebarToolbarTop.swift => NavigatorSidebarToolbar.swift} (68%) create mode 100644 CodeEdit/Features/NavigatorSidebar/OutlineView/FileSystemTableViewCell.swift create mode 100644 CodeEdit/Features/NavigatorSidebar/OutlineView/StandardTableViewCell.swift create mode 100644 CodeEdit/Features/NavigatorSidebar/OutlineView/TextTableViewCell.swift delete mode 100644 CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/OutlineTableViewCell.swift rename CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/{OutlineMenu.swift => ProjectNavigatorMenu.swift} (98%) rename CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/{OutlineView.swift => ProjectNavigatorOutlineView.swift} (76%) create mode 100644 CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/ProjectNavigatorTableViewCell.swift rename CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/{OutlintViewController+OutlineTableViewCellDelegate.swift => ProjectNavigatorViewController+OutlineTableViewCellDelegate.swift} (88%) rename CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/{OutlineViewController.swift => ProjectNavigatorViewController.swift} (93%) rename CodeEdit/Features/NavigatorSidebar/{NavigatorSidebarToolbarBottom.swift => ProjectNavigator/ProjectNavigatorToolbarBottom.swift} (52%) diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index 86022cb047..c3524f6ac2 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -19,8 +19,8 @@ 04C3254B27FF23B000C8DA2D /* ExtensionNavigatorData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04C3254A27FF23B000C8DA2D /* ExtensionNavigatorData.swift */; }; 04C3254F2800AA4700C8DA2D /* ExtensionInstallationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04C3254E2800AA4700C8DA2D /* ExtensionInstallationView.swift */; }; 04C325512800AC7400C8DA2D /* ExtensionInstallationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04C325502800AC7400C8DA2D /* ExtensionInstallationViewModel.swift */; }; - 04C3255B2801F86400C8DA2D /* OutlineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 285FEC6D27FE4B4A00E57D53 /* OutlineViewController.swift */; }; - 04C3255C2801F86900C8DA2D /* OutlineMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 285FEC7127FE4EEF00E57D53 /* OutlineMenu.swift */; }; + 04C3255B2801F86400C8DA2D /* ProjectNavigatorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 285FEC6D27FE4B4A00E57D53 /* ProjectNavigatorViewController.swift */; }; + 04C3255C2801F86900C8DA2D /* ProjectNavigatorMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 285FEC7127FE4EEF00E57D53 /* ProjectNavigatorMenu.swift */; }; 200412EF280F3EAC00BCAF5C /* HistoryInspectorNoHistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 200412EE280F3EAC00BCAF5C /* HistoryInspectorNoHistoryView.swift */; }; 201169D72837B2E300F92B46 /* SourceControlNavigatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 201169D62837B2E300F92B46 /* SourceControlNavigatorView.swift */; }; 201169D92837B31200F92B46 /* SourceControlSearchToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 201169D82837B31200F92B46 /* SourceControlSearchToolbar.swift */; }; @@ -51,10 +51,10 @@ 2816F594280CF50500DD548B /* CodeEditSymbols in Frameworks */ = {isa = PBXBuildFile; productRef = 2816F593280CF50500DD548B /* CodeEditSymbols */; }; 283BDCBD2972EEBD002AFF81 /* Package.resolved in Resources */ = {isa = PBXBuildFile; fileRef = 283BDCBC2972EEBD002AFF81 /* Package.resolved */; }; 283BDCC52972F236002AFF81 /* AcknowledgementsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 283BDCC42972F236002AFF81 /* AcknowledgementsTests.swift */; }; - 2847019E27FDDF7600F87B6B /* OutlineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2847019D27FDDF7600F87B6B /* OutlineView.swift */; }; + 2847019E27FDDF7600F87B6B /* ProjectNavigatorOutlineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2847019D27FDDF7600F87B6B /* ProjectNavigatorOutlineView.swift */; }; 284DC84F2978B7B400BF2770 /* ContributorsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 284DC84E2978B7B400BF2770 /* ContributorsView.swift */; }; 284DC8512978BA2600BF2770 /* .all-contributorsrc in Resources */ = {isa = PBXBuildFile; fileRef = 284DC8502978BA2600BF2770 /* .all-contributorsrc */; }; - 285FEC7027FE4B9800E57D53 /* OutlineTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 285FEC6F27FE4B9800E57D53 /* OutlineTableViewCell.swift */; }; + 285FEC7027FE4B9800E57D53 /* ProjectNavigatorTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 285FEC6F27FE4B9800E57D53 /* ProjectNavigatorTableViewCell.swift */; }; 286471AB27ED51FD0039369D /* ProjectNavigatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 286471AA27ED51FD0039369D /* ProjectNavigatorView.swift */; }; 287776E927E34BC700D46668 /* TabBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 287776E827E34BC700D46668 /* TabBarView.swift */; }; 2897E1C72979A29200741E32 /* OffsettableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2897E1C62979A29200741E32 /* OffsettableScrollView.swift */; }; @@ -62,11 +62,10 @@ 28A51002281673530087B0CC /* codeedit-xcode-light.json in Resources */ = {isa = PBXBuildFile; fileRef = 28A51000281673530087B0CC /* codeedit-xcode-light.json */; }; 28A51005281701B40087B0CC /* codeedit-github-light.json in Resources */ = {isa = PBXBuildFile; fileRef = 28A51003281701B40087B0CC /* codeedit-github-light.json */; }; 28A51006281701B40087B0CC /* codeedit-github-dark.json in Resources */ = {isa = PBXBuildFile; fileRef = 28A51004281701B40087B0CC /* codeedit-github-dark.json */; }; - 28B0A19827E385C300B73177 /* NavigatorSidebarToolbarTop.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28B0A19727E385C300B73177 /* NavigatorSidebarToolbarTop.swift */; }; + 28B0A19827E385C300B73177 /* NavigatorSidebarToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28B0A19727E385C300B73177 /* NavigatorSidebarToolbar.swift */; }; 28B8F884280FFE4600596236 /* NSTableView+Background.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28B8F883280FFE4600596236 /* NSTableView+Background.swift */; }; 28F43DE029738792008BBA45 /* codeedit-solarized-dark.json in Resources */ = {isa = PBXBuildFile; fileRef = 28F43DDF29738792008BBA45 /* codeedit-solarized-dark.json */; }; 28F43DE2297388C5008BBA45 /* codeedit-solarized-light.json in Resources */ = {isa = PBXBuildFile; fileRef = 28F43DE1297388C5008BBA45 /* codeedit-solarized-light.json */; }; - 28FFE1BF27E3A441001939DB /* NavigatorSidebarToolbarBottom.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28FFE1BE27E3A441001939DB /* NavigatorSidebarToolbarBottom.swift */; }; 2B7A583527E4BA0100D25D4E /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0468438427DC76E200F8E88E /* AppDelegate.swift */; }; 2B7AC06B282452FB0082A5B8 /* Media.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2B7AC06A282452FB0082A5B8 /* Media.xcassets */; }; 2BE487EF28245162003F3F64 /* FinderSync.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BE487EE28245162003F3F64 /* FinderSync.swift */; }; @@ -345,6 +344,10 @@ 6CFF967829BEBCF600182D6F /* MainCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CFF967729BEBCF600182D6F /* MainCommands.swift */; }; 6CFF967A29BEBD2400182D6F /* ViewCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CFF967929BEBD2400182D6F /* ViewCommands.swift */; }; 6CFF967C29BEBD5200182D6F /* WindowCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CFF967B29BEBD5200182D6F /* WindowCommands.swift */; }; + 9396827D29D01D1600AB4A6B /* ProjectNavigatorToolbarBottom.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9396827C29D01D1600AB4A6B /* ProjectNavigatorToolbarBottom.swift */; }; + 9396828329D01F1300AB4A6B /* StandardTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9396828029D01F1300AB4A6B /* StandardTableViewCell.swift */; }; + 9396828429D01F1300AB4A6B /* TextTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9396828129D01F1300AB4A6B /* TextTableViewCell.swift */; }; + 9396828529D01F1300AB4A6B /* FileSystemTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9396828229D01F1300AB4A6B /* FileSystemTableViewCell.swift */; }; B60BE8BD297A167600841125 /* AcknowledgementRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B60BE8BC297A167600841125 /* AcknowledgementRowView.swift */; }; B62617282964924E00E866AB /* CodeEditKit in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 2801BB89290D5A8E00EBF552 /* CodeEditKit */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; B658FB3427DA9E1000EA4DBD /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B658FB3327DA9E1000EA4DBD /* Assets.xcassets */; }; @@ -359,7 +362,7 @@ D7012EE827E757850001E1EF /* FindNavigatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7012EE727E757850001E1EF /* FindNavigatorView.swift */; }; D7211D4327E066CE008F2ED7 /* Localized+Ex.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7211D4227E066CE008F2ED7 /* Localized+Ex.swift */; }; D7211D4727E06BFE008F2ED7 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = D7211D4927E06BFE008F2ED7 /* Localizable.strings */; }; - D7DC4B76298FFBE900D6C83D /* OutlintViewController+OutlineTableViewCellDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DC4B75298FFBE900D6C83D /* OutlintViewController+OutlineTableViewCellDelegate.swift */; }; + D7DC4B76298FFBE900D6C83D /* ProjectNavigatorViewController+OutlineTableViewCellDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DC4B75298FFBE900D6C83D /* ProjectNavigatorViewController+OutlineTableViewCellDelegate.swift */; }; D7E201AE27E8B3C000CB86D0 /* String+Ranges.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7E201AD27E8B3C000CB86D0 /* String+Ranges.swift */; }; D7E201B027E8C07300CB86D0 /* FindNavigatorSearchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7E201AF27E8C07300CB86D0 /* FindNavigatorSearchBar.swift */; }; D7E201B227E8D50000CB86D0 /* FindNavigatorModeSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7E201B127E8D50000CB86D0 /* FindNavigatorModeSelector.swift */; }; @@ -462,12 +465,12 @@ 2806E903297958B9000040F4 /* ContributorRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContributorRowView.swift; sourceTree = ""; }; 283BDCBC2972EEBD002AFF81 /* Package.resolved */ = {isa = PBXFileReference; lastKnownFileType = text; name = Package.resolved; path = CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved; sourceTree = ""; }; 283BDCC42972F236002AFF81 /* AcknowledgementsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AcknowledgementsTests.swift; sourceTree = ""; }; - 2847019D27FDDF7600F87B6B /* OutlineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutlineView.swift; sourceTree = ""; }; + 2847019D27FDDF7600F87B6B /* ProjectNavigatorOutlineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProjectNavigatorOutlineView.swift; sourceTree = ""; }; 284DC84E2978B7B400BF2770 /* ContributorsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContributorsView.swift; sourceTree = ""; }; 284DC8502978BA2600BF2770 /* .all-contributorsrc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = ".all-contributorsrc"; sourceTree = ""; }; - 285FEC6D27FE4B4A00E57D53 /* OutlineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutlineViewController.swift; sourceTree = ""; }; - 285FEC6F27FE4B9800E57D53 /* OutlineTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutlineTableViewCell.swift; sourceTree = ""; }; - 285FEC7127FE4EEF00E57D53 /* OutlineMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutlineMenu.swift; sourceTree = ""; }; + 285FEC6D27FE4B4A00E57D53 /* ProjectNavigatorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProjectNavigatorViewController.swift; sourceTree = ""; }; + 285FEC6F27FE4B9800E57D53 /* ProjectNavigatorTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProjectNavigatorTableViewCell.swift; sourceTree = ""; }; + 285FEC7127FE4EEF00E57D53 /* ProjectNavigatorMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProjectNavigatorMenu.swift; sourceTree = ""; }; 286471AA27ED51FD0039369D /* ProjectNavigatorView.swift */ = {isa = PBXFileReference; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = ProjectNavigatorView.swift; sourceTree = ""; tabWidth = 4; }; 287776E627E3413200D46668 /* NavigatorSidebarView.swift */ = {isa = PBXFileReference; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = NavigatorSidebarView.swift; sourceTree = ""; tabWidth = 4; }; 287776E827E34BC700D46668 /* TabBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarView.swift; sourceTree = ""; }; @@ -476,11 +479,10 @@ 28A51000281673530087B0CC /* codeedit-xcode-light.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "codeedit-xcode-light.json"; sourceTree = ""; }; 28A51003281701B40087B0CC /* codeedit-github-light.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "codeedit-github-light.json"; sourceTree = ""; }; 28A51004281701B40087B0CC /* codeedit-github-dark.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "codeedit-github-dark.json"; sourceTree = ""; }; - 28B0A19727E385C300B73177 /* NavigatorSidebarToolbarTop.swift */ = {isa = PBXFileReference; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = NavigatorSidebarToolbarTop.swift; sourceTree = ""; tabWidth = 4; }; + 28B0A19727E385C300B73177 /* NavigatorSidebarToolbar.swift */ = {isa = PBXFileReference; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = NavigatorSidebarToolbar.swift; sourceTree = ""; tabWidth = 4; }; 28B8F883280FFE4600596236 /* NSTableView+Background.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSTableView+Background.swift"; sourceTree = ""; }; 28F43DDF29738792008BBA45 /* codeedit-solarized-dark.json */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 2; lastKnownFileType = text.json; path = "codeedit-solarized-dark.json"; sourceTree = ""; tabWidth = 2; }; 28F43DE1297388C5008BBA45 /* codeedit-solarized-light.json */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 2; lastKnownFileType = text.json; path = "codeedit-solarized-light.json"; sourceTree = ""; tabWidth = 2; }; - 28FFE1BE27E3A441001939DB /* NavigatorSidebarToolbarBottom.swift */ = {isa = PBXFileReference; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = NavigatorSidebarToolbarBottom.swift; sourceTree = ""; tabWidth = 4; }; 2B15CA0028254139004E8F22 /* OpenWithCodeEdit.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = OpenWithCodeEdit.entitlements; sourceTree = ""; }; 2B7AC06A282452FB0082A5B8 /* Media.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Media.xcassets; sourceTree = ""; }; 2BE487EC28245162003F3F64 /* OpenWithCodeEdit.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = OpenWithCodeEdit.appex; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -753,6 +755,10 @@ 6CFF967729BEBCF600182D6F /* MainCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainCommands.swift; sourceTree = ""; }; 6CFF967929BEBD2400182D6F /* ViewCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewCommands.swift; sourceTree = ""; }; 6CFF967B29BEBD5200182D6F /* WindowCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowCommands.swift; sourceTree = ""; }; + 9396827C29D01D1600AB4A6B /* ProjectNavigatorToolbarBottom.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProjectNavigatorToolbarBottom.swift; sourceTree = ""; }; + 9396828029D01F1300AB4A6B /* StandardTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StandardTableViewCell.swift; sourceTree = ""; }; + 9396828129D01F1300AB4A6B /* TextTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextTableViewCell.swift; sourceTree = ""; }; + 9396828229D01F1300AB4A6B /* FileSystemTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileSystemTableViewCell.swift; sourceTree = ""; }; B60BE8BC297A167600841125 /* AcknowledgementRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AcknowledgementRowView.swift; sourceTree = ""; }; B658FB2C27DA9E0F00EA4DBD /* CodeEdit.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CodeEdit.app; sourceTree = BUILT_PRODUCTS_DIR; }; B658FB3127DA9E0F00EA4DBD /* WorkspaceView.swift */ = {isa = PBXFileReference; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = WorkspaceView.swift; sourceTree = ""; tabWidth = 4; }; @@ -771,7 +777,7 @@ D7012EE727E757850001E1EF /* FindNavigatorView.swift */ = {isa = PBXFileReference; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = FindNavigatorView.swift; sourceTree = ""; tabWidth = 4; }; D7211D4227E066CE008F2ED7 /* Localized+Ex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Localized+Ex.swift"; sourceTree = ""; }; D7211D4827E06BFE008F2ED7 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; - D7DC4B75298FFBE900D6C83D /* OutlintViewController+OutlineTableViewCellDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OutlintViewController+OutlineTableViewCellDelegate.swift"; sourceTree = ""; }; + D7DC4B75298FFBE900D6C83D /* ProjectNavigatorViewController+OutlineTableViewCellDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProjectNavigatorViewController+OutlineTableViewCellDelegate.swift"; sourceTree = ""; }; D7E201AD27E8B3C000CB86D0 /* String+Ranges.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Ranges.swift"; sourceTree = ""; }; D7E201AF27E8C07300CB86D0 /* FindNavigatorSearchBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FindNavigatorSearchBar.swift; sourceTree = ""; }; D7E201B127E8D50000CB86D0 /* FindNavigatorModeSelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FindNavigatorModeSelector.swift; sourceTree = ""; }; @@ -993,11 +999,11 @@ 285FEC6C27FE4AC700E57D53 /* OutlineView */ = { isa = PBXGroup; children = ( - 2847019D27FDDF7600F87B6B /* OutlineView.swift */, - 285FEC6D27FE4B4A00E57D53 /* OutlineViewController.swift */, - 285FEC6F27FE4B9800E57D53 /* OutlineTableViewCell.swift */, - 285FEC7127FE4EEF00E57D53 /* OutlineMenu.swift */, - D7DC4B75298FFBE900D6C83D /* OutlintViewController+OutlineTableViewCellDelegate.swift */, + 2847019D27FDDF7600F87B6B /* ProjectNavigatorOutlineView.swift */, + 285FEC6D27FE4B4A00E57D53 /* ProjectNavigatorViewController.swift */, + 285FEC6F27FE4B9800E57D53 /* ProjectNavigatorTableViewCell.swift */, + 285FEC7127FE4EEF00E57D53 /* ProjectNavigatorMenu.swift */, + D7DC4B75298FFBE900D6C83D /* ProjectNavigatorViewController+OutlineTableViewCellDelegate.swift */, ); path = OutlineView; sourceTree = ""; @@ -1006,6 +1012,7 @@ isa = PBXGroup; children = ( 286471AA27ED51FD0039369D /* ProjectNavigatorView.swift */, + 9396827C29D01D1600AB4A6B /* ProjectNavigatorToolbarBottom.swift */, 285FEC6C27FE4AC700E57D53 /* OutlineView */, ); path = ProjectNavigator; @@ -1014,13 +1021,13 @@ 287776EA27E350A100D46668 /* NavigatorSidebar */ = { isa = PBXGroup; children = ( + 9396827F29D01F0C00AB4A6B /* OutlineView */, 201169D52837B29600F92B46 /* SourceControlNavigator */, 0483E34E27FDB15F00354AC0 /* ExtensionNavigator */, 286471AC27ED52950039369D /* ProjectNavigator */, D7012EE627E757660001E1EF /* FindNavigator */, 287776E627E3413200D46668 /* NavigatorSidebarView.swift */, - 28B0A19727E385C300B73177 /* NavigatorSidebarToolbarTop.swift */, - 28FFE1BE27E3A441001939DB /* NavigatorSidebarToolbarBottom.swift */, + 28B0A19727E385C300B73177 /* NavigatorSidebarToolbar.swift */, ); path = NavigatorSidebar; sourceTree = ""; @@ -2320,6 +2327,16 @@ path = Text; sourceTree = ""; }; + 9396827F29D01F0C00AB4A6B /* OutlineView */ = { + isa = PBXGroup; + children = ( + 9396828229D01F1300AB4A6B /* FileSystemTableViewCell.swift */, + 9396828029D01F1300AB4A6B /* StandardTableViewCell.swift */, + 9396828129D01F1300AB4A6B /* TextTableViewCell.swift */, + ); + path = OutlineView; + sourceTree = ""; + }; B658FB2327DA9E0F00EA4DBD = { isa = PBXGroup; children = ( @@ -2689,7 +2706,7 @@ 58822534292C280D00E83CDE /* CursorLocation.swift in Sources */, 201169E52837B40300F92B46 /* SourceControlNavigatorRepositoriesView.swift in Sources */, 587B9E6A29301D8F00AC7927 /* GitLabPermissions.swift in Sources */, - D7DC4B76298FFBE900D6C83D /* OutlintViewController+OutlineTableViewCellDelegate.swift in Sources */, + D7DC4B76298FFBE900D6C83D /* ProjectNavigatorViewController+OutlineTableViewCellDelegate.swift in Sources */, 587B9E9229301D8F00AC7927 /* BitBucketAccount.swift in Sources */, 58F2EAFE292FB2B0004A9BDE /* GitHubEnterpriseLoginView.swift in Sources */, DE513F52281B672D002260B9 /* TabBarAccessory.swift in Sources */, @@ -2777,6 +2794,7 @@ B6C6A43029771F7100A3D28F /* TabBarItemBackground.swift in Sources */, 58F2EB0F292FB2B0004A9BDE /* PreferencesSection.swift in Sources */, 587B9E8C29301D8F00AC7927 /* GitHubOpenness.swift in Sources */, + 9396828429D01F1300AB4A6B /* TextTableViewCell.swift in Sources */, 587B9E8229301D8F00AC7927 /* GitHubPreviewHeader.swift in Sources */, 58F2EB02292FB2B0004A9BDE /* Loopable.swift in Sources */, 28B8F884280FFE4600596236 /* NSTableView+Background.swift in Sources */, @@ -2788,6 +2806,7 @@ 587B9E6629301D8F00AC7927 /* GitLabProjectHook.swift in Sources */, 587B9E9329301D8F00AC7927 /* BitBucketOAuthConfiguration.swift in Sources */, 6C18620A298BF5A800C663EA /* RecentProjectsListView.swift in Sources */, + 9396828329D01F1300AB4A6B /* StandardTableViewCell.swift in Sources */, 58F2EB0A292FB2B0004A9BDE /* AppPreferences.swift in Sources */, 20EBB503280C327C00F3A5DA /* HistoryInspectorView.swift in Sources */, 587B9E7529301D8F00AC7927 /* String+QueryParameters.swift in Sources */, @@ -2813,7 +2832,7 @@ 587B9E5C29301D8F00AC7927 /* Parameters.swift in Sources */, 58798235292E30B90085B254 /* FeedbackModel.swift in Sources */, 587B9D56292FC27A00AC7927 /* ExtensionManager.swift in Sources */, - 04C3255C2801F86900C8DA2D /* OutlineMenu.swift in Sources */, + 04C3255C2801F86900C8DA2D /* ProjectNavigatorMenu.swift in Sources */, 587B9E6429301D8F00AC7927 /* GitLabCommit.swift in Sources */, 58A5DFA229339F6400D1BD5D /* KeybindingManager.swift in Sources */, 58AFAA2E2933C69E00482B53 /* TabBarItemRepresentable.swift in Sources */, @@ -2845,7 +2864,7 @@ 287776E927E34BC700D46668 /* TabBarView.swift in Sources */, B60BE8BD297A167600841125 /* AcknowledgementRowView.swift in Sources */, 587B9E6329301D8F00AC7927 /* GitLabAccount.swift in Sources */, - 285FEC7027FE4B9800E57D53 /* OutlineTableViewCell.swift in Sources */, + 285FEC7027FE4B9800E57D53 /* ProjectNavigatorTableViewCell.swift in Sources */, 6CB9144B29BEC7F100BC47F2 /* WelcomeWindow.swift in Sources */, 58F2EAF8292FB2B0004A9BDE /* AccountListItemView.swift in Sources */, 587B9E7429301D8F00AC7927 /* URL+URLParameters.swift in Sources */, @@ -2871,7 +2890,7 @@ 6CFF967829BEBCF600182D6F /* MainCommands.swift in Sources */, 587B9E7129301D8F00AC7927 /* GitURLSession.swift in Sources */, 5882252C292C280D00E83CDE /* StatusBarDrawer.swift in Sources */, - 2847019E27FDDF7600F87B6B /* OutlineView.swift in Sources */, + 2847019E27FDDF7600F87B6B /* ProjectNavigatorOutlineView.swift in Sources */, 6C14CEB32877A68F001468FE /* FindNavigatorMatchListCell.swift in Sources */, 58F2EAF6292FB2B0004A9BDE /* AccountPreferencesView.swift in Sources */, 20EBB501280C325D00F3A5DA /* FileInspectorView.swift in Sources */, @@ -2940,6 +2959,7 @@ 587B9E5A29301D8F00AC7927 /* GitCloneView.swift in Sources */, 58D01C99293167DC00C5B6B4 /* String+MD5.swift in Sources */, 20EBB505280C329800F3A5DA /* HistoryInspectorItemView.swift in Sources */, + 9396827D29D01D1600AB4A6B /* ProjectNavigatorToolbarBottom.swift in Sources */, 58F2EAEA292FB2B0004A9BDE /* SourceControlGeneralView.swift in Sources */, 5878DAB2291D627C00DD95A3 /* PathBarView.swift in Sources */, 043C321627E3201F006AE443 /* WorkspaceDocument.swift in Sources */, @@ -2959,7 +2979,7 @@ 5878DAB0291D627C00DD95A3 /* PathBarMenu.swift in Sources */, 04C325512800AC7400C8DA2D /* ExtensionInstallationViewModel.swift in Sources */, 587B9DA529300ABD00AC7927 /* PressActionsModifier.swift in Sources */, - 28B0A19827E385C300B73177 /* NavigatorSidebarToolbarTop.swift in Sources */, + 28B0A19827E385C300B73177 /* NavigatorSidebarToolbar.swift in Sources */, 587B9E8629301D8F00AC7927 /* GitHubComment.swift in Sources */, 58F2EAE9292FB2B0004A9BDE /* SourceControlPreferencesView.swift in Sources */, 6C147C4029A328BC0089B630 /* SplitViewData.swift in Sources */, @@ -2971,6 +2991,7 @@ 6C2C155829B4F49100EA60A5 /* SplitViewItem.swift in Sources */, 58F2EAF0292FB2B0004A9BDE /* PreviewThemeView.swift in Sources */, 58F2EAFF292FB2B0004A9BDE /* KeybindingsPreferencesView.swift in Sources */, + 9396828529D01F1300AB4A6B /* FileSystemTableViewCell.swift in Sources */, 6CDA84AD284C1BA000C1CC3A /* TabBarContextMenu.swift in Sources */, 6C81916729B3E80700B75C92 /* ModifierKeysObserver.swift in Sources */, 0463E51127FCC1DF00806D5C /* CodeEditAPI.swift in Sources */, @@ -2979,7 +3000,7 @@ 58F2EB00292FB2B0004A9BDE /* LocationsPreferencesView.swift in Sources */, 58798267292EC4080085B254 /* APIPage.swift in Sources */, 5882252A292C280D00E83CDE /* StatusBarToggleDrawerButton.swift in Sources */, - 04C3255B2801F86400C8DA2D /* OutlineViewController.swift in Sources */, + 04C3255B2801F86400C8DA2D /* ProjectNavigatorViewController.swift in Sources */, 58798263292EC4080085B254 /* DownloadedPlugin.swift in Sources */, 587B9E6029301D8F00AC7927 /* GitLabOAuthRouter.swift in Sources */, 6C05A8AF284D0CA3007F4EAA /* WorkspaceDocument+Listeners.swift in Sources */, @@ -2988,7 +3009,6 @@ 6C2C155D29B4F4E500EA60A5 /* SplitViewReader.swift in Sources */, 58F2EAE8292FB2B0004A9BDE /* TerminalPreferencesView.swift in Sources */, 58AFAA2F2933C69E00482B53 /* TabBarItemID.swift in Sources */, - 28FFE1BF27E3A441001939DB /* NavigatorSidebarToolbarBottom.swift in Sources */, 6C4104E3297C87A000F472BA /* BlurButtonStyle.swift in Sources */, 58F2EAFA292FB2B0004A9BDE /* GitLabHostedLoginView.swift in Sources */, ); diff --git a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile.swift b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile.swift index 29d0d5bf2f..dffa16c3fb 100644 --- a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile.swift +++ b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile.swift @@ -160,7 +160,7 @@ final class CEWorkspaceFile: Codable, Comparable, Hashable, Identifiable, TabBar /// If it has children this will return `"folder.fill"` otherwise `"folder"`. private func folderIcon(_ children: [CEWorkspaceFile]) -> String { if self.parent == nil { - return "square.dashed.inset.filled" + return "folder.fill.badge.gearshape" } if self.name == ".codeedit" { return "folder.fill.badge.gearshape" @@ -390,10 +390,16 @@ final class CEWorkspaceFile: Codable, Comparable, Hashable, Identifiable, TabBar // This function also has to account for how the // - file system can change outside of the editor let deleteConfirmation = NSAlert() - let message = "\(String(describing: self.fileName))\(self.isFolder ? " and its children" : "")" - deleteConfirmation.messageText = "Do you want to move \(message) to the bin?" + let message: String + if self.isFolder || (self.children?.isEmpty ?? false) { // if its a file or an empty folder, call it by its name + message = String(describing: self.fileName) + } else { + message = "the \((self.children?.count ?? 0) + 1) selected items" + } + deleteConfirmation.messageText = "Do you want to move \(message) to the Trash?" + deleteConfirmation.informativeText = "This operation cannot be undone" deleteConfirmation.alertStyle = .critical - deleteConfirmation.addButton(withTitle: "Delete") + deleteConfirmation.addButton(withTitle: "Move to Trash") deleteConfirmation.buttons.last?.hasDestructiveAction = true deleteConfirmation.addButton(withTitle: "Cancel") if deleteConfirmation.runModal() == .alertFirstButtonReturn { // "Delete" button diff --git a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager.swift b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager.swift index 153827160f..cdbadd95c5 100644 --- a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager.swift +++ b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager.swift @@ -14,6 +14,7 @@ final class CEWorkspaceFileManager { case fileNotExist } + // TODO: See if this needs to be removed, it isn't used anymore private var subject = CurrentValueSubject<[CEWorkspaceFile], Never>([]) private var isRunning = false private var anotherInstanceRan = 0 @@ -165,7 +166,9 @@ final class CEWorkspaceFileManager { } subject.send(workspaceItem.children ?? []) - isRunning = false + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + self.isRunning = false + } anotherInstanceRan = 0 // reload data in outline view controller through the main thread diff --git a/CodeEdit/Features/CodeEditUI/Views/ToolbarBranchPicker.swift b/CodeEdit/Features/CodeEditUI/Views/ToolbarBranchPicker.swift index d984466572..810909976b 100644 --- a/CodeEdit/Features/CodeEditUI/Views/ToolbarBranchPicker.swift +++ b/CodeEdit/Features/CodeEditUI/Views/ToolbarBranchPicker.swift @@ -47,7 +47,7 @@ struct ToolbarBranchPicker: View { .imageScale(.large) .foregroundColor(controlActive == .inactive ? inactiveColor : .primary) } else { - Image(systemName: "square.dashed.inset.filled") + Image(systemName: "folder.fill.badge.gearshape") .font(.title3) .imageScale(.medium) .foregroundColor(controlActive == .inactive ? inactiveColor : .accentColor) diff --git a/CodeEdit/Features/Documents/WorkspaceDocument.swift b/CodeEdit/Features/Documents/WorkspaceDocument.swift index 8b85596fa3..1e97f78f79 100644 --- a/CodeEdit/Features/Documents/WorkspaceDocument.swift +++ b/CodeEdit/Features/Documents/WorkspaceDocument.swift @@ -16,9 +16,6 @@ import CodeEditKit @Published var sortFoldersOnTop: Bool = true - @Published - var fileItems: [CEWorkspaceFile] = [] - @Published var targets: [Target] = [] @@ -39,6 +36,10 @@ import CodeEditKit } } + public var filter: String = "" { + didSet { workspaceFileManager?.onRefresh() } + } + var statusBarModel = StatusBarViewModel() var searchState: SearchState? var quickOpenViewModel: QuickOpenViewModel? @@ -136,33 +137,6 @@ import CodeEditKit override func read(from url: URL, ofType typeName: String) throws { try initWorkspaceState(url) - - // Initialize Workspace - workspaceFileManager? - .getFiles - .sink { [weak self] files in - guard let self = self else { return } - - guard !self.fileItems.isEmpty else { - self.fileItems = files - return - } - - // Instead of rebuilding the array we want to - // calculate the difference between the last iteration - // and now. If the index of the file exists in the array - // it means we need to remove the element, otherwise we need to append - // it. - let diff = files.difference(from: self.fileItems) - diff.forEach { newFile in - if let index = self.fileItems.firstIndex(of: newFile) { - self.fileItems.remove(at: index) - } else { - self.fileItems.append(newFile) - } - } - } - .store(in: &cancellables) } override func write(to url: URL, ofType typeName: String) throws {} diff --git a/CodeEdit/Features/NavigatorSidebar/FindNavigator/FindNavigatorResultList/FindNavigatorListViewController.swift b/CodeEdit/Features/NavigatorSidebar/FindNavigator/FindNavigatorResultList/FindNavigatorListViewController.swift index e34ee570d6..cc116cda8b 100644 --- a/CodeEdit/Features/NavigatorSidebar/FindNavigator/FindNavigatorResultList/FindNavigatorListViewController.swift +++ b/CodeEdit/Features/NavigatorSidebar/FindNavigator/FindNavigatorResultList/FindNavigatorListViewController.swift @@ -180,7 +180,7 @@ extension FindNavigatorListViewController: NSOutlineViewDelegate { width: tableColumn.width, height: prefs.general.projectNavigatorSize.rowHeight ) - let view = OutlineTableViewCell( + let view = ProjectNavigatorTableViewCell( frame: frameRect, item: (item as? SearchResultModel)?.file, isEditable: false diff --git a/CodeEdit/Features/NavigatorSidebar/NavigatorSidebarToolbarTop.swift b/CodeEdit/Features/NavigatorSidebar/NavigatorSidebarToolbar.swift similarity index 68% rename from CodeEdit/Features/NavigatorSidebar/NavigatorSidebarToolbarTop.swift rename to CodeEdit/Features/NavigatorSidebar/NavigatorSidebarToolbar.swift index f116f3a464..e8d83ed200 100644 --- a/CodeEdit/Features/NavigatorSidebar/NavigatorSidebarToolbarTop.swift +++ b/CodeEdit/Features/NavigatorSidebar/NavigatorSidebarToolbar.swift @@ -8,10 +8,12 @@ import SwiftUI import CodeEditSymbols -struct NavigatorSidebarToolbarTop: View { +struct NavigatorSidebarToolbar: View { @Environment(\.controlActiveState) private var activeState + var alignment: SidebarToolbarAlignment + @Binding private var selection: Int @@ -30,44 +32,80 @@ struct NavigatorSidebarToolbarTop: View { @State private var draggingItem: SidebarDockIcon? @State private var drugItemLocation: CGPoint? - init(selection: Binding) { + init(selection: Binding, alignment: SidebarToolbarAlignment) { self._selection = selection + self.alignment = alignment } var body: some View { + if alignment == .top { + topBody + } else { + leadingBody + } + } + + var topBody: some View { GeometryReader { proxy in - HStack(spacing: 0) { - ForEach(icons) { icon in - makeIcon(named: icon.imageName, title: icon.title, id: icon.id, sidebarWidth: proxy.size.width) - .opacity(draggingItem?.imageName == icon.imageName && - hasChangedLocation && - drugItemLocation != nil ? 0.0: icon.disabled ? 0.3 : 1.0) - .onDrop( - of: [.utf8PlainText], - delegate: NavigatorSidebarDockIconDelegate( - item: icon, - current: $draggingItem, - icons: $icons, - hasChangedLocation: $hasChangedLocation, - drugItemLocation: $drugItemLocation - ) - ) - .disabled(icon.disabled) + iconsView(size: proxy.size) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .overlay(alignment: .top) { + Divider() } - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - .overlay(alignment: .top) { - Divider() - } - .overlay(alignment: .bottom) { - Divider() - } - .animation(.default, value: icons) + .overlay(alignment: .bottom) { + Divider() + } + .animation(.default, value: icons) } .frame(maxWidth: .infinity, idealHeight: 29) .fixedSize(horizontal: false, vertical: true) } + var leadingBody: some View { + GeometryReader { proxy in + iconsView(size: proxy.size) + .padding(.vertical, 8) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .overlay(alignment: .trailing) { + HStack { + Divider() + } + } + .animation(.default, value: icons) + } + .frame(idealWidth: 29, maxHeight: .infinity) + .fixedSize(horizontal: true, vertical: false) + } + + @ViewBuilder + func iconsView(size: CGSize) -> some View { + let layout = alignment == .top ? + AnyLayout(HStackLayout(spacing: 0)) : + AnyLayout(VStackLayout(spacing: 10)) + layout { + ForEach(icons) { icon in + makeIcon(named: icon.imageName, title: icon.title, id: icon.id, sidebarWidth: size.width) + .opacity(draggingItem?.imageName == icon.imageName && + hasChangedLocation && + drugItemLocation != nil ? 0.0: icon.disabled ? 0.3 : 1.0) + .onDrop( + of: [.utf8PlainText], + delegate: NavigatorSidebarDockIconDelegate( + item: icon, + current: $draggingItem, + icons: $icons, + hasChangedLocation: $hasChangedLocation, + drugItemLocation: $drugItemLocation + ) + ) + .disabled(icon.disabled) + } + if alignment == .leading { + Spacer() + } + } + } + private func makeIcon( named: String, title: String, diff --git a/CodeEdit/Features/NavigatorSidebar/NavigatorSidebarView.swift b/CodeEdit/Features/NavigatorSidebar/NavigatorSidebarView.swift index d30f52fcde..3d9fe067e6 100644 --- a/CodeEdit/Features/NavigatorSidebar/NavigatorSidebarView.swift +++ b/CodeEdit/Features/NavigatorSidebar/NavigatorSidebarView.swift @@ -20,6 +20,8 @@ struct NavigatorSidebarView: View { self.workspace = workspace } + var sidebarAlignment: SidebarToolbarAlignment = .top + var body: some View { VStack { switch selection { @@ -35,19 +37,31 @@ struct NavigatorSidebarView: View { Spacer() } } + .padding(.top, sidebarAlignment == .leading ? toolbarPadding : 0) + .safeAreaInset(edge: .leading) { + if sidebarAlignment == .leading { + NavigatorSidebarToolbar(selection: $selection, alignment: sidebarAlignment) + .padding(.top, toolbarPadding) + .padding(.trailing, toolbarPadding) + } + } .safeAreaInset(edge: .top) { - NavigatorSidebarToolbarTop(selection: $selection) - .padding(.bottom, toolbarPadding) + if sidebarAlignment == .top { + NavigatorSidebarToolbar(selection: $selection, alignment: sidebarAlignment) + .padding(.bottom, toolbarPadding) + } else { + Divider() + } } .safeAreaInset(edge: .bottom) { Group { switch selection { case 0: - NavigatorSidebarToolbarBottom() + ProjectNavigatorToolbarBottom() case 1: SourceControlToolbarBottom() - default: - NavigatorSidebarToolbarBottom() + default: // TODO: As we implement more sidebars, put their bottom toolbars here. + EmptyView() } } .padding(.top, toolbarPadding) @@ -55,3 +69,7 @@ struct NavigatorSidebarView: View { .environmentObject(workspace) } } + +enum SidebarToolbarAlignment { + case top, leading +} diff --git a/CodeEdit/Features/NavigatorSidebar/OutlineView/FileSystemTableViewCell.swift b/CodeEdit/Features/NavigatorSidebar/OutlineView/FileSystemTableViewCell.swift new file mode 100644 index 0000000000..81e0fb1716 --- /dev/null +++ b/CodeEdit/Features/NavigatorSidebar/OutlineView/FileSystemTableViewCell.swift @@ -0,0 +1,149 @@ +// +// FileSystemOutlineView.swift +// CodeEdit +// +// Created by TAY KAI QUAN on 14/8/22. +// + +import SwiftUI + +class FileSystemTableViewCell: StandardTableViewCell { + + var fileItem: CEWorkspaceFile! + + var changeLabelLargeWidth: NSLayoutConstraint! + var changeLabelSmallWidth: NSLayoutConstraint! + + private let prefs = AppPreferencesModel.shared.preferences.general + + /// Initializes the `OutlineTableViewCell` with an `icon` and `label` + /// Both the icon and label will be colored, and sized based on the user's preferences. + /// - Parameters: + /// - frameRect: The frame of the cell. + /// - item: The file item the cell represents. + /// - isEditable: Set to true if the user should be able to edit the file name. + init(frame frameRect: NSRect, item: CEWorkspaceFile?, isEditable: Bool = true) { + super.init(frame: frameRect, isEditable: isEditable) + + if let item = item { + addIcon(item: item) + } + addModel() + } + + override func configLabel(label: NSTextField, isEditable: Bool) { + super.configLabel(label: label, isEditable: isEditable) + label.delegate = self + } + + func addIcon(item: CEWorkspaceFile) { + var imageName = item.systemImage + if item.watcherCode == nil { + imageName = "exclamationmark.arrow.triangle.2.circlepath" + } + if item.watcher == nil && !item.activateWatcher() { + // watcher failed to activate + imageName = "eye.trianglebadge.exclamationmark" + } + let image = NSImage(systemSymbolName: imageName, accessibilityDescription: nil)! + fileItem = item + icon.image = image + icon.contentTintColor = color(for: item) + toolTip = label(for: item) + label.stringValue = label(for: item) + } + + func addModel() { + secondaryLabel.stringValue = fileItem.gitStatus?.description ?? "" + if secondaryLabel.stringValue == "?" { secondaryLabel.stringValue = "A" } + } + + /// *Not Implemented* + override init(frame frameRect: NSRect) { + super.init(frame: frameRect) + fatalError(""" + init(frame: ) isn't implemented on `OutlineTableViewCell`. + Please use `.init(frame: NSRect, item: FileSystemClient.FileItem?) + """) + } + + /// *Not Implemented* + required init?(coder: NSCoder) { + fatalError(""" + init?(coder: NSCoder) isn't implemented on `OutlineTableViewCell`. + Please use `.init(frame: NSRect, item: FileSystemClient.FileItem?) + """) + } + + /// Returns the font size for the current row height. Defaults to `13.0` + private var fontSize: Double { + switch self.frame.height { + case 20: return 11 + case 22: return 13 + case 24: return 14 + default: return 13 + } + } + + /// Generates a string based on user's file name preferences. + /// - Parameter item: The FileItem to generate the name for. + /// - Returns: A `String` with the name to display. + func label(for item: CEWorkspaceFile) -> String { + switch prefs.fileExtensionsVisibility { + case .hideAll: + return item.fileName(typeHidden: true) + case .showAll: + return item.fileName(typeHidden: false) + case .showOnly: + return item.fileName(typeHidden: !prefs.shownFileExtensions.extensions.contains(item.type.rawValue)) + case .hideOnly: + return item.fileName(typeHidden: prefs.hiddenFileExtensions.extensions.contains(item.type.rawValue)) + } + } + + /// Get the appropriate color for the items icon depending on the users preferences. + /// - Parameter item: The `FileItem` to get the color for + /// - Returns: A `NSColor` for the given `FileItem`. + func color(for item: CEWorkspaceFile) -> NSColor { + if item.children == nil && prefs.fileIconStyle == .color { + return NSColor(item.iconColor) + } else { + return NSColor(named: "FolderBlue")! + } + } +} + +let errorRed = NSColor(red: 1, green: 0, blue: 0, alpha: 0.2) +extension FileSystemTableViewCell: NSTextFieldDelegate { + func controlTextDidChange(_ obj: Notification) { + label.backgroundColor = validateFileName(for: label?.stringValue ?? "") ? .none : errorRed + } + func controlTextDidEndEditing(_ obj: Notification) { + label.backgroundColor = validateFileName(for: label?.stringValue ?? "") ? .none : errorRed + if validateFileName(for: label?.stringValue ?? "") { + fileItem.move(to: fileItem.url.deletingLastPathComponent() + .appendingPathComponent(label?.stringValue ?? "")) + } else { + label?.stringValue = label(for: fileItem) + } + } + + func validateFileName(for newName: String) -> Bool { + guard newName != label(for: fileItem) else { return true } + + guard !newName.isEmpty && newName.isValidFilename && + !FileManager.default.fileExists(atPath: + fileItem.url.deletingLastPathComponent().appendingPathComponent(newName).path) + else { return false } + + return true + } +} + +extension String { + var isValidFilename: Bool { + let regex = "[^:]" + let testString = NSPredicate(format: "SELF MATCHES %@", regex) + return !testString.evaluate(with: self) + } +} diff --git a/CodeEdit/Features/NavigatorSidebar/OutlineView/StandardTableViewCell.swift b/CodeEdit/Features/NavigatorSidebar/OutlineView/StandardTableViewCell.swift new file mode 100644 index 0000000000..1fb74c68ee --- /dev/null +++ b/CodeEdit/Features/NavigatorSidebar/OutlineView/StandardTableViewCell.swift @@ -0,0 +1,198 @@ +// +// StandardTableViewCell.swift +// CodeEdit +// +// Created by TAY KAI QUAN on 17/8/22. +// + +import SwiftUI + +class StandardTableViewCell: NSTableCellView { + + var label: NSTextField! + var secondaryLabel: NSTextField! + var icon: NSImageView! + + var workspace: WorkspaceDocument? + + var secondaryLabelRightAlignmed: Bool = true { + didSet { + resizeSubviews(withOldSize: .zero) + } + } + + private let prefs = AppPreferencesModel.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: + /// - frameRect: The frame of the cell. + /// - item: The file item the cell represents. + /// - isEditable: Set to true if the user should be able to edit the file name. + init(frame frameRect: NSRect, isEditable: Bool = true) { + super.init(frame: frameRect) + setupViews(frame: frameRect, isEditable: isEditable) + } + + // Default init, assumes isEditable to be false + override init(frame frameRect: NSRect) { + super.init(frame: frameRect) + setupViews(frame: frameRect, isEditable: false) + } + + private func setupViews(frame frameRect: NSRect, isEditable: Bool) { + // Create the label + label = createLabel() + configLabel(label: self.label, isEditable: isEditable) + self.textField = label + + // Create the secondary label + secondaryLabel = createSecondaryLabel() + configSecondaryLabel(secondaryLabel: secondaryLabel) + + // Create the icon + icon = createIcon() + configIcon(icon: icon) + addSubview(icon) + imageView = icon + + // add constraints + createConstraints(frame: frameRect) + addSubview(label) + addSubview(secondaryLabel) + addSubview(icon) + } + + // MARK: Create and config stuff + func createLabel() -> NSTextField { + return SpecialSelectTextField(frame: .zero) + } + + func configLabel(label: NSTextField, isEditable: Bool) { + label.translatesAutoresizingMaskIntoConstraints = false + label.drawsBackground = false + label.isBordered = false + label.isEditable = isEditable + label.isSelectable = isEditable + label.layer?.cornerRadius = 10.0 + label.font = .labelFont(ofSize: fontSize) + label.lineBreakMode = .byTruncatingMiddle + } + + func createSecondaryLabel() -> NSTextField { + return NSTextField(frame: .zero) + } + + func configSecondaryLabel(secondaryLabel: NSTextField) { + secondaryLabel.translatesAutoresizingMaskIntoConstraints = false + secondaryLabel.drawsBackground = false + secondaryLabel.isBordered = false + secondaryLabel.isEditable = false + secondaryLabel.isSelectable = false + secondaryLabel.layer?.cornerRadius = 10.0 + secondaryLabel.font = .systemFont(ofSize: fontSize) + secondaryLabel.alignment = .center + secondaryLabel.textColor = NSColor(Color.secondary) + } + + func createIcon() -> NSImageView { + return NSImageView(frame: .zero) + } + + func configIcon(icon: NSImageView) { + icon.translatesAutoresizingMaskIntoConstraints = false + icon.symbolConfiguration = .init(pointSize: fontSize, weight: .regular, scale: .medium) + } + + func createConstraints(frame frameRect: NSRect) { + resizeSubviews(withOldSize: .zero) + } + + let iconWidth: CGFloat = 22 + override func resizeSubviews(withOldSize oldSize: NSSize) { + super.resizeSubviews(withOldSize: oldSize) + + icon.frame = NSRect( + x: 2, + y: 4, + width: iconWidth, + height: frame.height + ) + // center align the image + if let alignmentRect = icon.image?.alignmentRect { + icon.frame = NSRect( + x: (iconWidth - alignmentRect.width) / 2, + y: 4, + width: alignmentRect.width, + height: frame.height + ) + } + + // 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 + } 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 + ) + } + } + + /// *Not Implemented* + required init?(coder: NSCoder) { + fatalError(""" + init?(coder: NSCoder) isn't implemented on `StandardTableViewCell`. + Please use `.init(frame: NSRect, isEditable: Bool) + """) + } + + /// Returns the font size for the current row height. Defaults to `13.0` + private var fontSize: Double { + switch self.frame.height { + case 20: return 11 + case 22: return 13 + case 24: return 14 + default: return 13 + } + } + + class SpecialSelectTextField: NSTextField { +// override func becomeFirstResponder() -> Bool { + // TODO: Set text range + // this is the code to get the text range, however I cannot find a way to select it :( +// NSRange(location: 0, length: stringValue.distance(from: stringValue.startIndex, +// to: stringValue.lastIndex(of: ".") ?? stringValue.endIndex)) +// return true +// } + } +} diff --git a/CodeEdit/Features/NavigatorSidebar/OutlineView/TextTableViewCell.swift b/CodeEdit/Features/NavigatorSidebar/OutlineView/TextTableViewCell.swift new file mode 100644 index 0000000000..d72deef968 --- /dev/null +++ b/CodeEdit/Features/NavigatorSidebar/OutlineView/TextTableViewCell.swift @@ -0,0 +1,81 @@ +// +// TextTableViewCell.swift +// CodeEdit +// +// Created by TAY KAI QUAN on 11/9/22. +// + +import SwiftUI + +class TextTableViewCell: NSTableCellView { + + var label: NSTextField! + + init(frame frameRect: NSRect, isEditable: Bool = true, startingText: String = "") { + super.init(frame: frameRect) + setupViews(frame: frameRect, isEditable: isEditable) + self.label.stringValue = startingText + } + + // Default init, assumes isEditable to be false + override init(frame frameRect: NSRect) { + super.init(frame: frameRect) + setupViews(frame: frameRect, isEditable: false) + } + + private func setupViews(frame frameRect: NSRect, isEditable: Bool) { + // Create the label + label = createLabel() + configLabel(label: self.label, isEditable: isEditable) + self.textField = label + + addSubview(label) + createConstraints(frame: frameRect) + } + + // MARK: Create and config stuff + func createLabel() -> NSTextField { + return NSTextField(frame: .zero) + } + + func configLabel(label: NSTextField, isEditable: Bool) { + label.translatesAutoresizingMaskIntoConstraints = false + label.drawsBackground = false + label.isBordered = false + label.isEditable = isEditable + label.isSelectable = isEditable + label.layer?.cornerRadius = 10.0 + label.font = .boldSystemFont(ofSize: fontSize) + label.lineBreakMode = .byTruncatingMiddle + label.textColor = NSColor.textColor + label.alphaValue = 0.7 + } + + func createConstraints(frame frameRect: NSRect) { + resizeSubviews(withOldSize: .zero) + } + + override func resizeSubviews(withOldSize oldSize: NSSize) { + super.resizeSubviews(withOldSize: oldSize) + label.frame = NSRect(x: 2, y: 2.5, + width: frame.width - 4, height: 25) + } + + /// Returns the font size for the current row height. Defaults to `13.0` + private var fontSize: Double { + switch self.frame.height { + case 20: return 11 + case 22: return 13 + case 24: return 14 + default: return 13 + } + } + + /// *Not Implemented* + required init(coder: NSCoder) { + fatalError(""" + init?(coder: NSCoder) isn't implemented on `TextTableViewCell`. + Please use `.init(frame: NSRect, isEditable: Bool) + """) + } +} diff --git a/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/OutlineTableViewCell.swift b/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/OutlineTableViewCell.swift deleted file mode 100644 index 478cf66137..0000000000 --- a/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/OutlineTableViewCell.swift +++ /dev/null @@ -1,180 +0,0 @@ -// -// OutlineTableViewCell.swift -// CodeEdit -// -// Created by Lukas Pistrol on 07.04.22. -// - -import SwiftUI - -protocol OutlineTableViewCellDelegate: AnyObject { - func moveFile(file: CEWorkspaceFile, to destination: URL) - func copyFile(file: CEWorkspaceFile, to destination: URL) -} - -/// A `NSTableCellView` showing an ``icon`` and a ``label`` -final class OutlineTableViewCell: NSTableCellView { - - var label: NSTextField! - var icon: NSImageView! - private var fileItem: CEWorkspaceFile! - private var delegate: OutlineTableViewCellDelegate? - - private let prefs = AppPreferencesModel.shared.preferences.general - - /// Initializes the `OutlineTableViewCell` with an `icon` and `label` - /// Both the icon and label will be colored, and sized based on the user's preferences. - /// - Parameters: - /// - frameRect: The frame of the cell. - /// - item: The file item the cell represents. - /// - isEditable: Set to true if the user should be able to edit the file name. - init( - frame frameRect: NSRect, item: CEWorkspaceFile?, - isEditable: Bool = true, - delegate: OutlineTableViewCellDelegate? = nil - ) { - super.init(frame: frameRect) - - self.delegate = delegate - - // Create the label - - self.label = NSTextField(frame: .zero) - self.label.translatesAutoresizingMaskIntoConstraints = false - self.label.drawsBackground = false - self.label.isBordered = false - self.label.isEditable = isEditable - self.label.isSelectable = isEditable - self.label.delegate = self - self.label.layer?.cornerRadius = 10.0 - self.label.font = .labelFont(ofSize: fontSize) - self.label.lineBreakMode = .byTruncatingMiddle - - self.addSubview(label) - self.textField = label - - // Create the icon - - self.icon = NSImageView(frame: .zero) - self.icon.translatesAutoresizingMaskIntoConstraints = false - self.icon.symbolConfiguration = .init(pointSize: fontSize, weight: .regular, scale: .medium) - - self.addSubview(icon) - self.imageView = icon - - // Icon constraints - - self.icon.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: -2).isActive = true - self.icon.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true - self.icon.widthAnchor.constraint(equalToConstant: 25).isActive = true - self.icon.heightAnchor.constraint(equalToConstant: frameRect.height).isActive = true - - // Label constraints - - self.label.leadingAnchor.constraint(equalTo: icon.trailingAnchor, constant: 1).isActive = true - self.label.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: 1).isActive = true - self.label.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true - - if let item = item { - let image = NSImage(systemSymbolName: item.systemImage, accessibilityDescription: nil)! - fileItem = item - icon.image = image - icon.contentTintColor = color(for: item) - - label.stringValue = label(for: item) - } - } - - /// *Not Implemented* - override init(frame frameRect: NSRect) { - super.init(frame: frameRect) - fatalError(""" - init(frame: ) isn't implemented on `OutlineTableViewCell`. - Please use `.init(frame: NSRect, item: WorkspaceClient.FileItem?) - """) - } - - /// *Not Implemented* - required init?(coder: NSCoder) { - fatalError(""" - init?(coder: NSCoder) isn't implemented on `OutlineTableViewCell`. - Please use `.init(frame: NSRect, item: WorkspaceClient.FileItem?) - """) - } - - /// Returns the font size for the current row height. Defaults to `13.0` - private var fontSize: Double { - switch self.frame.height { - case 20: return 11 - case 22: return 13 - case 24: return 14 - default: return 13 - } - } - - /// Generates a string based on user's file name preferences. - /// - Parameter item: The FileItem to generate the name for. - /// - Returns: A `String` with the name to display. - private func label(for item: CEWorkspaceFile) -> String { - switch prefs.fileExtensionsVisibility { - case .hideAll: - return item.fileName(typeHidden: true) - case .showAll: - return item.fileName(typeHidden: false) - case .showOnly: - return item.fileName(typeHidden: !prefs.shownFileExtensions.extensions.contains(item.type.rawValue)) - case .hideOnly: - return item.fileName(typeHidden: prefs.hiddenFileExtensions.extensions.contains(item.type.rawValue)) - } - } - - /// Get the appropriate color for the items icon depending on the users preferences. - /// - Parameter item: The `FileItem` to get the color for - /// - Returns: A `NSColor` for the given `FileItem`. - private func color(for item: CEWorkspaceFile) -> NSColor { - return prefs.fileIconStyle == .color - ? item.children == nil ? NSColor(item.iconColor) : NSColor(named: "FolderBlue")! - : .secondaryLabelColor - } -} - -extension OutlineTableViewCell: NSTextFieldDelegate { - var errorRed: NSColor { .init(red: 1, green: 0, blue: 0, alpha: 0.2) } - - func controlTextDidChange(_ obj: Notification) { - print("Contents changed to \(label?.stringValue ?? "idk")") - print("File validity: \(validateFileName(for: label?.stringValue ?? ""))") - label.backgroundColor = validateFileName(for: label?.stringValue ?? "") ? .none : errorRed - } - func controlTextDidEndEditing(_ obj: Notification) { - print("File validity: \(validateFileName(for: label?.stringValue ?? ""))") - label.backgroundColor = validateFileName(for: label?.stringValue ?? "") ? .none : errorRed - if validateFileName(for: label?.stringValue ?? "") { - let destinationURL = fileItem.url - .deletingLastPathComponent() - .appendingPathComponent(label?.stringValue ?? "") - delegate?.moveFile(file: fileItem, to: destinationURL) - } else { - label?.stringValue = fileItem.name - } - } - - func validateFileName(for newName: String) -> Bool { - guard newName != fileItem.name else { return true } - - guard newName != "" && newName.isValidFilename && - !CEWorkspaceFile.fileManger.fileExists(atPath: - fileItem.url.deletingLastPathComponent().appendingPathComponent(newName).path) - else { return false } - - return true - } -} - -extension String { - var isValidFilename: Bool { - let regex = "[^:]" - let testString = NSPredicate(format: "SELF MATCHES %@", regex) - return !testString.evaluate(with: self) - } -} diff --git a/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/OutlineMenu.swift b/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/ProjectNavigatorMenu.swift similarity index 98% rename from CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/OutlineMenu.swift rename to CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/ProjectNavigatorMenu.swift index 412f4abab6..6f74a5bd9e 100644 --- a/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/OutlineMenu.swift +++ b/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/ProjectNavigatorMenu.swift @@ -9,7 +9,7 @@ import SwiftUI import UniformTypeIdentifiers /// A subclass of `NSMenu` implementing the contextual menu for the project navigator -final class OutlineMenu: NSMenu { +final class ProjectNavigatorMenu: NSMenu { /// The item to show the contextual menu for var item: CEWorkspaceFile? @@ -213,7 +213,7 @@ final class OutlineMenu: NSMenu { private func renameFile() { let row = outlineView.row(forItem: item) guard row > 0, - let cell = outlineView.view(atColumn: 0, row: row, makeIfNecessary: false) as? OutlineTableViewCell else { + let cell = outlineView.view(atColumn: 0, row: row, makeIfNecessary: false) as? ProjectNavigatorTableViewCell else { return } outlineView.window?.makeFirstResponder(cell.textField) diff --git a/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/OutlineView.swift b/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift similarity index 76% rename from CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/OutlineView.swift rename to CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift index 90a31828e0..6b8fc9595b 100644 --- a/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/OutlineView.swift +++ b/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift @@ -9,7 +9,7 @@ import SwiftUI import Combine /// Wraps an ``OutlineViewController`` inside a `NSViewControllerRepresentable` -struct OutlineView: NSViewControllerRepresentable { +struct ProjectNavigatorOutlineView: NSViewControllerRepresentable { @EnvironmentObject var workspace: WorkspaceDocument @@ -21,19 +21,22 @@ struct OutlineView: NSViewControllerRepresentable { @Binding var selection: CEWorkspaceFile? - typealias NSViewControllerType = OutlineViewController + typealias NSViewControllerType = ProjectNavigatorViewController - func makeNSViewController(context: Context) -> OutlineViewController { - let controller = OutlineViewController() + func makeNSViewController(context: Context) -> ProjectNavigatorViewController { + let controller = ProjectNavigatorViewController() controller.workspace = workspace controller.iconColor = prefs.preferences.general.fileIconStyle + workspace.workspaceFileManager?.onRefresh = { + controller.outlineView.reloadData() + } context.coordinator.controller = controller return controller } - func updateNSViewController(_ nsViewController: OutlineViewController, context: Context) { + func updateNSViewController(_ nsViewController: ProjectNavigatorViewController, context: Context) { nsViewController.iconColor = prefs.preferences.general.fileIconStyle nsViewController.rowHeight = prefs.preferences.general.projectNavigatorSize.rowHeight nsViewController.fileExtensionsVisibility = prefs.preferences.general.fileExtensionsVisibility @@ -63,7 +66,7 @@ struct OutlineView: NSViewControllerRepresentable { var listener: AnyCancellable? var workspace: WorkspaceDocument - var controller: OutlineViewController? + var controller: ProjectNavigatorViewController? deinit { listener?.cancel() diff --git a/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/ProjectNavigatorTableViewCell.swift b/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/ProjectNavigatorTableViewCell.swift new file mode 100644 index 0000000000..389488a8ff --- /dev/null +++ b/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/ProjectNavigatorTableViewCell.swift @@ -0,0 +1,64 @@ +// +// OutlineTableViewCell.swift +// CodeEdit +// +// Created by Lukas Pistrol on 07.04.22. +// + +import SwiftUI + +protocol OutlineTableViewCellDelegate: AnyObject { + func moveFile(file: CEWorkspaceFile, to destination: URL) + func copyFile(file: CEWorkspaceFile, to destination: URL) +} + +/// A `NSTableCellView` showing an ``icon`` and a ``label`` +final class ProjectNavigatorTableViewCell: FileSystemTableViewCell { + private 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. + /// - Parameters: + /// - frameRect: The frame of the cell. + /// - item: The file item the cell represents. + /// - isEditable: Set to true if the user should be able to edit the file name. + init( + frame frameRect: NSRect, + item: CEWorkspaceFile?, + isEditable: Bool = true, + delegate: OutlineTableViewCellDelegate? = nil + ) { + super.init(frame: frameRect, item: item, isEditable: isEditable) + + self.delegate = delegate + } + + /// *Not Implemented* + override init(frame frameRect: NSRect) { + super.init(frame: frameRect) + fatalError(""" + init(frame: ) isn't implemented on `OutlineTableViewCell`. + Please use `.init(frame: NSRect, item: WorkspaceClient.FileItem?) + """) + } + + /// *Not Implemented* + required init?(coder: NSCoder) { + fatalError(""" + init?(coder: NSCoder) isn't implemented on `OutlineTableViewCell`. + Please use `.init(frame: NSRect, item: WorkspaceClient.FileItem?) + """) + } + + override func controlTextDidEndEditing(_ obj: Notification) { + label.backgroundColor = validateFileName(for: label?.stringValue ?? "") ? .none : errorRed + if validateFileName(for: label?.stringValue ?? "") { + let destinationURL = fileItem.url + .deletingLastPathComponent() + .appendingPathComponent(label?.stringValue ?? "") + delegate?.moveFile(file: fileItem, to: destinationURL) + } else { + label?.stringValue = fileItem.name + } + } +} diff --git a/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/OutlintViewController+OutlineTableViewCellDelegate.swift b/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/ProjectNavigatorViewController+OutlineTableViewCellDelegate.swift similarity index 88% rename from CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/OutlintViewController+OutlineTableViewCellDelegate.swift rename to CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/ProjectNavigatorViewController+OutlineTableViewCellDelegate.swift index 911067010e..1f23f4f136 100644 --- a/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/OutlintViewController+OutlineTableViewCellDelegate.swift +++ b/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/ProjectNavigatorViewController+OutlineTableViewCellDelegate.swift @@ -9,7 +9,7 @@ import Foundation // MARK: - OutlineTableViewCellDelegate -extension OutlineViewController: OutlineTableViewCellDelegate { +extension ProjectNavigatorViewController: OutlineTableViewCellDelegate { func moveFile(file: CEWorkspaceFile, to destination: URL) { if !file.isFolder { workspace?.tabManager.tabGroups.closeAllTabs(of: file) diff --git a/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/OutlineViewController.swift b/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift similarity index 93% rename from CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/OutlineViewController.swift rename to CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift index 6462e08360..7a270bc4fe 100644 --- a/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/OutlineViewController.swift +++ b/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift @@ -11,7 +11,7 @@ import SwiftUI /// /// Adds a ``outlineView`` inside a ``scrollView`` which shows the folder structure of the /// currently open project. -final class OutlineViewController: NSViewController { +final class ProjectNavigatorViewController: NSViewController { var scrollView: NSScrollView! var outlineView: NSOutlineView! @@ -21,9 +21,7 @@ final class OutlineViewController: NSViewController { /// Also creates a top level item "root" which represents the projects root directory and automatically expands it. private var content: [CEWorkspaceFile] { guard let folderURL = workspace?.workspaceFileManager?.folderUrl else { return [] } - let children = workspace?.fileItems.sortItems(foldersOnTop: true) guard let root = try? workspace?.workspaceFileManager?.getFileItem(folderURL.path) else { return [] } - root.children = children return [root] } @@ -58,7 +56,7 @@ final class OutlineViewController: NSViewController { self.outlineView.autosaveExpandedItems = true self.outlineView.autosaveName = workspace?.workspaceFileManager?.folderUrl.path ?? "" self.outlineView.headerView = nil - self.outlineView.menu = OutlineMenu(sender: self.outlineView) + self.outlineView.menu = ProjectNavigatorMenu(sender: self.outlineView) self.outlineView.menu?.delegate = self self.outlineView.doubleAction = #selector(onItemDoubleClicked) @@ -69,12 +67,14 @@ final class OutlineViewController: NSViewController { outlineView.setDraggingSourceOperationMask(.move, forLocal: false) outlineView.registerForDraggedTypes([.fileURL]) - self.scrollView.documentView = outlineView - self.scrollView.contentView.automaticallyAdjustsContentInsets = false - self.scrollView.contentView.contentInsets = .init(top: 10, left: 0, bottom: 0, right: 0) + scrollView.documentView = outlineView + scrollView.contentView.automaticallyAdjustsContentInsets = false + scrollView.contentView.contentInsets = .init(top: 10, left: 0, bottom: 0, right: 0) + scrollView.scrollerStyle = .overlay + scrollView.hasVerticalScroller = true + scrollView.hasHorizontalScroller = false + scrollView.autohidesScrollers = true - // TODO: Kai needs to replace this with his implementation of the sidebar -// WorkspaceClient.onRefresh = self.outlineView.reloadData outlineView.expandItem(outlineView.item(atRow: 0)) } @@ -125,11 +125,12 @@ final class OutlineViewController: NSViewController { } } + // TODO: File filtering } // MARK: - NSOutlineViewDataSource -extension OutlineViewController: NSOutlineViewDataSource { +extension ProjectNavigatorViewController: NSOutlineViewDataSource { func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int { if let item = item as? CEWorkspaceFile { return item.children?.count ?? 0 @@ -242,7 +243,7 @@ extension OutlineViewController: NSOutlineViewDataSource { // MARK: - NSOutlineViewDelegate -extension OutlineViewController: NSOutlineViewDelegate { +extension ProjectNavigatorViewController: NSOutlineViewDelegate { func outlineView( _ outlineView: NSOutlineView, shouldShowCellExpansionFor tableColumn: NSTableColumn?, @@ -260,7 +261,7 @@ extension OutlineViewController: NSOutlineViewDelegate { let frameRect = NSRect(x: 0, y: 0, width: tableColumn.width, height: rowHeight) - return OutlineTableViewCell(frame: frameRect, item: item as? CEWorkspaceFile, delegate: self) + return ProjectNavigatorTableViewCell(frame: frameRect, item: item as? CEWorkspaceFile, delegate: self) } func outlineViewSelectionDidChange(_ notification: Notification) { @@ -359,7 +360,7 @@ extension OutlineViewController: NSOutlineViewDelegate { // MARK: - NSMenuDelegate -extension OutlineViewController: NSMenuDelegate { +extension ProjectNavigatorViewController: NSMenuDelegate { /// Once a menu gets requested by a `right click` setup the menu /// @@ -367,7 +368,7 @@ extension OutlineViewController: NSMenuDelegate { /// - Parameter menu: The menu that got requested func menuNeedsUpdate(_ menu: NSMenu) { let row = outlineView.clickedRow - guard let menu = menu as? OutlineMenu else { return } + guard let menu = menu as? ProjectNavigatorMenu else { return } if row == -1 { menu.item = nil diff --git a/CodeEdit/Features/NavigatorSidebar/NavigatorSidebarToolbarBottom.swift b/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/ProjectNavigatorToolbarBottom.swift similarity index 52% rename from CodeEdit/Features/NavigatorSidebar/NavigatorSidebarToolbarBottom.swift rename to CodeEdit/Features/NavigatorSidebar/ProjectNavigator/ProjectNavigatorToolbarBottom.swift index 9a83d6bea1..0695768fd1 100644 --- a/CodeEdit/Features/NavigatorSidebar/NavigatorSidebarToolbarBottom.swift +++ b/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/ProjectNavigatorToolbarBottom.swift @@ -1,28 +1,52 @@ // -// SideBarToolbarBottom.swift +// ProjectNavigatorToolbarBottom.swift // CodeEdit // -// Created by Lukas Pistrol on 17.03.22. +// Created by TAY KAI QUAN on 23/7/22. // import SwiftUI -struct NavigatorSidebarToolbarBottom: View { +struct ProjectNavigatorToolbarBottom: View { @Environment(\.controlActiveState) private var activeState + @Environment(\.colorScheme) + private var colorScheme + @EnvironmentObject var workspace: WorkspaceDocument + @State + var filter: String = "" + var body: some View { - HStack(spacing: 10) { + HStack { addNewFileButton - Spacer() - sortButton + .frame(width: 20) + .padding(.leading, 10) + HStack { + sortButton + TextField("Filter", text: $filter) + .textFieldStyle(.plain) + .font(.system(size: 12)) + if !filter.isEmpty { + clearFilterButton + .padding(.trailing, 5) + } + } + .onChange(of: filter, perform: { + workspace.filter = $0 + }) + .padding(.vertical, 3) + .background(colorScheme == .dark ? Color(hex: "#FFFFFF").opacity(0.1) : Color(hex: "#808080").opacity(0.2)) + .clipShape(RoundedRectangle(cornerRadius: 6)) + .overlay(RoundedRectangle(cornerRadius: 6).stroke(Color.gray, lineWidth: 0.5).cornerRadius(6)) + .padding(.trailing, 5) + .padding(.leading, -8) } - .frame(height: 29) + .frame(height: 29, alignment: .center) .frame(maxWidth: .infinity) - .padding(.horizontal, 4) .overlay(alignment: .top) { Divider() } @@ -67,4 +91,18 @@ struct NavigatorSidebarToolbarBottom: View { .frame(maxWidth: 30) .opacity(activeState == .inactive ? 0.45 : 1) } + + /// We clear the text and remove the first responder which removes the cursor + /// when the user clears the filter. + private var clearFilterButton: some View { + Button { + filter = "" + NSApp.keyWindow?.makeFirstResponder(nil) + } label: { + Image(systemName: "xmark.circle.fill") + .symbolRenderingMode(.hierarchical) + } + .buttonStyle(.plain) + .opacity(activeState == .inactive ? 0.45 : 1) + } } diff --git a/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/ProjectNavigatorView.swift b/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/ProjectNavigatorView.swift index 9e8791a3de..2161d7cb5c 100644 --- a/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/ProjectNavigatorView.swift +++ b/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/ProjectNavigatorView.swift @@ -20,7 +20,7 @@ struct ProjectNavigatorView: View { var tabManager: TabManager var body: some View { - OutlineView(selection: $tabManager.activeTabGroup.selected) + ProjectNavigatorOutlineView(selection: $tabManager.activeTabGroup.selected) } } From 7b4c603dc82887491a599b3be908b0d766004213 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Mon, 1 May 2023 10:51:41 +0200 Subject: [PATCH 3/4] Swiftlint fixes and code clean up --- CodeEdit.xcodeproj/project.pbxproj | 4 + .../Models/CEWorkspaceFile+Recursion.swift | 134 +++++++++++++++ .../CEWorkspace/Models/CEWorkspaceFile.swift | 156 ++---------------- .../Models/CEWorkspaceFileManager.swift | 2 +- .../OutlineView/TextTableViewCell.swift | 8 +- .../OutlineView/ProjectNavigatorMenu.swift | 6 +- .../ProjectNavigatorViewController.swift | 15 +- .../ProjectNavigatorToolbarBottom.swift | 4 +- .../Features/Tabs/TabGroup/TabGroupData.swift | 2 +- 9 files changed, 176 insertions(+), 155 deletions(-) create mode 100644 CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile+Recursion.swift diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index e895522c50..f43f27776c 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -216,6 +216,7 @@ 58822534292C280D00E83CDE /* CursorLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5882251E292C280D00E83CDE /* CursorLocation.swift */; }; 588847632992A2A200996D95 /* CEWorkspaceFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 588847622992A2A200996D95 /* CEWorkspaceFile.swift */; }; 588847692992ABCA00996D95 /* Array+CEWorkspaceFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 588847682992ABCA00996D95 /* Array+CEWorkspaceFile.swift */; }; + 5894E59729FEF7740077E59C /* CEWorkspaceFile+Recursion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5894E59629FEF7740077E59C /* CEWorkspaceFile+Recursion.swift */; }; 58A2E40C29C3975D005CB615 /* CEWorkspaceFileIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A2E40629C3975D005CB615 /* CEWorkspaceFileIcon.swift */; }; 58A5DF7D2931787A00D1BD5D /* ShellClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A5DF7C2931787A00D1BD5D /* ShellClient.swift */; }; 58A5DF8029325B5A00D1BD5D /* GitClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A5DF7F29325B5A00D1BD5D /* GitClient.swift */; }; @@ -625,6 +626,7 @@ 5882251E292C280D00E83CDE /* CursorLocation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CursorLocation.swift; sourceTree = ""; }; 588847622992A2A200996D95 /* CEWorkspaceFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CEWorkspaceFile.swift; sourceTree = ""; }; 588847682992ABCA00996D95 /* Array+CEWorkspaceFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+CEWorkspaceFile.swift"; sourceTree = ""; }; + 5894E59629FEF7740077E59C /* CEWorkspaceFile+Recursion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CEWorkspaceFile+Recursion.swift"; sourceTree = ""; }; 589F3E342936185400E1A4DA /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Platforms/MacOSX.platform/Developer/Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; }; 58A2E40629C3975D005CB615 /* CEWorkspaceFileIcon.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CEWorkspaceFileIcon.swift; sourceTree = ""; }; 58A5DF7C2931787A00D1BD5D /* ShellClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShellClient.swift; sourceTree = ""; }; @@ -1778,6 +1780,7 @@ isa = PBXGroup; children = ( 588847622992A2A200996D95 /* CEWorkspaceFile.swift */, + 5894E59629FEF7740077E59C /* CEWorkspaceFile+Recursion.swift */, 58A2E40629C3975D005CB615 /* CEWorkspaceFileIcon.swift */, 58710158298EB80000951BA4 /* CEWorkspaceFileManager.swift */, ); @@ -2660,6 +2663,7 @@ B6C6A43029771F7100A3D28F /* TabBarItemBackground.swift in Sources */, B6F0517D29D9E4B100D72287 /* TerminalSettingsView.swift in Sources */, 587B9E8C29301D8F00AC7927 /* GitHubOpenness.swift in Sources */, + 5894E59729FEF7740077E59C /* CEWorkspaceFile+Recursion.swift in Sources */, 587B9E8229301D8F00AC7927 /* GitHubPreviewHeader.swift in Sources */, 58F2EB02292FB2B0004A9BDE /* Loopable.swift in Sources */, 28B8F884280FFE4600596236 /* NSTableView+Background.swift in Sources */, diff --git a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile+Recursion.swift b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile+Recursion.swift new file mode 100644 index 0000000000..1c427d6a52 --- /dev/null +++ b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile+Recursion.swift @@ -0,0 +1,134 @@ +// +// CEWorkspaceFile+Recursion.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom on 30/04/2023. +// + +import Foundation + +extension CEWorkspaceFile { + + func childrenDescription(tabCount: Int) -> String { + var myDetails = "\(String(repeating: "| ", count: max(tabCount - 1, 0)))\(tabCount != 0 ? "╰--" : "")" + myDetails += "\(url.path)" + if !self.isFolder { // if im a file, just return the url + return myDetails + } else { // if im a folder, return the url and its children's details + var childDetails = "\(myDetails)" + for child in children ?? [] { + childDetails += "\n\(child.childrenDescription(tabCount: tabCount + 1))" + } + return childDetails + } + } + + /// Flattens the children of ``self`` recursively with depth. + /// - Parameters: + /// - depth: An int that indicates the how deep the tree files need to be flattened + /// - ignoringFolders: A boolean on whether to ignore files that are Folders + /// - Returns: An array of flattened `CEWorkspaceFiles` + func flattenedChildren(withDepth depth: Int, ignoringFolders: Bool) -> [CEWorkspaceFile] { + guard depth > 0 else { return [] } + guard self.isFolder else { return [self] } + var childItems: [CEWorkspaceFile] = ignoringFolders ? [] : [self] + children?.forEach { child in + childItems.append(contentsOf: child.flattenedChildren( + withDepth: depth - 1, + ignoringFolders: ignoringFolders + )) + } + return childItems + } + + /// Returns a list of `CEWorkspaceFiles` that are sibilings of ``self``. + /// The `height` parameter lets the function navigate up the folder hierarchy to + /// select a starting point from which it should start flettening the items. + /// - Parameters: + /// - height: `Int` that tells where to start in the hierarchy + /// - ignoringFolders: Wether the sibling folders should be flattened + /// - Returns: A list of `FileSystemItems` + func flattenedSiblings(withHeight height: Int, ignoringFolders: Bool) -> [CEWorkspaceFile] { + let topMostParent = self.getParent(withHeight: height) + return topMostParent.flattenedChildren(withDepth: height, ignoringFolders: ignoringFolders) + } + + /// Recursive function that returns the number of children + /// that contain the `searchString` in their path or their subitems' paths. + /// Returns `0` if the item is not a folder. + /// - Parameters: + /// - searchString: The string + /// - ignoredStrings: The prefixes to ignore if they prefix file names + /// - Returns: The number of children that match the conditiions + func appearanceWithinChildrenOf(searchString: String, ignoredStrings: [String] = [".", "~"]) -> Int { + var count = 0 + guard self.isFolder else { return 0 } + for child in self.children ?? [] { + var isIgnored: Bool = false + for ignoredString in ignoredStrings where child.name.hasPrefix(ignoredString) { + isIgnored = true // can use regex later + } + + if isIgnored { + continue + } + + guard !searchString.isEmpty else { count += 1; continue } + if child.isFolder { + count += child.appearanceWithinChildrenOf(searchString: searchString) > 0 ? 1 : 0 + } else { + count += child.name.lowercased().contains(searchString.lowercased()) ? 1 : 0 + } + } + return count + } + + /// Function that returns an array of the children + /// that contain the `searchString` in their path or their subitems' paths. + /// Similar to `appearanceWithinChildrenOf(searchString: String)` + /// Returns `[]` if the item is not a folder. + /// - Parameter searchString: The string + /// - Parameter ignoredStrings: The prefixes to ignore if they prefix file names + /// - Returns: The children that match the conditiions + func childrenSatisfying(searchString: String, ignoredStrings: [String] = [".", "~"]) -> [CEWorkspaceFile] { + var satisfyingChildren: [CEWorkspaceFile] = [] + guard self.isFolder else { return [] } + for child in self.children ?? [] { + var isIgnored: Bool = false + for ignoredString in ignoredStrings where child.name.hasPrefix(ignoredString) { + isIgnored = true // can use regex later + } + + if isIgnored { + continue + } + + guard !searchString.isEmpty else { satisfyingChildren.append(child); continue } + if child.isFolder { + if child.appearanceWithinChildrenOf(searchString: searchString) > 0 { + satisfyingChildren.append(child) + } + } else { + if child.name.lowercased().contains(searchString.lowercased()) { + satisfyingChildren.append(child) + } + } + } + return satisfyingChildren + } + + /// Using the current instance of `FileSystemItem` it will walk back up the Workspace file hiarchy + /// the amount of times specified with the `withHeight` parameter. + /// - Parameter height: The amount of times you want to up a folder. + /// - Returns: The found `FileSystemItem` object, This should always be a folder. + private func getParent(withHeight height: Int) -> CEWorkspaceFile { + var topmostParent = self + for _ in 0.. String { - var myDetails = "\(String(repeating: "| ", count: max(tabCount - 1, 0)))\(tabCount != 0 ? "╰--" : "")" - myDetails += "\(url.path)" - if !self.isFolder { // if im a file, just return the url - return myDetails - } else { // if im a folder, return the url and its children's details - var childDetails = "\(myDetails)" - for child in children ?? [] { - childDetails += "\n\(child.childrenDescription(tabCount: tabCount + 1))" - } - return childDetails - } - } - /// Returns a string describing a SFSymbol for folders /// /// If it is the top-level folder this will return `"square.dashed.inset.filled"`. @@ -173,18 +171,6 @@ final class CEWorkspaceFile: Codable, Comparable, Hashable, Identifiable, TabBar typeHidden ? url.deletingPathExtension().lastPathComponent : name } - /// Return the file's UTType - var contentType: UTType? { - try? url.resourceValues(forKeys: [.contentTypeKey]).contentType - } - - /// Returns a `Color` for a specific `fileType` - /// - /// If not specified otherwise this will return `Color.accentColor` - var iconColor: Color { - FileIcon.iconColor(fileType: type) - } - // MARK: Statics /// The default `FileManager` instance static let fileManger = FileManager.default @@ -200,114 +186,6 @@ final class CEWorkspaceFile: Codable, Comparable, Hashable, Identifiable, TabBar NSWorkspace.shared.open(url) } - /// Flattens the children of ``self`` recursively with depth. - /// - Parameters: - /// - depth: An int that indicates the how deep the tree files need to be flattened - /// - ignoringFolders: A boolean on whether to ignore files that are Folders - /// - Returns: An array of flattened `CEWorkspaceFiles` - func flattenedChildren(withDepth depth: Int, ignoringFolders: Bool) -> [CEWorkspaceFile] { - guard depth > 0 else { return [] } - guard self.isFolder else { return [self] } - var childItems: [CEWorkspaceFile] = ignoringFolders ? [] : [self] - children?.forEach { child in - childItems.append(contentsOf: child.flattenedChildren( - withDepth: depth - 1, - ignoringFolders: ignoringFolders - )) - } - return childItems - } - - /// Returns a list of `CEWorkspaceFiles` that are sibilings of ``self``. - /// The `height` parameter lets the function navigate up the folder hierarchy to - /// select a starting point from which it should start flettening the items. - /// - Parameters: - /// - height: `Int` that tells where to start in the hierarchy - /// - ignoringFolders: Wether the sibling folders should be flattened - /// - Returns: A list of `FileSystemItems` - func flattenedSiblings(withHeight height: Int, ignoringFolders: Bool) -> [CEWorkspaceFile] { - let topMostParent = self.getParent(withHeight: height) - return topMostParent.flattenedChildren(withDepth: height, ignoringFolders: ignoringFolders) - } - - /// Using the current instance of `FileSystemItem` it will walk back up the Workspace file hiarchy - /// the amount of times specified with the `withHeight` parameter. - /// - Parameter height: The amount of times you want to up a folder. - /// - Returns: The found `FileSystemItem` object, This should always be a folder. - private func getParent(withHeight height: Int) -> CEWorkspaceFile { - var topmostParent = self - for _ in 0.. Int { - var count = 0 - guard self.isFolder else { return 0 } - for child in self.children ?? [] { - var isIgnored: Bool = false - for ignoredString in ignoredStrings where child.name.hasPrefix(ignoredString) { - isIgnored = true // can use regex later - } - - if isIgnored { - continue - } - - guard !searchString.isEmpty else { count += 1; continue } - if child.isFolder { - count += child.appearanceWithinChildrenOf(searchString: searchString) > 0 ? 1 : 0 - } else { - count += child.name.lowercased().contains(searchString.lowercased()) ? 1 : 0 - } - } - return count - } - - /// Function that returns an array of the children - /// that contain the `searchString` in their path or their subitems' paths. - /// Similar to `appearanceWithinChildrenOf(searchString: String)` - /// Returns `[]` if the item is not a folder. - /// - Parameter searchString: The string - /// - Parameter ignoredStrings: The prefixes to ignore if they prefix file names - /// - Returns: The children that match the conditiions - func childrenSatisfying(searchString: String, ignoredStrings: [String] = [".", "~"]) -> [CEWorkspaceFile] { - var satisfyingChildren: [CEWorkspaceFile] = [] - guard self.isFolder else { return [] } - for child in self.children ?? [] { - var isIgnored: Bool = false - for ignoredString in ignoredStrings where child.name.hasPrefix(ignoredString) { - isIgnored = true // can use regex later - } - - if isIgnored { - continue - } - - guard !searchString.isEmpty else { satisfyingChildren.append(child); continue } - if child.isFolder { - if child.appearanceWithinChildrenOf(searchString: searchString) > 0 { - satisfyingChildren.append(child) - } - } else { - if child.name.lowercased().contains(searchString.lowercased()) { - satisfyingChildren.append(child) - } - } - } - return satisfyingChildren - } - /// This function allows creation of folders in the main directory or sub-folders /// - Parameter folderName: The name of the new folder func addFolder(folderName: String) { @@ -424,13 +302,11 @@ final class CEWorkspaceFile: Codable, Comparable, Hashable, Identifiable, TabBar previousName.replacingOccurrences(of: ".\(fileExtension)", with: "") fileUrl = fileUrl.deletingLastPathComponent().appendingPathComponent("\(fileName) copy\(fileExtension)") } -// Log.info("Duplicating file to \(fileUrl)") if CEWorkspaceFile.fileManger.fileExists(atPath: self.url.path) { do { try CEWorkspaceFile.fileManger.copyItem(at: self.url, to: fileUrl) } catch { -// Log.error("Error at \(self.url.path) to \(fileUrl.path)") fatalError(error.localizedDescription) } } @@ -442,7 +318,6 @@ final class CEWorkspaceFile: Codable, Comparable, Hashable, Identifiable, TabBar createMissingParentDirectory(for: newLocation.deletingLastPathComponent()) do { -// Log.info("Moving file \(self.url.debugDescription) to \(newLocation.debugDescription)") try CEWorkspaceFile.fileManger.moveItem(at: self.url, to: newLocation) } catch { fatalError(error.localizedDescription) } @@ -454,7 +329,6 @@ final class CEWorkspaceFile: Codable, Comparable, Hashable, Identifiable, TabBar } // if the folder doesn't exist and the function was ordered to create it, create it. if createSelf && !CEWorkspaceFile.fileManger.fileExists(atPath: url.path) { -// Log.info("Creating folder \(url.debugDescription)") // Create the folder do { try CEWorkspaceFile.fileManger.createDirectory( diff --git a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager.swift b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager.swift index cdbadd95c5..1a76719448 100644 --- a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager.swift +++ b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager.swift @@ -122,7 +122,7 @@ final class CEWorkspaceFileManager { /// within the scope of the `FileSystemClient`. /// - Parameter id: The file's full path /// - Returns: The file item corresponding to the file - func getFileItem(_ id: String) throws -> CEWorkspaceFile { + func getFile(_ id: String) throws -> CEWorkspaceFile { guard let item = flattenedFileItems[id] else { throw FileSystemClientError.fileNotExist } diff --git a/CodeEdit/Features/NavigatorSidebar/OutlineView/TextTableViewCell.swift b/CodeEdit/Features/NavigatorSidebar/OutlineView/TextTableViewCell.swift index d72deef968..d18e1cb1b2 100644 --- a/CodeEdit/Features/NavigatorSidebar/OutlineView/TextTableViewCell.swift +++ b/CodeEdit/Features/NavigatorSidebar/OutlineView/TextTableViewCell.swift @@ -57,8 +57,12 @@ class TextTableViewCell: NSTableCellView { override func resizeSubviews(withOldSize oldSize: NSSize) { super.resizeSubviews(withOldSize: oldSize) - label.frame = NSRect(x: 2, y: 2.5, - width: frame.width - 4, height: 25) + label.frame = NSRect( + x: 2, + y: 2.5, + width: frame.width - 4, + height: 25 + ) } /// Returns the font size for the current row height. Defaults to `13.0` diff --git a/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/ProjectNavigatorMenu.swift b/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/ProjectNavigatorMenu.swift index f1fc6dd203..a0bbf44f9a 100644 --- a/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/ProjectNavigatorMenu.swift +++ b/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/ProjectNavigatorMenu.swift @@ -213,7 +213,11 @@ final class ProjectNavigatorMenu: NSMenu { private func renameFile() { let row = outlineView.row(forItem: item) guard row > 0, - let cell = outlineView.view(atColumn: 0, row: row, makeIfNecessary: false) as? ProjectNavigatorTableViewCell else { + let cell = outlineView.view( + atColumn: 0, + row: row, + makeIfNecessary: false + ) as? ProjectNavigatorTableViewCell else { return } outlineView.window?.makeFirstResponder(cell.textField) diff --git a/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift b/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift index cc3fa2b4d0..f717430367 100644 --- a/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift +++ b/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift @@ -21,7 +21,7 @@ final class ProjectNavigatorViewController: NSViewController { /// Also creates a top level item "root" which represents the projects root directory and automatically expands it. private var content: [CEWorkspaceFile] { guard let folderURL = workspace?.workspaceFileManager?.folderUrl else { return [] } - guard let root = try? workspace?.workspaceFileManager?.getFileItem(folderURL.path) else { return [] } + guard let root = try? workspace?.workspaceFileManager?.getFile(folderURL.path) else { return [] } return [root] } @@ -198,7 +198,7 @@ extension ProjectNavigatorViewController: NSOutlineViewDataSource { } // Needs to come before call to .removeItem or else race condition occurs - var srcFileItem: CEWorkspaceFile? = try? workspace?.workspaceFileManager?.getFileItem(fileItemURL.path) + var srcFileItem: CEWorkspaceFile? = try? 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)) @@ -288,7 +288,7 @@ extension ProjectNavigatorViewController: NSOutlineViewDelegate { func outlineView(_ outlineView: NSOutlineView, itemForPersistentObject object: Any) -> Any? { guard let id = object as? CEWorkspaceFile.ID, - let item = try? workspace?.workspaceFileManager?.getFileItem(id) else { return nil } + let item = try? workspace?.workspaceFileManager?.getFile(id) else { return nil } return item } @@ -304,10 +304,11 @@ extension ProjectNavigatorViewController: NSOutlineViewDelegate { private func select(by id: TabBarItemID, from collection: [CEWorkspaceFile]) { // If the user has set "Reveal file on selection change" to on, we need to reveal the item before // selecting the row. - if Settings.shared.preferences.general.revealFileOnFocusChange, - case let .codeEditor(id) = id, - let fileItem = try? workspace?.workspaceFileManager?.getFileItem(id as CEWorkspaceFile.ID) as? CEWorkspaceFile { - reveal(fileItem) + if Settings.shared.preferences.general.revealFileOnFocusChange { + if case let .codeEditor(id) = id, + let fileItem = try? workspace?.workspaceFileManager?.getFile(id as CEWorkspaceFile.ID) { + reveal(fileItem) + } } guard let item = collection.first(where: { $0.tabID == id }) else { diff --git a/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/ProjectNavigatorToolbarBottom.swift b/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/ProjectNavigatorToolbarBottom.swift index 0695768fd1..2a1bf81ede 100644 --- a/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/ProjectNavigatorToolbarBottom.swift +++ b/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/ProjectNavigatorToolbarBottom.swift @@ -56,14 +56,14 @@ struct ProjectNavigatorToolbarBottom: View { Menu { Button("Add File") { guard let folderURL = workspace.workspaceFileManager?.folderUrl, - let root = try? workspace.workspaceFileManager?.getFileItem(folderURL.path) else { return } + let root = try? workspace.workspaceFileManager?.getFile(folderURL.path) else { return } // TODO: use currently selected file instead of root root.addFile(fileName: "untitled") } Button("Add Folder") { guard let folderURL = workspace.workspaceFileManager?.folderUrl, - let root = try? workspace.workspaceFileManager?.getFileItem(folderURL.path) else { return } + let root = try? workspace.workspaceFileManager?.getFile(folderURL.path) else { return } // TODO: use currently selected file instead of root root.addFolder(folderName: "untitled") diff --git a/CodeEdit/Features/Tabs/TabGroup/TabGroupData.swift b/CodeEdit/Features/Tabs/TabGroup/TabGroupData.swift index 7ffaea9649..570158065c 100644 --- a/CodeEdit/Features/Tabs/TabGroup/TabGroupData.swift +++ b/CodeEdit/Features/Tabs/TabGroup/TabGroupData.swift @@ -33,7 +33,7 @@ final class TabGroupData: ObservableObject, Identifiable { } } } - + /// The current offset in the history list. @Published var historyOffset: Int = 0 { From b8943cdfec205e64086ffd63bd8fea55f902355e Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Mon, 1 May 2023 11:02:14 +0200 Subject: [PATCH 4/4] Test fixes --- .../Features/CodeEditUI/CodeEditUITests.swift | 4 ++-- .../WorkspaceClient/WorkspaceClientTests.swift | 14 ++++++-------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/CodeEditTests/Features/CodeEditUI/CodeEditUITests.swift b/CodeEditTests/Features/CodeEditUI/CodeEditUITests.swift index d72e55795a..73fccef970 100644 --- a/CodeEditTests/Features/CodeEditUI/CodeEditUITests.swift +++ b/CodeEditTests/Features/CodeEditUI/CodeEditUITests.swift @@ -88,7 +88,7 @@ final class CodeEditUIUnitTests: XCTestCase { func testBranchPickerLight() throws { let view = ToolbarBranchPicker( shellClient: ShellClient(), - workspace: nil + workspaceFileManager: nil ) let hosting = NSHostingView(rootView: view) hosting.appearance = .init(named: .aqua) @@ -99,7 +99,7 @@ final class CodeEditUIUnitTests: XCTestCase { func testBranchPickerDark() throws { let view = ToolbarBranchPicker( shellClient: ShellClient(), - workspace: nil + workspaceFileManager: nil ) let hosting = NSHostingView(rootView: view) hosting.appearance = .init(named: .darkAqua) diff --git a/CodeEditTests/Utils/WorkspaceClient/WorkspaceClientTests.swift b/CodeEditTests/Utils/WorkspaceClient/WorkspaceClientTests.swift index 6d0fa87a38..0cd27fa6f2 100644 --- a/CodeEditTests/Utils/WorkspaceClient/WorkspaceClientTests.swift +++ b/CodeEditTests/Utils/WorkspaceClient/WorkspaceClientTests.swift @@ -34,13 +34,12 @@ final class WorkspaceClientUnitTests: XCTestCase { .appendingPathComponent($0) try fakeData!.write(to: fileUrl) } - let client: WorkspaceClient = try .default( - fileManager: .default, - folderURL: directory, + let client = CEWorkspaceFileManager( + folderUrl: directory, ignoredFilesAndFolders: [] ) - var newFiles: [WorkspaceClient.FileItem] = [] + var newFiles: [CEWorkspaceFile] = [] cancellable = client .getFiles @@ -80,13 +79,12 @@ final class WorkspaceClientUnitTests: XCTestCase { try fakeData!.write(to: fileUrl) } - let client: WorkspaceClient = try .default( - fileManager: .default, - folderURL: directory, + let client = CEWorkspaceFileManager( + folderUrl: directory, ignoredFilesAndFolders: [] ) - var newFiles: [WorkspaceClient.FileItem] = [] + var newFiles: [CEWorkspaceFile] = [] cancellable = client .getFiles