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) {