diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index ab9dee36ff..4ea22a121e 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -8,7 +8,6 @@ /* Begin PBXBuildFile section */ 043BCF03281DA18A000AC47C /* WorkspaceDocument+Search.swift in Sources */ = {isa = PBXBuildFile; fileRef = 043BCF02281DA18A000AC47C /* WorkspaceDocument+Search.swift */; }; - 043BCF05281DA19A000AC47C /* WorkspaceDocument+Selection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 043BCF04281DA19A000AC47C /* WorkspaceDocument+Selection.swift */; }; 043C321427E31FF6006AE443 /* CodeEditDocumentController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 043C321327E31FF6006AE443 /* CodeEditDocumentController.swift */; }; 043C321627E3201F006AE443 /* WorkspaceDocument.swift in Sources */ = {isa = PBXBuildFile; fileRef = 043C321527E3201F006AE443 /* WorkspaceDocument.swift */; }; 043C321A27E32295006AE443 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 043C321927E32295006AE443 /* MainMenu.xib */; }; @@ -19,7 +18,6 @@ 0483E35027FDB17700354AC0 /* ExtensionNavigatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0483E34F27FDB17700354AC0 /* ExtensionNavigatorView.swift */; }; 0485EB1F27E7458B00138301 /* WorkspaceCodeFileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0485EB1E27E7458B00138301 /* WorkspaceCodeFileView.swift */; }; 04C3254B27FF23B000C8DA2D /* ExtensionNavigatorData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04C3254A27FF23B000C8DA2D /* ExtensionNavigatorData.swift */; }; - 04C3254D27FF331B00C8DA2D /* ExtensionNavigatorItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04C3254C27FF331B00C8DA2D /* ExtensionNavigatorItemView.swift */; }; 04C3254F2800AA4700C8DA2D /* ExtensionInstallationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04C3254E2800AA4700C8DA2D /* ExtensionInstallationView.swift */; }; 04C325512800AC7400C8DA2D /* ExtensionInstallationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04C325502800AC7400C8DA2D /* ExtensionInstallationViewModel.swift */; }; 04C3255B2801F86400C8DA2D /* OutlineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 285FEC6D27FE4B4A00E57D53 /* OutlineViewController.swift */; }; @@ -305,18 +303,36 @@ 58FD7609291EA1CB0051D6E4 /* CommandPaletteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FD7607291EA1CB0051D6E4 /* CommandPaletteView.swift */; }; 5C4BB1E128212B1E00A92FB2 /* World.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4BB1E028212B1E00A92FB2 /* World.swift */; }; 6C05A8AF284D0CA3007F4EAA /* WorkspaceDocument+Listeners.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C05A8AE284D0CA3007F4EAA /* WorkspaceDocument+Listeners.swift */; }; + 6C147C4029A328BC0089B630 /* SplitViewData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C147C3F29A328560089B630 /* SplitViewData.swift */; }; + 6C147C4129A328BF0089B630 /* TabGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C147C3E29A3281D0089B630 /* TabGroup.swift */; }; + 6C147C4229A328C10089B630 /* TabGroupData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C147C3D29A3281D0089B630 /* TabGroupData.swift */; }; + 6C147C4529A329350089B630 /* OrderedCollections in Frameworks */ = {isa = PBXBuildFile; productRef = 6C147C4429A329350089B630 /* OrderedCollections */; }; + 6C147C4929A32A080089B630 /* EditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C147C4829A32A080089B630 /* EditorView.swift */; }; + 6C147C4B29A32A7B0089B630 /* Environment+SplitEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C147C4A29A32A7B0089B630 /* Environment+SplitEditor.swift */; }; + 6C147C4D29A32AA30089B630 /* WorkspaceTabGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C147C4C29A32AA30089B630 /* WorkspaceTabGroupView.swift */; }; 6C14CEB028777D3C001468FE /* FindNavigatorListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C14CEAF28777D3C001468FE /* FindNavigatorListViewController.swift */; }; 6C14CEB32877A68F001468FE /* FindNavigatorMatchListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C14CEB22877A68F001468FE /* FindNavigatorMatchListCell.swift */; }; 6C18620A298BF5A800C663EA /* RecentProjectsListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C186209298BF5A800C663EA /* RecentProjectsListView.swift */; }; + 6C2C155829B4F49100EA60A5 /* SplitViewItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C2C155729B4F49100EA60A5 /* SplitViewItem.swift */; }; + 6C2C155A29B4F4CC00EA60A5 /* Variadic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C2C155929B4F4CC00EA60A5 /* Variadic.swift */; }; + 6C2C155D29B4F4E500EA60A5 /* SplitViewReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C2C155C29B4F4E500EA60A5 /* SplitViewReader.swift */; }; + 6C2C156129B4F52F00EA60A5 /* SplitViewModifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C2C156029B4F52F00EA60A5 /* SplitViewModifiers.swift */; }; 6C4104E3297C87A000F472BA /* BlurButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C4104E2297C87A000F472BA /* BlurButtonStyle.swift */; }; 6C4104E6297C884F00F472BA /* AboutDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C4104E5297C884F00F472BA /* AboutDetailView.swift */; }; 6C4104E9297C970F00F472BA /* AboutDefaultView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C4104E8297C970F00F472BA /* AboutDefaultView.swift */; }; 6C48D8F22972DAFC00D6D205 /* Env+IsFullscreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C48D8F12972DAFC00D6D205 /* Env+IsFullscreen.swift */; }; 6C48D8F42972DB1A00D6D205 /* Env+Window.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C48D8F32972DB1A00D6D205 /* Env+Window.swift */; }; 6C48D8F72972E5F300D6D205 /* WindowObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C48D8F62972E5F300D6D205 /* WindowObserver.swift */; }; + 6C5228B529A868BD00AC48F6 /* Environment+ContentInsets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C5228B429A868BD00AC48F6 /* Environment+ContentInsets.swift */; }; + 6C53AAD829A6C4FD00EE9ED6 /* SplitView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C53AAD729A6C4FD00EE9ED6 /* SplitView.swift */; }; + 6C7256D729A3D7D000C2D3E0 /* SplitViewControllerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C7256D629A3D7D000C2D3E0 /* SplitViewControllerView.swift */; }; + 6C81916729B3E80700B75C92 /* ModifierKeysObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C81916629B3E80700B75C92 /* ModifierKeysObserver.swift */; }; + 6C81916B29B41DD300B75C92 /* DequeModule in Frameworks */ = {isa = PBXBuildFile; productRef = 6C81916A29B41DD300B75C92 /* DequeModule */; }; + 6C91D57229B176FF0059A90D /* TabManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C91D57129B176FF0059A90D /* TabManager.swift */; }; 6C97EBCC2978760400302F95 /* AcknowledgementsWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C97EBCB2978760400302F95 /* AcknowledgementsWindowController.swift */; }; 6C97EBCF297876E500302F95 /* AboutWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C97EBCE297876E500302F95 /* AboutWindowController.swift */; }; 6CBD1BC62978DE53006639D5 /* Font+Caption3.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CBD1BC52978DE53006639D5 /* Font+Caption3.swift */; }; + 6CC9E4B229B5669900C97388 /* Environment+ActiveTabGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CC9E4B129B5669900C97388 /* Environment+ActiveTabGroup.swift */; }; 6CDA84AD284C1BA000C1CC3A /* TabBarContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CDA84AC284C1BA000C1CC3A /* TabBarContextMenu.swift */; }; B60BE8BD297A167600841125 /* AcknowledgementRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B60BE8BC297A167600841125 /* AcknowledgementRowView.swift */; }; B62617282964924E00E866AB /* CodeEditKit in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 2801BB89290D5A8E00EBF552 /* CodeEditKit */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; @@ -393,7 +409,6 @@ /* Begin PBXFileReference section */ 043BCF02281DA18A000AC47C /* WorkspaceDocument+Search.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WorkspaceDocument+Search.swift"; sourceTree = ""; }; - 043BCF04281DA19A000AC47C /* WorkspaceDocument+Selection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WorkspaceDocument+Selection.swift"; sourceTree = ""; }; 043C321327E31FF6006AE443 /* CodeEditDocumentController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodeEditDocumentController.swift; sourceTree = ""; }; 043C321527E3201F006AE443 /* WorkspaceDocument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceDocument.swift; sourceTree = ""; }; 043C321927E32295006AE443 /* MainMenu.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = MainMenu.xib; sourceTree = ""; }; @@ -405,7 +420,6 @@ 0483E34F27FDB17700354AC0 /* ExtensionNavigatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionNavigatorView.swift; sourceTree = ""; }; 0485EB1E27E7458B00138301 /* WorkspaceCodeFileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceCodeFileView.swift; sourceTree = ""; }; 04C3254A27FF23B000C8DA2D /* ExtensionNavigatorData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionNavigatorData.swift; sourceTree = ""; }; - 04C3254C27FF331B00C8DA2D /* ExtensionNavigatorItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionNavigatorItemView.swift; sourceTree = ""; }; 04C3254E2800AA4700C8DA2D /* ExtensionInstallationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionInstallationView.swift; sourceTree = ""; }; 04C325502800AC7400C8DA2D /* ExtensionInstallationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionInstallationViewModel.swift; sourceTree = ""; }; 200412EE280F3EAC00BCAF5C /* HistoryInspectorNoHistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryInspectorNoHistoryView.swift; sourceTree = ""; }; @@ -688,18 +702,34 @@ 58FD7607291EA1CB0051D6E4 /* CommandPaletteView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CommandPaletteView.swift; sourceTree = ""; }; 5C4BB1E028212B1E00A92FB2 /* World.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = World.swift; sourceTree = ""; }; 6C05A8AE284D0CA3007F4EAA /* WorkspaceDocument+Listeners.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WorkspaceDocument+Listeners.swift"; sourceTree = ""; }; + 6C147C3D29A3281D0089B630 /* TabGroupData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabGroupData.swift; sourceTree = ""; }; + 6C147C3E29A3281D0089B630 /* TabGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabGroup.swift; sourceTree = ""; }; + 6C147C3F29A328560089B630 /* SplitViewData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitViewData.swift; sourceTree = ""; }; + 6C147C4829A32A080089B630 /* EditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditorView.swift; sourceTree = ""; }; + 6C147C4A29A32A7B0089B630 /* Environment+SplitEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Environment+SplitEditor.swift"; sourceTree = ""; }; + 6C147C4C29A32AA30089B630 /* WorkspaceTabGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceTabGroupView.swift; sourceTree = ""; }; 6C14CEAF28777D3C001468FE /* FindNavigatorListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FindNavigatorListViewController.swift; sourceTree = ""; }; 6C14CEB22877A68F001468FE /* FindNavigatorMatchListCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FindNavigatorMatchListCell.swift; sourceTree = ""; }; 6C186209298BF5A800C663EA /* RecentProjectsListView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecentProjectsListView.swift; sourceTree = ""; }; + 6C2C155729B4F49100EA60A5 /* SplitViewItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitViewItem.swift; sourceTree = ""; }; + 6C2C155929B4F4CC00EA60A5 /* Variadic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Variadic.swift; sourceTree = ""; }; + 6C2C155C29B4F4E500EA60A5 /* SplitViewReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitViewReader.swift; sourceTree = ""; }; + 6C2C156029B4F52F00EA60A5 /* SplitViewModifiers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitViewModifiers.swift; sourceTree = ""; }; 6C4104E2297C87A000F472BA /* BlurButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurButtonStyle.swift; sourceTree = ""; }; 6C4104E5297C884F00F472BA /* AboutDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutDetailView.swift; sourceTree = ""; }; 6C4104E8297C970F00F472BA /* AboutDefaultView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutDefaultView.swift; sourceTree = ""; }; 6C48D8F12972DAFC00D6D205 /* Env+IsFullscreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Env+IsFullscreen.swift"; sourceTree = ""; }; 6C48D8F32972DB1A00D6D205 /* Env+Window.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Env+Window.swift"; sourceTree = ""; }; 6C48D8F62972E5F300D6D205 /* WindowObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowObserver.swift; sourceTree = ""; }; + 6C5228B429A868BD00AC48F6 /* Environment+ContentInsets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Environment+ContentInsets.swift"; sourceTree = ""; }; + 6C53AAD729A6C4FD00EE9ED6 /* SplitView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitView.swift; sourceTree = ""; }; + 6C7256D629A3D7D000C2D3E0 /* SplitViewControllerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitViewControllerView.swift; sourceTree = ""; }; + 6C81916629B3E80700B75C92 /* ModifierKeysObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModifierKeysObserver.swift; sourceTree = ""; }; + 6C91D57129B176FF0059A90D /* TabManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabManager.swift; sourceTree = ""; }; 6C97EBCB2978760400302F95 /* AcknowledgementsWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AcknowledgementsWindowController.swift; sourceTree = ""; }; 6C97EBCE297876E500302F95 /* AboutWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutWindowController.swift; sourceTree = ""; }; 6CBD1BC52978DE53006639D5 /* Font+Caption3.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Font+Caption3.swift"; sourceTree = ""; }; + 6CC9E4B129B5669900C97388 /* Environment+ActiveTabGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Environment+ActiveTabGroup.swift"; sourceTree = ""; }; 6CDA84AC284C1BA000C1CC3A /* TabBarContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarContextMenu.swift; sourceTree = ""; }; B60BE8BC297A167600841125 /* AcknowledgementRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AcknowledgementRowView.swift; sourceTree = ""; }; B658FB2C27DA9E0F00EA4DBD /* CodeEdit.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CodeEdit.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -747,9 +777,11 @@ 58F2EB17292FB74D004A9BDE /* CodeEditTextView in Frameworks */, 5879826F292EC9870085B254 /* GRDB in Frameworks */, 58F2EB1E292FB954004A9BDE /* Sparkle in Frameworks */, + 6C147C4529A329350089B630 /* OrderedCollections in Frameworks */, 5879826B292EC7B00085B254 /* Light-Swift-Untar in Frameworks */, 5879828A292ED15F0085B254 /* SwiftTerm in Frameworks */, 2816F594280CF50500DD548B /* CodeEditSymbols in Frameworks */, + 6C81916B29B41DD300B75C92 /* DequeModule in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -778,7 +810,6 @@ 5831E3CF2933F4E000D5A6D2 /* Views */, 043C321527E3201F006AE443 /* WorkspaceDocument.swift */, 043BCF02281DA18A000AC47C /* WorkspaceDocument+Search.swift */, - 043BCF04281DA19A000AC47C /* WorkspaceDocument+Selection.swift */, 6C05A8AE284D0CA3007F4EAA /* WorkspaceDocument+Listeners.swift */, ); path = Documents; @@ -789,7 +820,6 @@ children = ( 0483E34F27FDB17700354AC0 /* ExtensionNavigatorView.swift */, 04C3254A27FF23B000C8DA2D /* ExtensionNavigatorData.swift */, - 04C3254C27FF331B00C8DA2D /* ExtensionNavigatorItemView.swift */, 04C3254E2800AA4700C8DA2D /* ExtensionInstallationView.swift */, 04C325502800AC7400C8DA2D /* ExtensionInstallationViewModel.swift */, ); @@ -974,13 +1004,14 @@ path = NavigatorSidebar; sourceTree = ""; }; - 287776EB27E350BA00D46668 /* TabBar */ = { + 287776EB27E350BA00D46668 /* Tabs */ = { isa = PBXGroup; children = ( + 6C147C3C29A328020089B630 /* TabGroup */, 58AFAA272933C65C00482B53 /* Models */, 58AFAA262933C65000482B53 /* Views */, ); - path = TabBar; + path = Tabs; sourceTree = ""; }; 2BE487ED28245162003F3F64 /* OpenWithCodeEdit */ = { @@ -1073,8 +1104,9 @@ 287776EA27E350A100D46668 /* NavigatorSidebar */, 5878DAA0291AE76700DD95A3 /* QuickOpen */, 58798210292D92370085B254 /* Search */, + 6C147C4729A329E50089B630 /* SplitView */, 588224FF292C280D00E83CDE /* StatusBar */, - 287776EB27E350BA00D46668 /* TabBar */, + 287776EB27E350BA00D46668 /* Tabs */, 5879827E292ED0FB0085B254 /* TerminalEmulator */, 581BFB4B2926431000D251EC /* Welcome */, ); @@ -1797,6 +1829,7 @@ isa = PBXGroup; children = ( 58A5DF9F29339F6400D1BD5D /* CommandManager.swift */, + 6C81916629B3E80700B75C92 /* ModifierKeysObserver.swift */, 58A5DFA129339F6400D1BD5D /* default_keybindings.json */, 58A5DF9E29339F6400D1BD5D /* KeybindingManager.swift */, ); @@ -1823,6 +1856,7 @@ 58AFAA272933C65C00482B53 /* Models */ = { isa = PBXGroup; children = ( + 6C91D57129B176FF0059A90D /* TabManager.swift */, 58AFAA2D2933C69E00482B53 /* TabBarItemID.swift */, 58AFAA2C2933C69E00482B53 /* TabBarItemRepresentable.swift */, ); @@ -2159,6 +2193,34 @@ name = Frameworks; sourceTree = ""; }; + 6C147C3C29A328020089B630 /* TabGroup */ = { + isa = PBXGroup; + children = ( + 6C147C4C29A32AA30089B630 /* WorkspaceTabGroupView.swift */, + 6CC9E4B129B5669900C97388 /* Environment+ActiveTabGroup.swift */, + 6C147C3E29A3281D0089B630 /* TabGroup.swift */, + 6C147C3D29A3281D0089B630 /* TabGroupData.swift */, + ); + path = TabGroup; + sourceTree = ""; + }; + 6C147C4729A329E50089B630 /* SplitView */ = { + isa = PBXGroup; + children = ( + 6C147C4A29A32A7B0089B630 /* Environment+SplitEditor.swift */, + 6C5228B429A868BD00AC48F6 /* Environment+ContentInsets.swift */, + 6C147C4829A32A080089B630 /* EditorView.swift */, + 6C53AAD729A6C4FD00EE9ED6 /* SplitView.swift */, + 6C2C156029B4F52F00EA60A5 /* SplitViewModifiers.swift */, + 6C2C155C29B4F4E500EA60A5 /* SplitViewReader.swift */, + 6C2C155929B4F4CC00EA60A5 /* Variadic.swift */, + 6C7256D629A3D7D000C2D3E0 /* SplitViewControllerView.swift */, + 6C2C155729B4F49100EA60A5 /* SplitViewItem.swift */, + 6C147C3F29A328560089B630 /* SplitViewData.swift */, + ); + path = SplitView; + sourceTree = ""; + }; 6C14CEB12877A5BE001468FE /* FindNavigatorResultList */ = { isa = PBXGroup; children = ( @@ -2323,6 +2385,8 @@ 58F2EB16292FB74D004A9BDE /* CodeEditTextView */, 58F2EB19292FB91C004A9BDE /* Preferences */, 58F2EB1D292FB954004A9BDE /* Sparkle */, + 6C147C4429A329350089B630 /* OrderedCollections */, + 6C81916A29B41DD300B75C92 /* DequeModule */, ); productName = CodeEdit; productReference = B658FB2C27DA9E0F00EA4DBD /* CodeEdit.app */; @@ -2416,6 +2480,7 @@ 58F2EB18292FB91C004A9BDE /* XCRemoteSwiftPackageReference "Preferences" */, 58F2EB1C292FB954004A9BDE /* XCRemoteSwiftPackageReference "Sparkle" */, 583E529A29361BAB001AB554 /* XCRemoteSwiftPackageReference "swift-snapshot-testing" */, + 6C147C4329A329350089B630 /* XCRemoteSwiftPackageReference "swift-collections" */, ); productRefGroup = B658FB2D27DA9E0F00EA4DBD /* Products */; projectDirPath = ""; @@ -2582,6 +2647,7 @@ 58F2EAF4292FB2B0004A9BDE /* ThemeModel.swift in Sources */, 58F2EB0D292FB2B0004A9BDE /* ThemePreferences.swift in Sources */, 587B9D9F29300ABD00AC7927 /* SegmentedControl.swift in Sources */, + 6C7256D729A3D7D000C2D3E0 /* SplitViewControllerView.swift in Sources */, 58FD7609291EA1CB0051D6E4 /* CommandPaletteView.swift in Sources */, 58F2EAFC292FB2B0004A9BDE /* GitAccountItemView.swift in Sources */, 587B9E8F29301D8F00AC7927 /* BitBucketUserRouter.swift in Sources */, @@ -2599,6 +2665,7 @@ 5878DAA7291AE76700DD95A3 /* QuickOpenViewModel.swift in Sources */, 58F2EAED292FB2B0004A9BDE /* IgnoredFileView.swift in Sources */, 587B9E6529301D8F00AC7927 /* GitLabGroupAccess.swift in Sources */, + 6C91D57229B176FF0059A90D /* TabManager.swift in Sources */, 58D01C9B293167DC00C5B6B4 /* CodeEditKeychainConstants.swift in Sources */, 58F2EB0C292FB2B0004A9BDE /* SourceControlAccounts.swift in Sources */, 20EBB50F280C389300F3A5DA /* FileInspectorModel.swift in Sources */, @@ -2608,13 +2675,14 @@ 58798234292E30B90085B254 /* FeedbackIssueArea.swift in Sources */, 587B9E5F29301D8F00AC7927 /* GitLabProjectRouter.swift in Sources */, 587B9E7329301D8F00AC7927 /* GitRouter.swift in Sources */, + 6C2C156129B4F52F00EA60A5 /* SplitViewModifiers.swift in Sources */, 0463E51327FCC1FB00806D5C /* CodeEditTargetsAPI.swift in Sources */, 201169DD2837B3AC00F92B46 /* SourceControlToolbarBottom.swift in Sources */, - 04C3254D27FF331B00C8DA2D /* ExtensionNavigatorItemView.swift in Sources */, 587B9E8B29301D8F00AC7927 /* GitHubAccount+deleteReference.swift in Sources */, D7E201B027E8C07300CB86D0 /* FindNavigatorSearchBar.swift in Sources */, 58798237292E30B90085B254 /* FeedbackView.swift in Sources */, 587B9E9829301D8F00AC7927 /* GitCommit.swift in Sources */, + 6C5228B529A868BD00AC48F6 /* Environment+ContentInsets.swift in Sources */, 587B9E9429301D8F00AC7927 /* BitBucketTokenConfiguration.swift in Sources */, 581BFB672926431000D251EC /* WelcomeWindowView.swift in Sources */, 58A5DFA329339F6400D1BD5D /* CommandManager.swift in Sources */, @@ -2646,6 +2714,7 @@ 587B9E7529301D8F00AC7927 /* String+QueryParameters.swift in Sources */, 58798219292D92370085B254 /* SearchModeModel.swift in Sources */, 58D01C9D293167DC00C5B6B4 /* KeychainSwiftAccessOptions.swift in Sources */, + 6C2C155A29B4F4CC00EA60A5 /* Variadic.swift in Sources */, 5882252B292C280D00E83CDE /* StatusBarCursorLocationLabel.swift in Sources */, 587B9D57292FC27A00AC7927 /* FolderMonitor.swift in Sources */, 58798252292E78D80085B254 /* ImageFileView.swift in Sources */, @@ -2656,6 +2725,7 @@ 587B9E8729301D8F00AC7927 /* GitHubRepositories.swift in Sources */, 587B9DA329300ABD00AC7927 /* SettingsTextEditor.swift in Sources */, 2072FA1A280D872600C7F8D4 /* LineEndings.swift in Sources */, + 6C147C4D29A32AA30089B630 /* WorkspaceTabGroupView.swift in Sources */, 58F2EAFB292FB2B0004A9BDE /* GitLabLoginView.swift in Sources */, 587B9E7B29301D8F00AC7927 /* GitHubRouter.swift in Sources */, 201169E22837B3D800F92B46 /* SourceControlNavigatorChangesView.swift in Sources */, @@ -2683,8 +2753,10 @@ 58F2EB04292FB2B0004A9BDE /* SourceControlPreferences.swift in Sources */, 582213F0291834A500EFE361 /* AboutView.swift in Sources */, 58F2EB12292FB2B0004A9BDE /* PreferencesPlaceholderView.swift in Sources */, + 6CC9E4B229B5669900C97388 /* Environment+ActiveTabGroup.swift in Sources */, 58822526292C280D00E83CDE /* StatusBarBreakpointButton.swift in Sources */, 58D01C96293167DC00C5B6B4 /* Date+Formatted.swift in Sources */, + 6C53AAD829A6C4FD00EE9ED6 /* SplitView.swift in Sources */, 587B9D9E29300ABD00AC7927 /* FontPickerView.swift in Sources */, 6C97EBCF297876E500302F95 /* AboutWindowController.swift in Sources */, 58822529292C280D00E83CDE /* StatusBarLineEndSelector.swift in Sources */, @@ -2724,6 +2796,7 @@ 58822531292C280D00E83CDE /* View+isHovering.swift in Sources */, 587B9E9929301D8F00AC7927 /* GitChangedFile.swift in Sources */, 58F2EAF2292FB2B0004A9BDE /* EditorThemeView.swift in Sources */, + 6C147C4B29A32A7B0089B630 /* Environment+SplitEditor.swift in Sources */, 2897E1C72979A29200741E32 /* OffsettableScrollView.swift in Sources */, 58F2EB0E292FB2B0004A9BDE /* SoftwareUpdater.swift in Sources */, 587B9E9529301D8F00AC7927 /* BitBucketUser.swift in Sources */, @@ -2748,7 +2821,6 @@ 587B9E6129301D8F00AC7927 /* GitLabOAuthConfiguration.swift in Sources */, 587B9E6229301D8F00AC7927 /* GitLabConfiguration.swift in Sources */, 5879821B292D92370085B254 /* SearchResultMatchModel.swift in Sources */, - 043BCF05281DA19A000AC47C /* WorkspaceDocument+Selection.swift in Sources */, 587B9E5929301D8F00AC7927 /* GitCheckoutBranchView+CheckoutBranch.swift in Sources */, 58F2EB09292FB2B0004A9BDE /* TerminalPreferences.swift in Sources */, 587D9B742933BF5700BF7490 /* FileItem+Array.swift in Sources */, @@ -2766,6 +2838,8 @@ 04C3254F2800AA4700C8DA2D /* ExtensionInstallationView.swift in Sources */, 58822530292C280D00E83CDE /* FilterTextField.swift in Sources */, 58798266292EC4080085B254 /* APIResponse.swift in Sources */, + 6C147C4929A32A080089B630 /* EditorView.swift in Sources */, + 6C147C4129A328BF0089B630 /* TabGroup.swift in Sources */, B6D7EA592971078500301FAC /* InspectorSection.swift in Sources */, 58F2EAEF292FB2B0004A9BDE /* ThemePreferencesView.swift in Sources */, B6EE989027E8879A00CDD8AB /* InspectorSidebarView.swift in Sources */, @@ -2804,13 +2878,17 @@ 28B0A19827E385C300B73177 /* NavigatorSidebarToolbarTop.swift in Sources */, 587B9E8629301D8F00AC7927 /* GitHubComment.swift in Sources */, 58F2EAE9292FB2B0004A9BDE /* SourceControlPreferencesView.swift in Sources */, + 6C147C4029A328BC0089B630 /* SplitViewData.swift in Sources */, 587B9E9029301D8F00AC7927 /* BitBucketTokenRouter.swift in Sources */, B6C6A42E29771A8D00A3D28F /* TabBarItemButtonStyle.swift in Sources */, 58822525292C280D00E83CDE /* StatusBarMenuLabel.swift in Sources */, 58F2EB11292FB2B0004A9BDE /* PreferencesToolbar.swift in Sources */, + 6C147C4229A328C10089B630 /* TabGroupData.swift in Sources */, + 6C2C155829B4F49100EA60A5 /* SplitViewItem.swift in Sources */, 58F2EAF0292FB2B0004A9BDE /* PreviewThemeView.swift in Sources */, 58F2EAFF292FB2B0004A9BDE /* KeybindingsPreferencesView.swift in Sources */, 6CDA84AD284C1BA000C1CC3A /* TabBarContextMenu.swift in Sources */, + 6C81916729B3E80700B75C92 /* ModifierKeysObserver.swift in Sources */, 0463E51127FCC1DF00806D5C /* CodeEditAPI.swift in Sources */, 587B9E8129301D8F00AC7927 /* PublicKey.swift in Sources */, 58F2EB0B292FB2B0004A9BDE /* AccountsPreferences.swift in Sources */, @@ -2823,6 +2901,7 @@ 6C05A8AF284D0CA3007F4EAA /* WorkspaceDocument+Listeners.swift in Sources */, 587D9B732933BF5700BF7490 /* FileIcon.swift in Sources */, 58F2EAF9292FB2B0004A9BDE /* AccountSelectionDialog.swift in Sources */, + 6C2C155D29B4F4E500EA60A5 /* SplitViewReader.swift in Sources */, 58F2EAE8292FB2B0004A9BDE /* TerminalPreferencesView.swift in Sources */, 58AFAA2F2933C69E00482B53 /* TabBarItemID.swift in Sources */, 28FFE1BF27E3A441001939DB /* NavigatorSidebarToolbarBottom.swift in Sources */, @@ -3775,6 +3854,14 @@ version = 2.3.0; }; }; + 6C147C4329A329350089B630 /* XCRemoteSwiftPackageReference "swift-collections" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/apple/swift-collections.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.0.0; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -3828,6 +3915,16 @@ package = 58F2EB1C292FB954004A9BDE /* XCRemoteSwiftPackageReference "Sparkle" */; productName = Sparkle; }; + 6C147C4429A329350089B630 /* OrderedCollections */ = { + isa = XCSwiftPackageProductDependency; + package = 6C147C4329A329350089B630 /* XCRemoteSwiftPackageReference "swift-collections" */; + productName = OrderedCollections; + }; + 6C81916A29B41DD300B75C92 /* DequeModule */ = { + isa = XCSwiftPackageProductDependency; + package = 6C147C4329A329350089B630 /* XCRemoteSwiftPackageReference "swift-collections" */; + productName = DequeModule; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = B658FB2427DA9E0F00EA4DBD /* Project object */; diff --git a/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 0c0ad57b7f..2f6d76eadb 100644 --- a/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -86,8 +86,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/krzyzanowskim/STTextView.git", "state" : { - "revision" : "41c7c87a552e6286ebea5f348e820b529c6f5662", - "version" : "0.4.2" + "revision" : "d6bec568df43028352c4062215fc3b5fab07c2d2", + "version" : "0.4.3" + } + }, + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-collections.git", + "state" : { + "revision" : "937e904258d22af6e447a0b72c0bc67583ef64a2", + "version" : "1.0.4" } }, { diff --git a/CodeEdit/Features/AppPreferences/Model/AppPreferencesModel.swift b/CodeEdit/Features/AppPreferences/Model/AppPreferencesModel.swift index 5ad5cd391c..7d705b2fe7 100644 --- a/CodeEdit/Features/AppPreferences/Model/AppPreferencesModel.swift +++ b/CodeEdit/Features/AppPreferences/Model/AppPreferencesModel.swift @@ -32,7 +32,6 @@ final class AppPreferencesModel: ObservableObject { var preferences: AppPreferences { didSet { try? savePreferences() - objectWillChange.send() } } diff --git a/CodeEdit/Features/Breadcrumbs/Views/BreadcrumbsComponent.swift b/CodeEdit/Features/Breadcrumbs/Views/BreadcrumbsComponent.swift index 0a48d019db..efc9d44591 100644 --- a/CodeEdit/Features/Breadcrumbs/Views/BreadcrumbsComponent.swift +++ b/CodeEdit/Features/Breadcrumbs/Views/BreadcrumbsComponent.swift @@ -6,6 +6,7 @@ // import SwiftUI +import Combine struct BreadcrumbsComponent: View { @@ -59,7 +60,9 @@ struct BreadcrumbsComponent: View { } struct NSPopUpButtonView: NSViewRepresentable where ItemType: Equatable { - @Binding var selection: ItemType + @Binding + var selection: ItemType + var popupCreator: () -> NSPopUpButton typealias NSViewType = NSPopUpButton @@ -67,6 +70,9 @@ struct BreadcrumbsComponent: View { func makeNSView(context: NSViewRepresentableContext) -> NSPopUpButton { let newPopupButton = popupCreator() setPopUpFromSelection(newPopupButton, selection: selection) + if let menu = newPopupButton.menu { + context.coordinator.registerForChanges(in: menu) + } return newPopupButton } @@ -89,24 +95,24 @@ struct BreadcrumbsComponent: View { } class Coordinator: NSObject { - var parent: NSPopUpButtonView! + var parent: NSPopUpButtonView + + var cancellable: AnyCancellable? init(_ parent: NSPopUpButtonView) { - super.init() self.parent = parent - NotificationCenter.default.addObserver( - self, - selector: #selector(dropdownItemSelected), - name: NSMenu.didSendActionNotification, - object: nil - ) + super.init() } - @objc func dropdownItemSelected(_ notification: NSNotification) { - let menuItem = (notification.userInfo?["MenuItem"])! as? NSMenuItem - if let selection = menuItem?.representedObject as? ItemType { - parent.selection = selection - } + func registerForChanges(in menu: NSMenu) { + cancellable = NotificationCenter.default + .publisher(for: NSMenu.didSendActionNotification, object: menu) + .sink { notification in + if let menuItem = notification.userInfo?["MenuItem"] as? NSMenuItem, + let selection = menuItem as? ItemType { + self.parent.selection = selection + } + } } } } diff --git a/CodeEdit/Features/Breadcrumbs/Views/BreadcrumbsView.swift b/CodeEdit/Features/Breadcrumbs/Views/BreadcrumbsView.swift index 2af31842c6..1ed08bcf32 100644 --- a/CodeEdit/Features/Breadcrumbs/Views/BreadcrumbsView.swift +++ b/CodeEdit/Features/Breadcrumbs/Views/BreadcrumbsView.swift @@ -15,9 +15,14 @@ struct BreadcrumbsView: View { @Environment(\.colorScheme) private var colorScheme + @Environment(\.isActiveTabGroup) + private var isActiveTabGroup + @Environment(\.controlActiveState) private var activeState + static let height = 27.0 + init( file: WorkspaceClient.FileItem, tappedOpenFile: @escaping (WorkspaceClient.FileItem) -> Void @@ -51,8 +56,10 @@ struct BreadcrumbsView: View { } .padding(.horizontal, 10) } - .frame(height: 27, alignment: .center) - .background(EffectView(.headerView).frame(height: 27)) + .frame(height: Self.height, alignment: .center) + .opacity(activeState == .inactive ? 0.8 : 1.0) + .grayscale(isActiveTabGroup ? 0.0 : 1.0) + .background(EffectView(.headerView).frame(height: Self.height)) } private var chevron: some View { diff --git a/CodeEdit/Features/CodeFile/CodeFileView.swift b/CodeEdit/Features/CodeFile/CodeFileView.swift index fe19c8c8fc..28051daf80 100644 --- a/CodeEdit/Features/CodeFile/CodeFileView.swift +++ b/CodeEdit/Features/CodeFile/CodeFileView.swift @@ -60,6 +60,12 @@ struct CodeFileView: View { return AppPreferencesModel.shared.preferences.textEditing.font.current() }() + @Environment(\.edgeInsets) + private var edgeInsets + + @EnvironmentObject + private var tabgroup: TabGroupData + var body: some View { CodeEditTextView( $codeFile.content, @@ -70,7 +76,8 @@ struct CodeFileView: View { lineHeight: $prefs.preferences.textEditing.lineHeightMultiple, wrapLines: $prefs.preferences.textEditing.wrapLinesToEditorWidth, cursorPosition: codeFile.$cursorPosition, - useThemeBackground: prefs.preferences.theme.useThemeBackground + useThemeBackground: prefs.preferences.theme.useThemeBackground, + contentInsets: edgeInsets.nsEdgeInsets ) .id(codeFile.fileURL) .background { @@ -90,7 +97,8 @@ struct CodeFileView: View { } } .disabled(!editable) - .frame(maxHeight: .infinity) + // minHeight zero fixes a bug where the app would freeze if the contents of the file are empty. + .frame(minHeight: .zero, maxHeight: .infinity) .onChange(of: ThemeModel.shared.selectedTheme) { newValue in guard let theme = newValue else { return } self.selectedTheme = theme diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift index f66edd98f1..84e7b99b22 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift @@ -71,6 +71,9 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate { let splitVC = CodeEditSplitViewController(workspace: workspace, feedbackPerformer: feedbackPerformer) let navigatorView = NavigatorSidebarView(workspace: workspace) + .environmentObject(workspace) + .environmentObject(workspace.tabManager) + let navigator = NSSplitViewItem( sidebarWithViewController: NSHostingController(rootView: navigatorView) ) @@ -80,7 +83,9 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate { splitVC.addSplitViewItem(navigator) let workspaceView = WindowObserver(window: window!) { - WorkspaceView(workspace: workspace) + WorkspaceView() + .environmentObject(workspace) + .environmentObject(workspace.tabManager) } let mainContent = NSSplitViewItem( @@ -90,6 +95,9 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate { splitVC.addSplitViewItem(mainContent) let inspectorView = InspectorSidebarView(workspace: workspace) + .environmentObject(workspace) + .environmentObject(workspace.tabManager) + let inspector = NSSplitViewItem( viewController: NSHostingController(rootView: inspectorView) ) @@ -113,12 +121,10 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate { if prefs.preferences.general.tabBarStyle == .native { // Set titlebar background as transparent by default in order to // style the toolbar background in native tab bar style. - self.window?.titlebarAppearsTransparent = true self.window?.titlebarSeparatorStyle = .none } else { // In xcode tab bar style, we use default toolbar background with // line separator. - self.window?.titlebarAppearsTransparent = false self.window?.titlebarSeparatorStyle = .automatic } self.window?.toolbar = toolbar @@ -234,17 +240,12 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate { } private func getSelectedCodeFile() -> CodeFileDocument? { - guard let id = workspace?.selectionState.selectedId else { return nil } - guard let item = workspace?.selectionState.openFileItems.first(where: { item in - item.tabID == id - }) else { return nil } - guard let file = workspace?.selectionState.openedCodeFiles[item] else { return nil } - return file + workspace?.tabManager.activeTabGroup.selected?.fileDocument } @IBAction func saveDocument(_ sender: Any) { getSelectedCodeFile()?.save(sender) - workspace?.convertTemporaryTab() + workspace?.tabManager.activeTabGroup.temporaryTab = nil } @IBAction func openCommandPalette(_ sender: Any) { @@ -283,11 +284,13 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate { } else { let panel = OverlayPanel() self.quickOpenPanel = panel - let contentView = QuickOpenView( - state: state, - onClose: { panel.close() }, - openFile: workspace.openTab(item:) - ) + + let contentView = QuickOpenView(state: state) { + panel.close() + } openFile: { file in + workspace.tabManager.openTab(item: file) + } + panel.contentView = NSHostingView(rootView: contentView) window?.addChildWindow(panel, ordered: .above) panel.makeKeyAndOrderFront(self) diff --git a/CodeEdit/Features/Documents/Views/WorkspaceCodeFileView.swift b/CodeEdit/Features/Documents/Views/WorkspaceCodeFileView.swift index 01b3b1e7ac..89042f4592 100644 --- a/CodeEdit/Features/Documents/Views/WorkspaceCodeFileView.swift +++ b/CodeEdit/Features/Documents/Views/WorkspaceCodeFileView.swift @@ -9,48 +9,37 @@ import SwiftUI import UniformTypeIdentifiers struct WorkspaceCodeFileView: View { + + @EnvironmentObject + private var tabManager: TabManager + @EnvironmentObject - private var workspace: WorkspaceDocument + private var tabgroup: TabGroupData + + var file: WorkspaceClient.FileItem @StateObject private var prefs: AppPreferencesModel = .shared @ViewBuilder var codeView: some View { - ZStack { - if let item = workspace.selectionState.openFileItems.first(where: { file in - if file.tabID == workspace.selectionState.selectedId { - print("Item loaded is: ", file.url) + if let document = file.fileDocument { + Group { + switch document.typeOfFile { + case .some(.text), .some(.data): + CodeFileView(codeFile: document) + default: + otherFileView(document, for: file) } - return file.tabID == workspace.selectionState.selectedId - }) { - if let fileItem = workspace.selectionState.openedCodeFiles[item] { - if fileItem.typeOfFile == .text || fileItem.typeOfFile == .data { - codeFileView(fileItem, for: item) - } else { - otherFileView(fileItem, for: item) - } - } - } else { - Text("No Editor") - .font(.system(size: 17)) - .foregroundColor(.secondary) - .frame(minHeight: 0) - .clipped() } - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - } - - @ViewBuilder - private func codeFileView( - _ codeFile: CodeFileDocument, - for item: WorkspaceClient.FileItem - ) -> some View { - VStack(spacing: 0) { - BreadcrumbsView(file: item, tappedOpenFile: workspace.openTab(item:)) - Divider() - CodeFileView(codeFile: codeFile) + .frame(maxWidth: .infinity, maxHeight: .infinity) + } else { + Spacer() + VStack(spacing: 10) { + ProgressView() + Text("Opening \(file.fileName)...") + } + Spacer() } } @@ -60,8 +49,6 @@ struct WorkspaceCodeFileView: View { for item: WorkspaceClient.FileItem ) -> some View { VStack(spacing: 0) { - BreadcrumbsView(file: item, tappedOpenFile: workspace.openTab(item:)) - Divider() if let url = otherFile.previewItemURL, let image = NSImage(contentsOf: url), diff --git a/CodeEdit/Features/Documents/WorkspaceDocument+Selection.swift b/CodeEdit/Features/Documents/WorkspaceDocument+Selection.swift deleted file mode 100644 index fb87eabd12..0000000000 --- a/CodeEdit/Features/Documents/WorkspaceDocument+Selection.swift +++ /dev/null @@ -1,65 +0,0 @@ -// -// WorkspaceDocument+Selection.swift -// CodeEdit -// -// Created by Pavel Kasila on 30.04.22. -// - -import Foundation - -struct WorkspaceSelectionState: Codable { - - var selectedId: TabBarItemID? - var openedTabs: [TabBarItemID] = [] - var temporaryTab: TabBarItemID? - var previousTemporaryTab: TabBarItemID? - - var selected: TabBarItemRepresentable? { - guard let selectedId = selectedId else { return nil } - return getItemByTab(id: selectedId) - } - - var openFileItems: [WorkspaceClient.FileItem] = [] - var openedCodeFiles: [WorkspaceClient.FileItem: CodeFileDocument] = [:] - - var openedExtensions: [Plugin] = [] - - enum CodingKeys: String, CodingKey { - case selectedId, openedTabs, temporaryTab, openedExtensions - } - - init() { - } - - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - selectedId = try container.decode(TabBarItemID?.self, forKey: .selectedId) - openedTabs = try container.decode([TabBarItemID].self, forKey: .openedTabs) - temporaryTab = try container.decode(TabBarItemID?.self, forKey: .temporaryTab) - openedExtensions = try container.decode([Plugin].self, forKey: .openedExtensions) - } - - func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(selectedId, forKey: .selectedId) - try container.encode(openedTabs, forKey: .openedTabs) - try container.encode(temporaryTab, forKey: .temporaryTab) - try container.encode(openedExtensions, forKey: .openedExtensions) - } - - /// Returns TabBarItemRepresentable by its identifier - /// - Parameter id: tab bar item's identifier - /// - Returns: item with passed identifier - func getItemByTab(id: TabBarItemID) -> TabBarItemRepresentable? { - switch id { - case .codeEditor: - return self.openFileItems.first { item in - item.tabID == id - } - case .extensionInstallation: - return self.openedExtensions.first { item in - item.tabID == id - } - } - } -} diff --git a/CodeEdit/Features/Documents/WorkspaceDocument.swift b/CodeEdit/Features/Documents/WorkspaceDocument.swift index 04bdbbca14..47ec07c00d 100644 --- a/CodeEdit/Features/Documents/WorkspaceDocument.swift +++ b/CodeEdit/Features/Documents/WorkspaceDocument.swift @@ -11,17 +11,17 @@ import SwiftUI import Combine import CodeEditKit -// swiftlint:disable type_body_length -// swiftlint:disable file_length @objc(WorkspaceDocument) final class WorkspaceDocument: NSDocument, ObservableObject, NSToolbarDelegate { var workspaceClient: WorkspaceClient? var extensionNavigatorData = ExtensionNavigatorData() @Published var sortFoldersOnTop: Bool = true - @Published var selectionState: WorkspaceSelectionState = .init() + @Published var fileItems: [WorkspaceClient.FileItem] = [] + var tabManager = TabManager() + var workspaceState: [String: Any] { get { let key = "workspaceState-\(self.fileURL?.absoluteString ?? "")" @@ -33,12 +33,16 @@ import CodeEditKit } } - var statusBarModel: StatusBarViewModel? + var statusBarModel = StatusBarViewModel() var searchState: SearchState? var quickOpenViewModel: QuickOpenViewModel? var commandsPaletteState: CommandPaletteViewModel? var listenerModel: WorkspaceNotificationModel = .init() + override init() { + super.init() + } + private var cancellables = Set() private let openTabsStateName: String = "\(String(describing: WorkspaceDocument.self))-OpenTabs" private let activeTabStateName: String = "\(String(describing: WorkspaceDocument.self))-ActiveTab" @@ -59,253 +63,6 @@ import CodeEditKit workspaceState.updateValue(value, forKey: key) } - // MARK: Open Tabs - /// Opens new tab - /// - Parameter item: any item which can be represented as a tab - func openTab(item: TabBarItemRepresentable) { - do { - updateNewlyOpenedTabs(item: item) - if selectionState.selectedId != item.tabID { - selectionState.selectedId = item.tabID - } - switch item.tabID { - case .codeEditor: - guard let file = item as? WorkspaceClient.FileItem else { return } - try self.openFile(item: file) - case .extensionInstallation: - guard let plugin = item as? Plugin else { return } - self.openExtension(item: plugin) - } - - } catch let err { - Swift.print(err) - } - } - - /// Updates the opened tabs and temporary tab. - /// - Parameter item: The item to use to update the tab state. - private func updateNewlyOpenedTabs(item: TabBarItemRepresentable) { - if !selectionState.openedTabs.contains(item.tabID) { - // If this isn't opened then we do the temp tab functionality - - // But, if there is already a temporary tab, close it first - if selectionState.temporaryTab != nil { - if let index = selectionState.openedTabs.firstIndex(of: selectionState.temporaryTab!) { - closeTemporaryTab() - selectionState.openedTabs[index] = item.tabID - } else { - selectionState.openedTabs.append(item.tabID) - } - } else { - selectionState.openedTabs.append(item.tabID) - } - - selectionState.previousTemporaryTab = selectionState.temporaryTab - selectionState.temporaryTab = item.tabID - } - } - - private func openFile(item: WorkspaceClient.FileItem) throws { - guard !selectionState.openFileItems.contains(item) else { - return - } - selectionState.openFileItems.append(item) - - let contentType = try item.url.resourceValues(forKeys: [.contentTypeKey]).contentType - let codeFile = try CodeFileDocument( - for: item.url, - withContentsOf: item.url, - ofType: contentType?.identifier ?? "" - ) - selectionState.openedCodeFiles[item] = codeFile - CodeEditDocumentController.shared.addDocument(codeFile) - Swift.print("Opening file for item: ", item.url) - } - - private func openExtension(item: Plugin) { - if !selectionState.openedExtensions.contains(item) { - selectionState.openedExtensions.append(item) - } - } - - // MARK: Close Tabs - - /// Closes single tab - /// - Parameter id: tab bar item's identifier to be closed - func closeTab(item id: TabBarItemID) { - switch id { - case .codeEditor: - guard let item = selectionState.getItemByTab(id: id) as? WorkspaceClient.FileItem else { return } - closeFileTab(item: item) - case .extensionInstallation: - guard let item = selectionState.getItemByTab(id: id) as? Plugin else { return } - closeExtensionTab(item: item) - } - } - - /// Closes collection of tab bar items - /// - Parameter items: items to be closed - func closeTabs(items: Items) where Items: Collection, Items.Element == TabBarItemID { - // TODO: Could potentially be optimized - for item in items { - closeTab(item: item) - } - } - - /// Closes tabs according to predicator - /// - Parameter predicate: predicator which returns whether tab should be closed based on its identifier - func closeTab(where predicate: (TabBarItemID) -> Bool) { - closeTabs(items: selectionState.openedTabs.filter(predicate)) - } - - /// Closes tabs after specified identifier - /// - Parameter id: identifier after which tabs will be closed - func closeTabs(after id: TabBarItemID) { - guard let startIdx = selectionState.openFileItems.firstIndex(where: { $0.tabID == id }) else { - assert(false, "Expected file item to be present in openFileItems") - return - } - - let range = selectionState.openedTabs[(startIdx+1)...] - closeTabs(items: range) - } - - /// Switched the active tab to current tab - /// - Parameter item: tab item that is now active. - func switchedTab(item: TabBarItemRepresentable) { - selectionState.selectedId = item.tabID - guard let fileItem = item as? WorkspaceClient.FileItem else { return } - self.addToWorkspaceState(key: activeTabStateName, value: fileItem.url.absoluteString) - } - - /// Tabs reordered - /// - Parameter openedTabs: reordered tabs - func reorderedTabs(openedTabs: [TabBarItemID]) { - selectionState.openedTabs = openedTabs - - if openedTabsFromState { - var openTabsInState: [String] = [] - for openTabId in openedTabs { - guard let item = selectionState.getItemByTab(id: openTabId) as? WorkspaceClient.FileItem - else { continue } - openTabsInState.append(item.url.absoluteString) - } - self.addToWorkspaceState(key: openTabsStateName, value: openTabsInState) - } - } - - /// Closes an open temporary tab, does not save the temporary tab's file. - /// Removes the tab item from `openedCodeFiles`, `openedExtensions`, and `openFileItems`. - private func closeTemporaryTab() { - guard let id = selectionState.temporaryTab else { return } - - switch id { - case .codeEditor: - guard let item = selectionState.getItemByTab(id: id) - as? WorkspaceClient.FileItem else { return } - selectionState.openedCodeFiles.removeValue(forKey: item) - case .extensionInstallation: - guard let item = selectionState.getItemByTab(id: id) - as? Plugin else { return } - closeExtensionTab(item: item) - } - - guard let openFileItemIdx = selectionState - .openFileItems - .firstIndex(where: { $0.tabID == id }) else { return } - selectionState.openFileItems.remove(at: openFileItemIdx) - } - - /// Closes an open tab, save text files only. - /// Removes the tab item from `openedCodeFiles`, `openedExtensions`, and `openFileItems`. - private func closeFileTab(item: WorkspaceClient.FileItem) { - guard let file = selectionState.openedCodeFiles[item], - let openFileItemIndex = selectionState.openFileItems.firstIndex(of: item) - else { - return - } - if file.isDocumentEdited { - let shouldClose = UnsafeMutablePointer.allocate(capacity: 1) - shouldClose.initialize(to: true) - defer { - _ = shouldClose.move() - shouldClose.deallocate() - } - file.canClose( - withDelegate: self, - shouldClose: #selector(document(_:shouldClose:contextInfo:)), - contextInfo: shouldClose - ) - guard shouldClose.pointee else { - return - } - } - selectionState.openedCodeFiles.removeValue(forKey: item) - selectionState.openFileItems.remove(at: openFileItemIndex) - removeTab(id: item.tabID) - - if openedTabsFromState { - var openTabsInState = self.getFromWorkspaceState(key: openTabsStateName) as? [String] ?? [] - if let index = openTabsInState.firstIndex(of: item.url.absoluteString) { - openTabsInState.remove(at: index) - self.addToWorkspaceState(key: openTabsStateName, value: openTabsInState) - } - } - } - - private func closeExtensionTab(item: Plugin) { - guard let idx = selectionState.openedExtensions.firstIndex(of: item) else { return } - selectionState.openedExtensions.remove(at: idx) - - removeTab(id: item.tabID) - } - - /// Makes the temporary tab permanent when a file save or edit happens. - @objc func convertTemporaryTab() { - if selectionState.selectedId == selectionState.temporaryTab && - selectionState.temporaryTab != nil { - let item = selectionState.getItemByTab(id: selectionState.temporaryTab!) - selectionState.previousTemporaryTab = selectionState.temporaryTab - selectionState.temporaryTab = nil - - guard let file = item as? WorkspaceClient.FileItem else { return } - - if openedTabsFromState && item != nil { - var openTabsInState = self.getFromWorkspaceState(key: openTabsStateName) as? [String] ?? [] - if !openTabsInState.contains(file.url.absoluteString) { - openTabsInState.append(file.url.absoluteString) - self.addToWorkspaceState(key: openTabsStateName, value: openTabsInState) - } - } - } - } - - /// Removes the tab from `openedTabs`. - /// - Parameter id: The id of `TabBarItemID` which will be removed. - private func removeTab(id: TabBarItemID) { - if id == selectionState.temporaryTab { - selectionState.previousTemporaryTab = selectionState.temporaryTab - selectionState.temporaryTab = nil - } - - guard let idx = selectionState.openedTabs.firstIndex(of: id) else { return } - let closedID = selectionState.openedTabs.remove(at: idx) - guard closedID == id else { return } - - if selectionState.openedTabs.isEmpty { - selectionState.selectedId = nil - } else if selectionState.selectedId == closedID { - // If the closed item is the selected one, then select another tab. - if idx == 0 { - selectionState.selectedId = selectionState.openedTabs.first - } else { - selectionState.selectedId = selectionState.openedTabs[idx - 1] - } - } else { - // If the closed item is not the selected one, then do nothing. - } - } - // MARK: NSDocument private let ignoredFilesAndDirectory = [ @@ -336,24 +93,21 @@ import CodeEditKit windowController.window?.setFrameAutosaveName(self.fileURL?.absoluteString ?? "Untitled") self.addWindowController(windowController) - var activeTabID: TabBarItemID? - var activeTabInState = self.getFromWorkspaceState(key: activeTabStateName) as? String ?? "" - var openTabsInState = self.getFromWorkspaceState(key: openTabsStateName) as? [String] ?? [] - for openTab in openTabsInState { - let tabUrl = URL(string: openTab)! - if FileManager.default.fileExists(atPath: tabUrl.path) { - let item = WorkspaceClient.FileItem(url: tabUrl) - self.openTab(item: item) - self.convertTemporaryTab() - if activeTabInState == openTab { - activeTabID = item.tabID - } - } - } - - if activeTabID != nil { - selectionState.selectedId = activeTabID - } + // TODO: Fix restoration +// var activeTabID: TabBarItemID? +// var activeTabInState = self.getFromWorkspaceState(key: activeTabStateName) as? String ?? "" +// var openTabsInState = self.getFromWorkspaceState(key: openTabsStateName) as? [String] ?? [] +// for openTab in openTabsInState { +// let tabUrl = URL(string: openTab)! +// if FileManager.default.fileExists(atPath: tabUrl.path) { +// let item = WorkspaceClient.FileItem(url: tabUrl) +// self.tabManager.openTab(item: item) +// self.convertTemporaryTab() +// if activeTabInState == openTab { +// activeTabID = item.tabID +// } +// } +// } self.openedTabsFromState = true } @@ -369,14 +123,6 @@ import CodeEditKit self.searchState = .init(self) self.quickOpenViewModel = .init(fileURL: url) self.commandsPaletteState = .init() - self.statusBarModel = .init(workspace: self, workspaceURL: url) - - NotificationCenter.default.addObserver( - self, - selector: #selector(convertTemporaryTab), - name: NSNotification.Name("CodeEditor.didBeginEditing"), - object: nil - ) } override func read(from url: URL, ofType typeName: String) throws { @@ -408,18 +154,6 @@ import CodeEditKit } } .store(in: &cancellables) - - // initialize extensions - ExtensionManager.shared.loadExtensions { extensionID in - CodeEditAPI(extensionId: extensionID, workspace: self) - } -// do { -// try ExtensionsManager.shared?.load { extensionID in -// CodeEditAPI(extensionId: extensionID, workspace: self) -// } -// } catch let error { -// Swift.print(error) -// } } override func write(to url: URL, ofType typeName: String) throws {} @@ -427,12 +161,6 @@ import CodeEditKit // MARK: Close Workspace override func close() { - selectionState.selectedId = nil - selectionState.openedCodeFiles.removeAll() - - if let url = self.fileURL { - ExtensionsManager.shared?.close(url: url) - } super.close() } @@ -471,7 +199,11 @@ import CodeEditKit return } // Save unsaved changes before closing - let editedCodeFiles = selectionState.openedCodeFiles.values.filter { $0.isDocumentEdited } + let editedCodeFiles = tabManager.tabGroups + .gatherOpenFiles() + .compactMap(\.fileDocument) + .filter(\.isDocumentEdited) + for editedCodeFile in editedCodeFiles { let shouldClose = UnsafeMutablePointer.allocate(capacity: 1) shouldClose.initialize(to: true) @@ -496,7 +228,9 @@ import CodeEditKit implementation, to: (@convention(c)(Any, Selector, Any, Bool, UnsafeMutableRawPointer?) -> Void).self ) - let areAllOpenedCodeFilesClean = selectionState.openedCodeFiles.values.allSatisfy { !$0.isDocumentEdited } + let areAllOpenedCodeFilesClean = tabManager.tabGroups.gatherOpenFiles() + .compactMap(\.fileDocument) + .allSatisfy { !$0.isDocumentEdited } function(object, shouldCloseSelector, self, areAllOpenedCodeFilesClean, contextInfo) } @@ -510,7 +244,7 @@ import CodeEditKit /// `shouldClose` becomes false if the user selects cancel, otherwise true. /// - contextInfo: The additional info which will be set `shouldClose`. /// `contextInfo` must be `UnsafeMutablePointer`. - @objc private func document( + @objc func document( _ document: NSDocument, shouldClose: Bool, contextInfo: UnsafeMutableRawPointer diff --git a/CodeEdit/Features/InspectorSidebar/InspectorSidebarView.swift b/CodeEdit/Features/InspectorSidebar/InspectorSidebarView.swift index 8ac356c360..718dab1db3 100644 --- a/CodeEdit/Features/InspectorSidebar/InspectorSidebarView.swift +++ b/CodeEdit/Features/InspectorSidebar/InspectorSidebarView.swift @@ -12,6 +12,9 @@ struct InspectorSidebarView: View { @ObservedObject private var workspace: WorkspaceDocument + @EnvironmentObject + private var tabManager: TabManager + @State private var selection: Int = 0 @@ -21,25 +24,22 @@ struct InspectorSidebarView: View { var body: some View { VStack { - if let item = workspace.selectionState.openFileItems.first(where: { file in - file.tabID == workspace.selectionState.selectedId - }) { - if let codeFile = workspace.selectionState.openedCodeFiles[item] { - switch selection { - case 0: - FileInspectorView( - workspaceURL: workspace.fileURL!, - fileURL: codeFile.fileURL!.path - ) - case 1: - HistoryInspectorView( - workspaceURL: workspace.fileURL!, - fileURL: codeFile.fileURL!.path - ) - case 2: - QuickHelpInspectorView().padding(5) - default: EmptyView() - } + if let path = tabManager.activeTabGroup.selected?.fileDocument?.fileURL?.path(percentEncoded: false) { + switch selection { + case 0: + FileInspectorView( + workspaceURL: workspace.fileURL!, + fileURL: path + ) + case 1: + HistoryInspectorView( + workspaceURL: workspace.fileURL!, + fileURL: path + ) + case 2: + QuickHelpInspectorView().padding(5) + default: + NoSelectionInspectorView() } } else { NoSelectionInspectorView() diff --git a/CodeEdit/Features/Keybindings/ModifierKeysObserver.swift b/CodeEdit/Features/Keybindings/ModifierKeysObserver.swift new file mode 100644 index 0000000000..8a38875c1e --- /dev/null +++ b/CodeEdit/Features/Keybindings/ModifierKeysObserver.swift @@ -0,0 +1,118 @@ +// +// ModifierKeysObserver.swift +// CodeEdit +// +// Created by Wouter Hennen on 04/03/2023. +// + +import SwiftUI +import Combine + +struct EventModifierEnvironmentKey: EnvironmentKey { + static var defaultValue: NSEvent.ModifierFlags = [] +} + +extension EnvironmentValues { + var modifierKeys: EventModifierEnvironmentKey.Value { + get { self[EventModifierEnvironmentKey.self] } + set { self[EventModifierEnvironmentKey.self] = newValue } + } +} + +extension NSEvent { + static func publisher(scope: Publisher.Scope, matching: EventTypeMask) -> Publisher { + return Publisher(scope: scope, matching: matching) + } + + public struct Publisher: Combine.Publisher { + public enum Scope { + case local, global + } + + public typealias Output = NSEvent + + public typealias Failure = Never + + let scope: Scope + let matching: EventTypeMask + + init(scope: Scope, matching: EventTypeMask) { + self.scope = scope + self.matching = matching + } + + public func receive(subscriber: S) where S: Subscriber, Self.Failure == S.Failure, Self.Output == S.Input { + let subscription = Subscription(scope: scope, matching: matching, subscriber: subscriber) + subscriber.receive(subscription: subscription) + } + } +} + +private extension NSEvent.Publisher { + final class Subscription where S.Input == NSEvent, S.Failure == Never { + fileprivate let lock = NSLock() + fileprivate var demand = Subscribers.Demand.none + private var monitor: Any? + + fileprivate let subscriberLock = NSRecursiveLock() + + init(scope: Scope, matching: NSEvent.EventTypeMask, subscriber: S) { + switch scope { + case .local: + self.monitor = NSEvent.addLocalMonitorForEvents(matching: matching) { [weak self] (event) -> NSEvent? in + self?.didReceive(event: event, subscriber: subscriber) + return event + } + + case .global: + self.monitor = NSEvent.addGlobalMonitorForEvents(matching: matching) { [weak self] in + self?.didReceive(event: $0, subscriber: subscriber) + } + } + + } + + deinit { + if let monitor = monitor { + NSEvent.removeMonitor(monitor) + } + } + + func didReceive(event: NSEvent, subscriber: S) { + let val = { () -> Subscribers.Demand in + lock.lock() + defer { lock.unlock() } + let before = demand + if demand > 0 { + demand -= 1 + } + return before + }() + + guard val > 0 else { return } + + let newDemand = subscriber.receive(event) + + lock.lock() + demand += newDemand + lock.unlock() + } + } +} + +extension NSEvent.Publisher.Subscription: Combine.Subscription { + func request(_ demand: Subscribers.Demand) { + lock.lock() + defer { lock.unlock() } + self.demand += demand + } + + func cancel() { + lock.lock() + defer { lock.unlock() } + guard let monitor = monitor else { return } + + self.monitor = nil + NSEvent.removeMonitor(monitor) + } +} diff --git a/CodeEdit/Features/NavigatorSidebar/ExtensionNavigator/ExtensionNavigatorItemView.swift b/CodeEdit/Features/NavigatorSidebar/ExtensionNavigator/ExtensionNavigatorItemView.swift deleted file mode 100644 index 3a4024e2a3..0000000000 --- a/CodeEdit/Features/NavigatorSidebar/ExtensionNavigator/ExtensionNavigatorItemView.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// ExtensionNavigatorItem.swift -// CodeEdit -// -// Created by Pavel Kasila on 7.04.22. -// - -import SwiftUI - -struct ExtensionNavigatorItemView: View { - var plugin: Plugin - @EnvironmentObject var document: WorkspaceDocument - - var body: some View { - Button { - document.openTab(item: plugin) - } label: { - ZStack { - HStack { - VStack(alignment: .leading) { - Text(plugin.manifest.displayName) - .font(.headline) - Text(plugin.manifest.name) - .font(.subheadline) - } - Spacer() - } - } - } - .buttonStyle(.plain) - } -} diff --git a/CodeEdit/Features/NavigatorSidebar/ExtensionNavigator/ExtensionNavigatorView.swift b/CodeEdit/Features/NavigatorSidebar/ExtensionNavigator/ExtensionNavigatorView.swift index df6b865a36..0e4568ea4a 100644 --- a/CodeEdit/Features/NavigatorSidebar/ExtensionNavigator/ExtensionNavigatorView.swift +++ b/CodeEdit/Features/NavigatorSidebar/ExtensionNavigator/ExtensionNavigatorView.swift @@ -18,11 +18,6 @@ struct ExtensionNavigatorView: View { VStack { Divider() // TODO: fix this workaround because when switching tabs without this, the app crashes List { - ForEach(workspace.extensionNavigatorData.plugins) { plugin in - ExtensionNavigatorItemView(plugin: plugin) - .tag(plugin) - } - if !workspace.extensionNavigatorData.listFull { HStack { Spacer() diff --git a/CodeEdit/Features/NavigatorSidebar/FindNavigator/FindNavigatorResultList/FindNavigatorListViewController.swift b/CodeEdit/Features/NavigatorSidebar/FindNavigator/FindNavigatorResultList/FindNavigatorListViewController.swift index e942c40381..6668c7b4f6 100644 --- a/CodeEdit/Features/NavigatorSidebar/FindNavigator/FindNavigatorResultList/FindNavigatorListViewController.swift +++ b/CodeEdit/Features/NavigatorSidebar/FindNavigator/FindNavigatorResultList/FindNavigatorListViewController.swift @@ -204,13 +204,13 @@ extension FindNavigatorListViewController: NSOutlineViewDelegate { let selectedMatch = self.selectedItem as? SearchResultMatchModel if selectedItem == nil || selectedMatch != item { self.selectedItem = item - workspace.openTab(item: item.file) + workspace.tabManager.openTab(item: item.file) } } else if let item = outlineView.item(atRow: selectedIndex) as? SearchResultModel { let selectedFile = self.selectedItem as? SearchResultModel if selectedItem == nil || selectedFile != item { self.selectedItem = item - workspace.openTab(item: item.file) + workspace.tabManager.openTab(item: item.file) } } } diff --git a/CodeEdit/Features/NavigatorSidebar/NavigatorSidebarToolbarBottom.swift b/CodeEdit/Features/NavigatorSidebar/NavigatorSidebarToolbarBottom.swift index 3d743f6a61..8c698c3a49 100644 --- a/CodeEdit/Features/NavigatorSidebar/NavigatorSidebarToolbarBottom.swift +++ b/CodeEdit/Features/NavigatorSidebar/NavigatorSidebarToolbarBottom.swift @@ -20,7 +20,7 @@ struct NavigatorSidebarToolbarBottom: View { Spacer() sortButton } - .frame(height: 29, alignment: .center) + .frame(height: 29) .frame(maxWidth: .infinity) .padding(.horizontal, 4) .overlay(alignment: .top) { @@ -39,7 +39,7 @@ struct NavigatorSidebarToolbarBottom: View { guard let newFileItem = try? workspace.workspaceClient?.getFileItem(newFile) else { return } - workspace.openTab(item: newFileItem) + workspace.tabManager.openTab(item: newFileItem) } } diff --git a/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/OutlineMenu.swift b/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/OutlineMenu.swift index f9031a2ff1..1bd682d820 100644 --- a/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/OutlineMenu.swift +++ b/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/OutlineMenu.swift @@ -182,7 +182,7 @@ final class OutlineMenu: NSMenu { @objc private func openInTab() { if let item = item { - workspace?.openTab(item: item) + workspace?.tabManager.openTab(item: item) } } diff --git a/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/OutlineView.swift b/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/OutlineView.swift index 37f75a036c..606b41e620 100644 --- a/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/OutlineView.swift +++ b/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/OutlineView.swift @@ -17,6 +17,10 @@ struct OutlineView: NSViewControllerRepresentable { @StateObject var prefs: AppPreferencesModel = .shared + // This is mainly just used to trigger a view update. + @Binding + var selection: WorkspaceClient.FileItem? + typealias NSViewControllerType = OutlineViewController func makeNSViewController(context: Context) -> OutlineViewController { diff --git a/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/OutlineViewController.swift b/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/OutlineViewController.swift index cbab79a6ea..fa0737ae1b 100644 --- a/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/OutlineViewController.swift +++ b/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/OutlineViewController.swift @@ -90,12 +90,12 @@ final class OutlineViewController: NSViewController { /// /// Most importantly when the `id` changes from an external view. func updateSelection() { - guard let itemID = workspace?.selectionState.selectedId else { + guard let itemID = workspace?.tabManager.activeTabGroup.selected?.id else { outlineView.deselectRow(outlineView.selectedRow) return } - select(by: itemID, from: content) + select(by: .codeEditor(itemID), from: content) } /// Expand or collapse the folder on double click @@ -110,9 +110,7 @@ final class OutlineViewController: NSViewController { outlineView.expandItem(item) } } else { - if workspace?.selectionState.temporaryTab == item.tabID { - workspace?.convertTemporaryTab() - } + workspace?.tabManager.activeTabGroup.openTab(item: item, asTemporary: false) } } @@ -275,7 +273,7 @@ extension OutlineViewController: NSOutlineViewDelegate { guard let item = outlineView.item(atRow: selectedIndex) as? Item else { return } if item.children == nil && shouldSendSelectionUpdate { - workspace?.openTab(item: item) + workspace?.tabManager.activeTabGroup.openTab(item: item, asTemporary: true) } } diff --git a/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/OutlintViewController+OutlineTableViewCellDelegate.swift b/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/OutlintViewController+OutlineTableViewCellDelegate.swift index e15becf403..9fc423f3f5 100644 --- a/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/OutlintViewController+OutlineTableViewCellDelegate.swift +++ b/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/OutlineView/OutlintViewController+OutlineTableViewCellDelegate.swift @@ -12,11 +12,11 @@ import Foundation extension OutlineViewController: OutlineTableViewCellDelegate { func moveFile(file: Item, to destination: URL) { if !file.isFolder { - workspace?.closeTab(item: .codeEditor(file.id)) + workspace?.tabManager.tabGroups.closeAllTabs(of: file) } file.move(to: destination) if !file.isFolder { - workspace?.openTab(item: file) + workspace?.tabManager.openTab(item: file) } } diff --git a/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/ProjectNavigatorView.swift b/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/ProjectNavigatorView.swift index ed9ca184eb..755a02f3dd 100644 --- a/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/ProjectNavigatorView.swift +++ b/CodeEdit/Features/NavigatorSidebar/ProjectNavigator/ProjectNavigatorView.swift @@ -16,7 +16,9 @@ import SwiftUI /// struct ProjectNavigatorView: View { + @EnvironmentObject var tabManager: TabManager + var body: some View { - OutlineView() + OutlineView(selection: $tabManager.activeTabGroup.selected) } } diff --git a/CodeEdit/Features/SplitView/EditorView.swift b/CodeEdit/Features/SplitView/EditorView.swift new file mode 100644 index 0000000000..e1ce901070 --- /dev/null +++ b/CodeEdit/Features/SplitView/EditorView.swift @@ -0,0 +1,98 @@ +// +// EditorView.swift +// CodeEdit +// +// Created by Wouter Hennen on 20/02/2023. +// + +import SwiftUI + +struct EditorView: View { + var tabgroup: TabGroup + + @FocusState.Binding + var focus: TabGroupData? + + @Environment(\.window) + private var window + + @Environment(\.isAtEdge) + private var isAtEdge + + var toolbarHeight: CGFloat { + window.contentView?.safeAreaInsets.top ?? .zero + } + + var body: some View { + VStack { + switch tabgroup { + case .one(let detailTabGroup): + WorkspaceTabGroupView(tabgroup: detailTabGroup, focus: $focus) + .transformEnvironment(\.edgeInsets) { insets in + switch isAtEdge { + case .all: + insets.top += toolbarHeight + insets.bottom += StatusBarView.height + 5 + case .top: + insets.top += toolbarHeight + case .bottom: + insets.bottom += StatusBarView.height + 5 + default: + return + } + } + case .vertical(let data), .horizontal(let data): + SubEditorView(data: data, focus: $focus) + } + } + } + + struct SubEditorView: View { + @ObservedObject var data: SplitViewData + + @FocusState.Binding var focus: TabGroupData? + + var body: some View { + SplitView(axis: data.axis) { + splitView + } + .edgesIgnoringSafeArea([.top, .bottom]) + } + + var splitView: some View { + ForEach(Array(data.tabgroups.enumerated()), id: \.offset) { index, item in + EditorView(tabgroup: item, focus: $focus) + .transformEnvironment(\.isAtEdge) { belowToolbar in + calcIsAtEdge(current: &belowToolbar, index: index) + } + .environment(\.splitEditor) { edge, newTabGroup in + data.split(edge, at: index, new: newTabGroup) + } + } + } + + func calcIsAtEdge(current: inout VerticalEdge.Set, index: Int) { + if case .vertical = data.axis { + guard data.tabgroups.count != 1 else { return } + if index == data.tabgroups.count - 1 { + current.remove(.top) + } else if index == 0 { + current.remove(.bottom) + } else { + current = [] + } + } + } + } +} + +private struct BelowToolbarEnvironmentKey: EnvironmentKey { + static var defaultValue: VerticalEdge.Set = .all +} + +extension EnvironmentValues { + fileprivate var isAtEdge: BelowToolbarEnvironmentKey.Value { + get { self[BelowToolbarEnvironmentKey.self] } + set { self[BelowToolbarEnvironmentKey.self] = newValue } + } +} diff --git a/CodeEdit/Features/SplitView/Environment+ContentInsets.swift b/CodeEdit/Features/SplitView/Environment+ContentInsets.swift new file mode 100644 index 0000000000..449f88e707 --- /dev/null +++ b/CodeEdit/Features/SplitView/Environment+ContentInsets.swift @@ -0,0 +1,25 @@ +// +// Environment+ContentInsets.swift +// CodeEdit +// +// Created by Wouter Hennen on 24/02/2023. +// + +import SwiftUI + +struct EdgeInsetsEnvironmentKey: EnvironmentKey { + static var defaultValue: EdgeInsets = .init() +} + +extension EnvironmentValues { + var edgeInsets: EdgeInsetsEnvironmentKey.Value { + get { self[EdgeInsetsEnvironmentKey.self] } + set { self[EdgeInsetsEnvironmentKey.self] = newValue } + } +} + +extension EdgeInsets { + var nsEdgeInsets: NSEdgeInsets { + .init(top: top, left: leading, bottom: bottom, right: trailing) + } +} diff --git a/CodeEdit/Features/SplitView/Environment+SplitEditor.swift b/CodeEdit/Features/SplitView/Environment+SplitEditor.swift new file mode 100644 index 0000000000..a96c0fab56 --- /dev/null +++ b/CodeEdit/Features/SplitView/Environment+SplitEditor.swift @@ -0,0 +1,19 @@ +// +// Environment+SplitEditor.swift +// CodeEdit +// +// Created by Wouter Hennen on 16/02/2023. +// + +import SwiftUI + +struct SplitEditorEnvironmentKey: EnvironmentKey { + static var defaultValue: (Edge, TabGroupData) -> Void = { _, _ in } +} + +extension EnvironmentValues { + var splitEditor: SplitEditorEnvironmentKey.Value { + get { self[SplitEditorEnvironmentKey.self] } + set { self[SplitEditorEnvironmentKey.self] = newValue } + } +} diff --git a/CodeEdit/Features/SplitView/SplitView.swift b/CodeEdit/Features/SplitView/SplitView.swift new file mode 100644 index 0000000000..9740699e4d --- /dev/null +++ b/CodeEdit/Features/SplitView/SplitView.swift @@ -0,0 +1,30 @@ +// +// SequenceView.swift +// CodeEdit +// +// Created by Wouter Hennen on 22/02/2023. +// + +import SwiftUI + +struct SplitView: View { + var content: Content + + @State + var viewController: SplitViewController + + init(axis: Axis, @ViewBuilder content: () -> Content) { + self.content = content() + let vc = SplitViewController(axis: axis) + self._viewController = .init(wrappedValue: vc) + } + + var body: some View { + VStack { + content.variadic { children in + SplitViewControllerView(children: children, viewController: viewController) + } + } + ._trait(SplitViewControllerLayoutValueKey.self, viewController) + } +} diff --git a/CodeEdit/Features/SplitView/SplitViewControllerView.swift b/CodeEdit/Features/SplitView/SplitViewControllerView.swift new file mode 100644 index 0000000000..e9ab1585f6 --- /dev/null +++ b/CodeEdit/Features/SplitView/SplitViewControllerView.swift @@ -0,0 +1,89 @@ +// +// EditorSplitView.swift +// CodeEdit +// +// Created by Wouter Hennen on 20/02/2023. +// + +import SwiftUI + +struct SplitViewControllerView: NSViewControllerRepresentable { + + var children: _VariadicView.Children + var viewController: SplitViewController + + func makeNSViewController(context: Context) -> SplitViewController { + return viewController + } + + func updateNSViewController(_ controller: SplitViewController, context: Context) { + updateItems(controller: controller) + } + + private func updateItems(controller: SplitViewController) { + var hasChanged = false + // Reorder viewcontrollers if needed and add new ones. + controller.items = children.map { child in + let item: SplitViewItem + if let foundItem = controller.items.first(where: { $0.id == child.id }) { + item = foundItem + item.update(child: child) + } else { + hasChanged = true + item = SplitViewItem(child: child) + } + return item + } + + controller.splitViewItems = controller.items.map(\.item) + + if hasChanged && controller.splitViewItems.count > 1 { + let splitView = controller.splitView + let numerator = splitView.isVertical ? splitView.frame.width : splitView.frame.height + + for idx in 0.. Bool { + false + } + + func collapse(for id: AnyHashable, enabled: Bool) { + items.first { $0.id == id }?.item.animator().isCollapsed = enabled + } +} diff --git a/CodeEdit/Features/SplitView/SplitViewData.swift b/CodeEdit/Features/SplitView/SplitViewData.swift new file mode 100644 index 0000000000..b66715757e --- /dev/null +++ b/CodeEdit/Features/SplitView/SplitViewData.swift @@ -0,0 +1,75 @@ +// +// WorkspaceSplitViewData.swift +// CodeEdit +// +// Created by Wouter Hennen on 16/02/2023. +// + +import SwiftUI + +class SplitViewData: ObservableObject { + @Published var tabgroups: [TabGroup] + + var axis: Axis + + init(_ axis: Axis, tabgroups: [TabGroup] = []) { + self.tabgroups = tabgroups + self.axis = axis + + tabgroups.forEach { + if case .one(let tabGroupData) = $0 { + tabGroupData.parent = self + } + } + } + + /// Splits the editor at a certain index into two separate editors. + /// - Parameters: + /// - direction: direction in which the editor will be split. + /// If the direction is the same as the ancestor direction, + /// the editor is added to the ancestor instead of creating a new split container. + /// - index: index where the divider will be added. + /// - tabgroup: new tabgroup class that will be used for the editor. + func split(_ direction: Edge, at index: Int, new tabgroup: TabGroupData) { + tabgroup.parent = self + switch (axis, direction) { + case (.horizontal, .trailing), (.vertical, .bottom): + tabgroups.insert(.one(tabgroup), at: index+1) + + case (.horizontal, .leading), (.vertical, .top): + tabgroups.insert(.one(tabgroup), at: index) + + case (.horizontal, .top): + tabgroups[index] = .vertical(.init(.vertical, tabgroups: [.one(tabgroup), tabgroups[index]])) + + case (.horizontal, .bottom): + tabgroups[index] = .vertical(.init(.vertical, tabgroups: [tabgroups[index], .one(tabgroup)])) + + case (.vertical, .leading): + tabgroups[index] = .horizontal(.init(.horizontal, tabgroups: [.one(tabgroup), tabgroups[index]])) + + case (.vertical, .trailing): + tabgroups[index] = .horizontal(.init(.horizontal, tabgroups: [tabgroups[index], .one(tabgroup)])) + } + } + + /// Closes a TabGroup. + /// - Parameter id: ID of the TabGroup. + func closeTabGroup(with id: TabGroupData.ID) { + tabgroups.removeAll { tabgroup in + if case .one(let tabGroupData) = tabgroup { + if tabGroupData.id == id { + return true + } + } + return false + } + } + + /// Flattens the splitviews. + func flatten() { + for index in tabgroups.indices { + tabgroups[index].flatten(parent: self) + } + } +} diff --git a/CodeEdit/Features/SplitView/SplitViewItem.swift b/CodeEdit/Features/SplitView/SplitViewItem.swift new file mode 100644 index 0000000000..f8b2388b72 --- /dev/null +++ b/CodeEdit/Features/SplitView/SplitViewItem.swift @@ -0,0 +1,50 @@ +// +// SplitViewItem.swift +// CodeEdit +// +// Created by Wouter Hennen on 05/03/2023. +// + +import SwiftUI +import Combine + +class SplitViewItem: ObservableObject { + + var id: AnyHashable + var item: NSSplitViewItem + + var collapsed: Binding + + var cancellables: [AnyCancellable] = [] + + var observers: [NSKeyValueObservation] = [] + + init(child: _VariadicView.Children.Element) { + self.id = child.id + self.item = NSSplitViewItem(viewController: NSHostingController(rootView: child)) + self.collapsed = child[SplitViewItemCollapsedViewTraitKey.self] + self.item.canCollapse = child[SplitViewItemCanCollapseViewTraitKey.self] + self.item.isCollapsed = self.collapsed.wrappedValue + self.observers = createObservers() + } + + private func createObservers() -> [NSKeyValueObservation] { + [ + item.observe(\.isCollapsed) { item, _ in + self.collapsed.wrappedValue = item.isCollapsed + } + ] + } + + /// Updates a SplitViewItem. + /// This will fetch updated binding values and update them if needed. + /// - Parameter child: the view corresponding to the SplitViewItem. + func update(child: _VariadicView.Children.Element) { + self.item.canCollapse = child[SplitViewItemCanCollapseViewTraitKey.self] + DispatchQueue.main.async { + self.observers = [] + self.item.animator().isCollapsed = child[SplitViewItemCollapsedViewTraitKey.self].wrappedValue + self.observers = self.createObservers() + } + } +} diff --git a/CodeEdit/Features/SplitView/SplitViewModifiers.swift b/CodeEdit/Features/SplitView/SplitViewModifiers.swift new file mode 100644 index 0000000000..832a34e169 --- /dev/null +++ b/CodeEdit/Features/SplitView/SplitViewModifiers.swift @@ -0,0 +1,37 @@ +// +// SplitViewModifiers.swift +// CodeEdit +// +// Created by Wouter Hennen on 05/03/2023. +// + +import SwiftUI + +struct SplitViewControllerLayoutValueKey: _ViewTraitKey { + static var defaultValue: SplitViewController? +} + +struct SplitViewItemCollapsedViewTraitKey: _ViewTraitKey { + static var defaultValue: Binding = .constant(false) +} + +struct SplitViewItemCanCollapseViewTraitKey: _ViewTraitKey { + static var defaultValue: Bool = false +} + +extension View { + func collapsed(_ value: Binding) -> some View { + self + // Use get/set instead of binding directly, so a view update will be triggered if the binding changes. + ._trait(SplitViewItemCollapsedViewTraitKey.self, .init { + value.wrappedValue + } set: { + value.wrappedValue = $0 + }) + } + + func collapsable() -> some View { + self + ._trait(SplitViewItemCanCollapseViewTraitKey.self, true) + } +} diff --git a/CodeEdit/Features/SplitView/SplitViewReader.swift b/CodeEdit/Features/SplitView/SplitViewReader.swift new file mode 100644 index 0000000000..faba280ba2 --- /dev/null +++ b/CodeEdit/Features/SplitView/SplitViewReader.swift @@ -0,0 +1,61 @@ +// +// SplitViewReader.swift +// CodeEdit +// +// Created by Wouter Hennen on 05/03/2023. +// + +import SwiftUI + +struct SplitViewReader: View { + + @ViewBuilder var content: (SplitViewProxy) -> Content + + @State private var viewController: SplitViewController? + + private var proxy: SplitViewProxy { + .init { + viewController + } + } + + var body: some View { + content(proxy) + .variadic { children in + ForEach(children, id: \.id) { child in + child + .task { + if let vc = child[SplitViewControllerLayoutValueKey.self] { + viewController = vc + } + } + } + } + } +} + +struct SplitViewProxy { + private var viewController: () -> SplitViewController? + + fileprivate init(viewController: @escaping () -> SplitViewController?) { + self.viewController = viewController + } + + /// Set the position of a divider in a splitview. + /// - Parameters: + /// - index: index of the divider. The mostleft / top divider has index 0. + /// - position: position to place the divider. This is a position inside the views width / height. + /// For example, if the splitview has a width of 500, setting the position to 250 + /// will put the divider in the middle of the splitview. + func setPosition(of index: Int, position: CGFloat) { + viewController()?.splitView.setPosition(position, ofDividerAt: index) + } + + /// Collapse a view of the splitview. + /// - Parameters: + /// - id: ID of the view + /// - enabled: true for collapse. + func collapseView(with id: AnyHashable, _ enabled: Bool) { + viewController()?.collapse(for: id, enabled: enabled) + } +} diff --git a/CodeEdit/Features/SplitView/Variadic.swift b/CodeEdit/Features/SplitView/Variadic.swift new file mode 100644 index 0000000000..d3b7f94a1b --- /dev/null +++ b/CodeEdit/Features/SplitView/Variadic.swift @@ -0,0 +1,25 @@ +// +// Variadic.swift +// CodeEdit +// +// Created by Wouter Hennen on 05/03/2023. +// + +import SwiftUI + +// swiftlint:disable identifier_name +struct Helper: _VariadicView_UnaryViewRoot { + var _body: (_VariadicView.Children) -> Result + + func body(children: _VariadicView.Children) -> some View { + _body(children) + } +} + +extension View { + + /// Exposes the children of a ViewBuilder so they can be accessed individually. + func variadic(@ViewBuilder process: @escaping (_VariadicView.Children) -> R) -> some View { + _VariadicView.Tree(Helper(_body: process), content: { self }) + } +} diff --git a/CodeEdit/Features/StatusBar/ViewModels/StatusBarViewModel.swift b/CodeEdit/Features/StatusBar/ViewModels/StatusBarViewModel.swift index be0b08fa3c..c2503e2c0e 100644 --- a/CodeEdit/Features/StatusBar/ViewModels/StatusBarViewModel.swift +++ b/CodeEdit/Features/StatusBar/ViewModels/StatusBarViewModel.swift @@ -58,11 +58,6 @@ class StatusBarViewModel: ObservableObject { /// Returns the font for status bar items to use private(set) var toolbarFont: Font = .system(size: 11) - private(set) var workspace: WorkspaceDocument - - /// The base URL of the workspace - private(set) var workspaceURL: URL - /// The maximum height of the drawer /// when isMaximized is true the height gets set to maxHeight private(set) var maxHeight: Double = 5000 @@ -73,29 +68,7 @@ class StatusBarViewModel: ObservableObject { /// The minimum height of the drawe private(set) var minHeight: Double = 100 - /// Initialize with a GitClient - /// - Parameter workspaceURL: the current workspace URL - init(workspace: WorkspaceDocument, workspaceURL: URL) { - self.workspace = workspace - self.workspaceURL = workspaceURL - - var currentHeight = workspace.getFromWorkspaceState(key: statusBarDrawerHeightStateName) as? Double - ?? self.standardHeight - if currentHeight == 0 { - currentHeight = self.standardHeight - } - - self.isExpanded = workspace.getFromWorkspaceState(key: isStatusBarDrawerCollapsedStateName) as? Bool ?? false - if self.isExpanded { - self.currentHeight = currentHeight - } - } - - func saveIsExpandedToState() { - self.workspace.addToWorkspaceState(key: isStatusBarDrawerCollapsedStateName, value: self.isExpanded) - } - - func saveHeightToState(height: Double) { - self.workspace.addToWorkspaceState(key: statusBarDrawerHeightStateName, value: height) + init() { + // !!!: Lots of things in this class can be removed, such as maxHeight, as they are defined in the UI. } } diff --git a/CodeEdit/Features/StatusBar/Views/StatusBarDrawer/StatusBarDrawer.swift b/CodeEdit/Features/StatusBar/Views/StatusBarDrawer/StatusBarDrawer.swift index d9a24b5e1d..c2add2fd03 100644 --- a/CodeEdit/Features/StatusBar/Views/StatusBarDrawer/StatusBarDrawer.swift +++ b/CodeEdit/Features/StatusBar/Views/StatusBarDrawer/StatusBarDrawer.swift @@ -9,10 +9,7 @@ import SwiftUI struct StatusBarDrawer: View { @EnvironmentObject - private var model: StatusBarViewModel - - @ObservedObject - private var prefs: AppPreferencesModel = .shared + private var workspace: WorkspaceDocument @Environment(\.colorScheme) private var colorScheme @@ -20,61 +17,23 @@ struct StatusBarDrawer: View { @State private var searchText = "" - var height: CGFloat { - if model.isMaximized { - return model.maxHeight - } - if model.isExpanded { - return model.currentHeight - } - return 0 - } - var body: some View { - VStack(spacing: 0) { - GeometryReader { geometryProxy in - switch model.selectedTab { - case 0: - TerminalEmulatorView(url: model.workspaceURL) - .background { - if colorScheme == .dark { - if prefs.preferences.theme.selectedTheme == prefs.preferences.theme.selectedLightTheme { - Color.white - } else { - EffectView(.underPageBackground) - } - } else { - if prefs.preferences.theme.selectedTheme == prefs.preferences.theme.selectedDarkTheme { - Color.black - } else { - EffectView(.contentBackground) - } - } - } - // When size changes, save new height to workspace state. - .onChange(of: geometryProxy.size.height) { _ in - model.saveHeightToState(height: geometryProxy.size.height) - } - default: Rectangle().foregroundColor(Color(nsColor: .textBackgroundColor)) + if let url = workspace.workspaceClient?.folderURL() { + VStack(spacing: 0) { + TerminalEmulatorView(url: url) + HStack(alignment: .center, spacing: 10) { + FilterTextField(title: "Filter", text: $searchText) + .frame(maxWidth: 300) + Spacer() + StatusBarClearButton() + Divider() + StatusBarSplitTerminalButton() + StatusBarMaximizeButton() } + .padding(10) + .frame(maxHeight: 29) + .background(.bar) } - HStack(alignment: .center, spacing: 10) { - FilterTextField(title: "Filter", text: $searchText) - .frame(maxWidth: 300) - Spacer() - StatusBarClearButton() - Divider() - StatusBarSplitTerminalButton() - StatusBarMaximizeButton() - } - .padding(10) - .frame(maxHeight: 29) - .background(.bar) } - .frame( - minHeight: 0, - idealHeight: height, - maxHeight: height - ) } } diff --git a/CodeEdit/Features/StatusBar/Views/StatusBarItems/StatusBarToggleDrawerButton.swift b/CodeEdit/Features/StatusBar/Views/StatusBarItems/StatusBarToggleDrawerButton.swift index bf0cf3e613..7408829798 100644 --- a/CodeEdit/Features/StatusBar/Views/StatusBarItems/StatusBarToggleDrawerButton.swift +++ b/CodeEdit/Features/StatusBar/Views/StatusBarItems/StatusBarToggleDrawerButton.swift @@ -11,7 +11,11 @@ internal struct StatusBarToggleDrawerButton: View { @EnvironmentObject private var model: StatusBarViewModel - init() { + @Binding + var collapsed: Bool + + init(collapsed: Binding) { + self._collapsed = collapsed CommandManager.shared.addCommand( name: "Toggle Drawer", title: "Toggle Drawer", @@ -23,11 +27,8 @@ internal struct StatusBarToggleDrawerButton: View { func togglePanel() { withAnimation { model.isExpanded.toggle() - if model.isExpanded && model.currentHeight < 1 { - model.currentHeight = 300 - } + collapsed.toggle() } - self.model.saveIsExpandedToState() } internal var body: some View { @@ -38,7 +39,7 @@ internal struct StatusBarToggleDrawerButton: View { Image(systemName: "rectangle.bottomthird.inset.filled") .imageScale(.medium) } - .tint(model.isExpanded ? .accentColor : .primary) + .tint(collapsed ? .primary : .accentColor) .keyboardShortcut("Y", modifiers: [.command, .shift]) .buttonStyle(.borderless) .onHover { isHovering($0) } diff --git a/CodeEdit/Features/StatusBar/Views/StatusBarView.swift b/CodeEdit/Features/StatusBar/Views/StatusBarView.swift index 0cffcfa17c..0e3c5eb10c 100644 --- a/CodeEdit/Features/StatusBar/Views/StatusBarView.swift +++ b/CodeEdit/Features/StatusBar/Views/StatusBarView.swift @@ -23,24 +23,25 @@ struct StatusBarView: View { @EnvironmentObject private var model: StatusBarViewModel - var body: some View { - VStack(spacing: 0) { - bar - if model.isExpanded { - StatusBarDrawer() - .transition(.move(edge: .bottom)) - } - } - .disabled(controlActive == .inactive) - // removes weird light gray bar above when in light mode - .padding(.top, -8) // (comment out to make it look normal in preview) - } + @ObservedObject + private var prefs: AppPreferencesModel = .shared + + static let height = 29.0 + + @Environment(\.colorScheme) + private var colorScheme + + var proxy: SplitViewProxy + + @Binding + var collapsed: Bool + + static let statusbarID = "statusbarID" /// The actual status bar - private var bar: some View { - ZStack { - Rectangle() - .foregroundStyle(.bar) + var body: some View { + VStack(spacing: 4) { + Divider() HStack(spacing: 15) { HStack(spacing: 5) { StatusBarBreakpointButton() @@ -48,50 +49,42 @@ struct StatusBarView: View { .frame(maxHeight: 12) .padding(.horizontal, 7) SegmentedControl($model.selectedTab, options: StatusBarTabType.allOptions) - .opacity(model.isExpanded ? 1 : 0) + .opacity(collapsed ? 0 : 1) } Spacer() StatusBarCursorLocationLabel() StatusBarIndentSelector() StatusBarEncodingSelector() StatusBarLineEndSelector() - StatusBarToggleDrawerButton() + StatusBarToggleDrawerButton(collapsed: $collapsed) } .padding(.horizontal, 10) + .padding(.bottom, 3) } - .overlay(alignment: .top) { - PanelDivider() - } - .overlay(alignment: .bottom) { - if model.isExpanded { - PanelDivider() - } - } - .frame(height: 29) + .cursor(.resizeUpDown) + .frame(height: Self.height) + .background(.bar) .gesture(dragGesture) - .onHover { isHovering($0, isDragging: model.isDragging, cursor: .resizeUpDown) } + .disabled(controlActive == .inactive) } /// A drag gesture to resize the drawer beneath the status bar private var dragGesture: some Gesture { - DragGesture() + DragGesture(coordinateSpace: .global) .onChanged { value in - model.isDragging = true - var newHeight = max(0, min(model.currentHeight - value.translation.height, 500)) - if newHeight-0.5 > model.currentHeight || newHeight+0.5 < model.currentHeight { - if newHeight < model.minHeight { // simulate the snapping/resistance after reaching minimal height - if newHeight > model.minHeight / 2 { - newHeight = model.minHeight - } else { - newHeight = 0 - } - } - model.currentHeight = newHeight - } - model.isExpanded = model.currentHeight < 1 ? false : true + proxy.setPosition(of: 0, position: value.location.y + Self.height / 2) } - .onEnded { _ in - model.isDragging = false + } +} + +extension View { + func cursor(_ cursor: NSCursor) -> some View { + onHover { + if $0 { + cursor.push() + } else { + cursor.pop() } + } } } diff --git a/CodeEdit/Features/TabBar/Models/TabBarItemID.swift b/CodeEdit/Features/Tabs/Models/TabBarItemID.swift similarity index 100% rename from CodeEdit/Features/TabBar/Models/TabBarItemID.swift rename to CodeEdit/Features/Tabs/Models/TabBarItemID.swift diff --git a/CodeEdit/Features/TabBar/Models/TabBarItemRepresentable.swift b/CodeEdit/Features/Tabs/Models/TabBarItemRepresentable.swift similarity index 100% rename from CodeEdit/Features/TabBar/Models/TabBarItemRepresentable.swift rename to CodeEdit/Features/Tabs/Models/TabBarItemRepresentable.swift diff --git a/CodeEdit/Features/Tabs/Models/TabManager.swift b/CodeEdit/Features/Tabs/Models/TabManager.swift new file mode 100644 index 0000000000..8ff36c76ac --- /dev/null +++ b/CodeEdit/Features/Tabs/Models/TabManager.swift @@ -0,0 +1,50 @@ +// +// TabManager.swift +// CodeEdit +// +// Created by Wouter Hennen on 03/03/2023. +// + +import Foundation +import OrderedCollections +import DequeModule + +class TabManager: ObservableObject { + /// Collection of all the tabgroups. + @Published var tabGroups: TabGroup + + /// The TabGroup with active focus. + @Published var activeTabGroup: TabGroupData { + didSet { + activeTabGroupHistory.prepend { [weak oldValue] in oldValue } + } + } + + /// History of last-used tab groups. + var activeTabGroupHistory: Deque<() -> TabGroupData?> = [] + + var fileDocuments: [WorkspaceClient.FileItem: CodeFileDocument] = [:] + + init() { + let tab = TabGroupData() + self.activeTabGroup = tab + self.activeTabGroupHistory.prepend { [weak tab] in tab } + self.tabGroups = .horizontal(.init(.horizontal, tabgroups: [.one(tab)])) + } + + /// Flattens the splitviews. + func flatten() { + if case .horizontal(let data) = tabGroups { + data.flatten() + } + } + + /// Opens a new tab in a tabgroup. + /// - 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) { + let tabgroup = tabgroup ?? activeTabGroup + tabgroup.openTab(item: item) + } +} diff --git a/CodeEdit/Features/Tabs/TabGroup/Environment+ActiveTabGroup.swift b/CodeEdit/Features/Tabs/TabGroup/Environment+ActiveTabGroup.swift new file mode 100644 index 0000000000..fa63f2e38f --- /dev/null +++ b/CodeEdit/Features/Tabs/TabGroup/Environment+ActiveTabGroup.swift @@ -0,0 +1,19 @@ +// +// Environment+ActiveTabGroup.swift +// CodeEdit +// +// Created by Wouter Hennen on 06/03/2023. +// + +import SwiftUI + +struct ActiveTabGroupEnvironmentKey: EnvironmentKey { + static var defaultValue = false +} + +extension EnvironmentValues { + var isActiveTabGroup: Bool { + get { self[ActiveTabGroupEnvironmentKey.self] } + set { self[ActiveTabGroupEnvironmentKey.self] = newValue } + } +} diff --git a/CodeEdit/Features/Tabs/TabGroup/TabGroup.swift b/CodeEdit/Features/Tabs/TabGroup/TabGroup.swift new file mode 100644 index 0000000000..1e005ec089 --- /dev/null +++ b/CodeEdit/Features/Tabs/TabGroup/TabGroup.swift @@ -0,0 +1,74 @@ +// +// TabGroup.swift +// CodeEdit +// +// Created by Wouter Hennen on 06/02/2023. +// + +import Foundation + +enum TabGroup { + case one(TabGroupData) + case vertical(SplitViewData) + case horizontal(SplitViewData) + + /// Closes all tabs which present the given file + /// - Parameter file: a file. + func closeAllTabs(of file: WorkspaceClient.FileItem) { + switch self { + case .one(let tabGroupData): + tabGroupData.tabs.remove(file) + case .vertical(let data), .horizontal(let data): + data.tabgroups.forEach { + $0.closeAllTabs(of: file) + } + } + } + + /// Returns some tabgroup, except the given tabgroup. + /// - Parameter except: the search will exclude this tabgroup. + /// - Returns: Some tabgroup. + func findSomeTabGroup(except: TabGroupData? = nil) -> TabGroupData? { + switch self { + case .one(let tabGroupData) where tabGroupData != except: + return tabGroupData + case .vertical(let data), .horizontal(let data): + for tabgroup in data.tabgroups { + if let result = tabgroup.findSomeTabGroup(except: except), result != except { + return result + } + } + return nil + default: + return nil + } + } + + /// Forms a set of all files currently represented by tabs. + func gatherOpenFiles() -> Set { + switch self { + case .one(let tabGroupData): + return Set(tabGroupData.tabs) + case .vertical(let data), .horizontal(let data): + return data.tabgroups.map { $0.gatherOpenFiles() }.reduce(into: []) { $0.formUnion($1) } + } + } + + /// Flattens the splitviews. + mutating func flatten(parent: SplitViewData) { + switch self { + case .one: + break + case .horizontal(let data), .vertical(let data): + if data.tabgroups.count == 1 { + let one = data.tabgroups[0] + if case .one(let tabGroupData) = one { + tabGroupData.parent = parent + } + self = one + } else { + data.flatten() + } + } + } +} diff --git a/CodeEdit/Features/Tabs/TabGroup/TabGroupData.swift b/CodeEdit/Features/Tabs/TabGroup/TabGroupData.swift new file mode 100644 index 0000000000..f4cebbdd83 --- /dev/null +++ b/CodeEdit/Features/Tabs/TabGroup/TabGroupData.swift @@ -0,0 +1,215 @@ +// +// TabGroupData.swift +// CodeEdit +// +// Created by Wouter Hennen on 16/02/2023. +// + +import Foundation +import OrderedCollections +import DequeModule + +final class TabGroupData: ObservableObject, Identifiable { + typealias Tab = WorkspaceClient.FileItem + + /// Set of open tabs. + @Published var tabs: OrderedSet = [] { + didSet { + let change = tabs.symmetricDifference(oldValue) + + if tabs.count > oldValue.count { + // Amount of tabs grew, so set the first new as selected. + selected = change.first + } else { + // Selected file was removed + if let selected, change.contains(selected) { + if let oldIndex = oldValue.firstIndex(of: selected), oldIndex - 1 < tabs.count, !tabs.isEmpty { + self.selected = tabs[max(0, oldIndex-1)] + } else { + self.selected = nil + } + } + } + } + } + + /// History of tab switching. + @Published var history: Deque = [] + + /// The current offset in the history list. + @Published var historyOffset: Int = 0 { + didSet { + let tab = history[historyOffset] + + if !tabs.contains(tab) { + if let selected { + openTab(item: tab, at: tabs.firstIndex(of: selected), fromHistory: true) + } else { + openTab(item: tab, fromHistory: true) + } + } + selected = tab + } + } + + /// Currently selected tab. + @Published var selected: Tab? + + @Published var temporaryTab: Tab? + + let id = UUID() + + weak var parent: SplitViewData? + + init( + files: OrderedSet = [], + selected: Tab? = nil, + parent: SplitViewData? = nil + ) { + self.tabs = [] + self.parent = parent + files.forEach { openTab(item: $0) } + self.selected = selected ?? files.first + } + + /// Closes the tabgroup. + func close() { + parent?.closeTabGroup(with: id) + } + + /// Closes a tab in the tabgroup. + /// This will also write any changes to the file on disk and will add the tab to the tab history. + /// - Parameter item: the tab to close. + func closeTab(item: Tab) { + historyOffset = 0 + if item != selected { + history.prepend(item) + } + tabs.remove(item) + if let selected { + history.prepend(selected) + } + + guard let file = item.fileDocument else { return } + + if file.isDocumentEdited { + let shouldClose = UnsafeMutablePointer.allocate(capacity: 1) + shouldClose.initialize(to: true) + defer { + _ = shouldClose.move() + shouldClose.deallocate() + } + file.canClose( + withDelegate: self, + shouldClose: #selector(WorkspaceDocument.document(_:shouldClose:contextInfo:)), + contextInfo: shouldClose + ) + guard shouldClose.pointee else { + return + } + } + + // TODO: Fix state +// if openedTabsFromState { +// var openTabsInState = self.getFromWorkspaceState(key: openTabsStateName) as? [String] ?? [] +// if let index = openTabsInState.firstIndex(of: item.url.absoluteString) { +// openTabsInState.remove(at: index) +// self.addToWorkspaceState(key: openTabsStateName, value: openTabsInState) +// } +// } + } + + /// Opens a tab in the tabgroup. + /// If a tab for the item already exists, it is used instead. + /// - Parameters: + /// - item: the tab to open. + /// - asTemporary: indicates whether the tab should be opened as a temporary tab or a permanent tab. + func openTab(item: Tab, asTemporary: Bool) { + // Item is already opened in a tab. + guard !tabs.contains(item) || !asTemporary else { + selected = item + history.prepend(item) + return + } + + switch (temporaryTab, asTemporary) { + case (.some(let tab), true): + if let index = tabs.firstIndex(of: tab) { + history.prepend(item) + tabs.remove(tab) + tabs.insert(item, at: index) + self.selected = item + temporaryTab = item + } + + case (.some(let tab), false) where tab == item: + temporaryTab = nil + + case (.none, true): + openTab(item: item) + temporaryTab = item + + case (.none, false): + openTab(item: item) + + default: + break + } + + do { + try openFile(item: item) + } catch { + print(error) + } + } + + /// Opens a tab in the tabgroup. + /// - Parameters: + /// - item: The tab to open. + /// - index: Index where the tab needs to be added. If nil, it is added to the back. + /// - fromHistory: Indicates whether the tab has been opened from going back in history. + func openTab(item: Tab, at index: Int? = nil, fromHistory: Bool = false) { + if let index { + tabs.insert(item, at: index) + } else { + tabs.append(item) + } + selected = item + if !fromHistory { + history.removeFirst(historyOffset) + history.prepend(item) + historyOffset = 0 + } + do { + try openFile(item: item) + } catch { + print(error) + } + } + + private func openFile(item: Tab) throws { + guard item.fileDocument == nil else { + return + } + + let contentType = try item.url.resourceValues(forKeys: [.contentTypeKey]).contentType + let codeFile = try CodeFileDocument( + for: item.url, + withContentsOf: item.url, + ofType: contentType?.identifier ?? "" + ) + item.fileDocument = codeFile + CodeEditDocumentController.shared.addDocument(codeFile) + print("Opening file for item: ", item.url) + } +} + +extension TabGroupData: Equatable, Hashable { + static func == (lhs: TabGroupData, rhs: TabGroupData) -> Bool { + lhs.id == rhs.id + } + + func hash(into hasher: inout Hasher) { + hasher.combine(id) + } +} diff --git a/CodeEdit/Features/Tabs/TabGroup/WorkspaceTabGroupView.swift b/CodeEdit/Features/Tabs/TabGroup/WorkspaceTabGroupView.swift new file mode 100644 index 0000000000..c97c04c83d --- /dev/null +++ b/CodeEdit/Features/Tabs/TabGroup/WorkspaceTabGroupView.swift @@ -0,0 +1,71 @@ +// +// WorkspaceTabGroupView.swift +// CodeEdit +// +// Created by Wouter Hennen on 16/02/2023. +// + +import SwiftUI + +struct WorkspaceTabGroupView: View { + @ObservedObject + var tabgroup: TabGroupData + + @FocusState.Binding + var focus: TabGroupData? + + @EnvironmentObject + private var tabManager: TabManager + + var body: some View { + VStack { + if let selected = tabgroup.selected { + WorkspaceCodeFileView(file: selected) + .transformEnvironment(\.edgeInsets) { insets in + insets.top += TabBarView.height + BreadcrumbsView.height + 1 + 1 + } + } else { + VStack { + Spacer() + Text("No Editor") + .font(.system(size: 17)) + .foregroundColor(.secondary) + .frame(minHeight: 0) + .clipped() + Spacer() + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .contentShape(Rectangle()) + .onTapGesture { + tabManager.activeTabGroup = tabgroup + } + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .ignoresSafeArea(.all) + .safeAreaInset(edge: .top, spacing: 0) { + VStack(spacing: 0) { + TabBarView() + .id("TabBarView" + tabgroup.id.uuidString) + .environmentObject(tabgroup) + + Divider() + if let file = tabgroup.selected { + BreadcrumbsView(file: file) { newFile in + let index = tabgroup.tabs.firstIndex(of: file) + if let index { + tabgroup.openTab(item: newFile, at: index) + } + } + Divider() + } + } + .environment(\.isActiveTabGroup, tabgroup == tabManager.activeTabGroup) + .background(EffectView(.titlebar, blendingMode: .withinWindow, emphasized: false)) + } + .focused($focus, equals: tabgroup) + .onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("CodeEditor.didBeginEditing"))) { _ in + tabgroup.temporaryTab = nil + } + } +} diff --git a/CodeEdit/Features/TabBar/Views/TabBarAccessory.swift b/CodeEdit/Features/Tabs/Views/TabBarAccessory.swift similarity index 95% rename from CodeEdit/Features/TabBar/Views/TabBarAccessory.swift rename to CodeEdit/Features/Tabs/Views/TabBarAccessory.swift index 74f2c17eb0..d981e44749 100644 --- a/CodeEdit/Features/TabBar/Views/TabBarAccessory.swift +++ b/CodeEdit/Features/Tabs/Views/TabBarAccessory.swift @@ -10,7 +10,7 @@ import SwiftUI /// Accessory icon's view for tab bar. struct TabBarAccessoryIcon: View { /// Unifies icon font for tab bar accessories. - private static let iconFont = Font.system(size: 14, weight: .regular, design: .default) + static let iconFont = Font.system(size: 14, weight: .regular, design: .default) private let icon: Image private let action: () -> Void diff --git a/CodeEdit/Features/TabBar/Views/TabBarContextMenu.swift b/CodeEdit/Features/Tabs/Views/TabBarContextMenu.swift similarity index 58% rename from CodeEdit/Features/TabBar/Views/TabBarContextMenu.swift rename to CodeEdit/Features/Tabs/Views/TabBarContextMenu.swift index 7a3adc59ba..f1ff9d074c 100644 --- a/CodeEdit/Features/TabBar/Views/TabBarContextMenu.swift +++ b/CodeEdit/Features/Tabs/Views/TabBarContextMenu.swift @@ -9,14 +9,14 @@ import Foundation import SwiftUI extension View { - func tabBarContextMenu(item: TabBarItemRepresentable, isTemporary: Bool) -> some View { + func tabBarContextMenu(item: WorkspaceClient.FileItem, isTemporary: Bool) -> some View { modifier(TabBarContextMenu(item: item, isTemporary: isTemporary)) } } struct TabBarContextMenu: ViewModifier { init( - item: TabBarItemRepresentable, + item: WorkspaceClient.FileItem, isTemporary: Bool ) { self.item = item @@ -26,7 +26,12 @@ struct TabBarContextMenu: ViewModifier { @EnvironmentObject var workspace: WorkspaceDocument - private var item: TabBarItemRepresentable + @EnvironmentObject + var tabs: TabGroupData + + @Environment(\.splitEditor) var splitEditor + + private var item: WorkspaceClient.FileItem private var isTemporary: Bool // swiftlint:disable:next function_body_length @@ -35,66 +40,89 @@ struct TabBarContextMenu: ViewModifier { Group { Button("Close Tab") { withAnimation { - workspace.closeTab(item: item.tabID) + tabs.closeTab(item: item) } } .keyboardShortcut("w", modifiers: [.command]) Button("Close Other Tabs") { withAnimation { - workspace.closeTab(where: { $0 != item.tabID }) + tabs.tabs.forEach { file in + if file != item { + tabs.closeTab(item: file) + } + } } } Button("Close Tabs to the Right") { withAnimation { - workspace.closeTabs(after: item.tabID) + if let index = tabs.tabs.firstIndex(of: item) { + tabs.tabs[index...].forEach { + tabs.closeTab(item: $0) + } + } } } // Disable this option when current tab is the last one. - .disabled(workspace.selectionState.openedTabs.last?.id == item.tabID.id) + .disabled(tabs.tabs.last == item) Button("Close All") { withAnimation { - workspace.closeTabs(items: workspace.selectionState.openedTabs) + tabs.tabs.forEach { + tabs.closeTab(item: $0) + } } } if isTemporary { Button("Keep Open") { - workspace.convertTemporaryTab() + tabs.temporaryTab = nil } } } Divider() - if let item = item as? WorkspaceClient.FileItem { - Group { - Button("Copy Path") { - copyPath(item: item) - } + Group { + Button("Copy Path") { + copyPath(item: item) + } - Button("Copy Relative Path") { - copyRelativePath(item: item) - } + Button("Copy Relative Path") { + copyRelativePath(item: item) } + } - Divider() + Divider() - Group { - Button("Show in Finder") { - item.showInFinder() - } + Group { + Button("Show in Finder") { + item.showInFinder() + } - Button("Reveal in Project Navigator") { - workspace.listenerModel.highlightedFileItem = item - } + Button("Reveal in Project Navigator") { + workspace.listenerModel.highlightedFileItem = item + } - Button("Open in New Window") { + Button("Open in New Window") { - } - .disabled(true) } + .disabled(true) + } + + Divider() + + Button("Split Up") { + moveToNewSplit(.top) + } + Button("Split Down") { + moveToNewSplit(.bottom) + } + Button("Split Left") { + moveToNewSplit(.leading) + } + Button("Split Right") { + moveToNewSplit(.trailing) } }) } @@ -108,6 +136,13 @@ struct TabBarContextMenu: ViewModifier { NSPasteboard.general.setString(item.url.standardizedFileURL.path, forType: .string) } + func moveToNewSplit(_ edge: Edge) { + let newTabGroup = TabGroupData(files: [item]) + splitEditor(edge, newTabGroup) + tabs.closeTab(item: item) + workspace.tabManager.activeTabGroup = newTabGroup + } + /// 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) { diff --git a/CodeEdit/Features/TabBar/Views/TabBarDivider.swift b/CodeEdit/Features/Tabs/Views/TabBarDivider.swift similarity index 100% rename from CodeEdit/Features/TabBar/Views/TabBarDivider.swift rename to CodeEdit/Features/Tabs/Views/TabBarDivider.swift diff --git a/CodeEdit/Features/TabBar/Views/TabBarItemBackground.swift b/CodeEdit/Features/Tabs/Views/TabBarItemBackground.swift similarity index 93% rename from CodeEdit/Features/TabBar/Views/TabBarItemBackground.swift rename to CodeEdit/Features/Tabs/Views/TabBarItemBackground.swift index 4616914e89..143afde2e5 100644 --- a/CodeEdit/Features/TabBar/Views/TabBarItemBackground.swift +++ b/CodeEdit/Features/Tabs/Views/TabBarItemBackground.swift @@ -18,6 +18,9 @@ struct TabBarItemBackground: View { @Environment(\.controlActiveState) private var activeState + @Environment(\.isActiveTabGroup) + private var isActiveTabGroup + private var inHoldingState: Bool { isPressing || isDragging } @@ -38,6 +41,7 @@ struct TabBarItemBackground: View { : activeState == .inactive ? 0.1 : inHoldingState ? 0.27 : 0.2 : 0 ) + .saturation(isActiveTabGroup ? 1.0 : 0.0) // Highlight (if in dark mode) Color(.white) diff --git a/CodeEdit/Features/TabBar/Views/TabBarItemButtonStyle.swift b/CodeEdit/Features/Tabs/Views/TabBarItemButtonStyle.swift similarity index 100% rename from CodeEdit/Features/TabBar/Views/TabBarItemButtonStyle.swift rename to CodeEdit/Features/Tabs/Views/TabBarItemButtonStyle.swift diff --git a/CodeEdit/Features/TabBar/Views/TabBarItemCloseButton.swift b/CodeEdit/Features/Tabs/Views/TabBarItemCloseButton.swift similarity index 100% rename from CodeEdit/Features/TabBar/Views/TabBarItemCloseButton.swift rename to CodeEdit/Features/Tabs/Views/TabBarItemCloseButton.swift diff --git a/CodeEdit/Features/TabBar/Views/TabBarItemView.swift b/CodeEdit/Features/Tabs/Views/TabBarItemView.swift similarity index 78% rename from CodeEdit/Features/TabBar/Views/TabBarItemView.swift rename to CodeEdit/Features/Tabs/Views/TabBarItemView.swift index 258ba3d7da..30bdc1bca7 100644 --- a/CodeEdit/Features/TabBar/Views/TabBarItemView.swift +++ b/CodeEdit/Features/Tabs/Views/TabBarItemView.swift @@ -8,15 +8,24 @@ import SwiftUI struct TabBarItemView: View { + + typealias Item = WorkspaceClient.FileItem + @Environment(\.colorScheme) private var colorScheme @Environment(\.controlActiveState) private var activeState + @Environment(\.isActiveTabGroup) + private var isActiveTabGroup + @Environment(\.isFullscreen) private var isFullscreen + @EnvironmentObject + private var tabManager: TabManager + /// User preferences. @StateObject private var prefs: AppPreferencesModel = .shared @@ -44,44 +53,40 @@ struct TabBarItemView: View { private var isAppeared: Bool = false /// The expected tab width in native tab bar style. - @Binding private var expectedWidth: CGFloat /// The id associating with the tab that is currently being dragged. /// /// When `nil`, then there is no tab being dragged. - @Binding - private var draggingTabId: TabBarItemID? + private var draggingTabId: Item.ID? - @Binding - private var onDragTabId: TabBarItemID? + private var onDragTabId: Item.ID? @Binding private var closeButtonGestureActive: Bool - /// The current WorkspaceDocument object. - /// - /// It contains the workspace-related information like selection states. @EnvironmentObject - private var workspace: WorkspaceDocument + private var tabgroup: TabGroupData /// The item associated with the current tab. /// /// You can get tab-related information from here, like `label`, `icon`, etc. - private var item: TabBarItemRepresentable + private var item: Item + + var index: Int private var isTemporary: Bool { - workspace.selectionState.temporaryTab == item.tabID + tabgroup.temporaryTab == item } /// Is the current tab the active tab. private var isActive: Bool { - item.tabID == workspace.selectionState.selectedId + item == tabgroup.selected } /// Is the current tab being dragged. private var isDragging: Bool { - draggingTabId == item.tabID + draggingTabId == item.id } /// Is the current tab being held (by click and hold, not drag). @@ -94,8 +99,12 @@ struct TabBarItemView: View { /// Switch the active tab to current tab. private func switchAction() { // Only set the `selectedId` when they are not equal to avoid performance issue for now. - if workspace.selectionState.selectedId != item.tabID { - workspace.switchedTab(item: item) + tabManager.activeTabGroup = tabgroup + if tabgroup.selected != item { + tabgroup.selected = item + tabgroup.history.removeFirst(tabgroup.historyOffset) + tabgroup.history.prepend(item) + tabgroup.historyOffset = 0 } } @@ -105,21 +114,23 @@ struct TabBarItemView: View { withAnimation( .easeOut(duration: prefs.preferences.general.tabBarStyle == .native ? 0.15 : 0.20) ) { - workspace.closeTab(item: item.tabID) + tabgroup.closeTab(item: item) } } init( - expectedWidth: Binding, - item: TabBarItemRepresentable, - draggingTabId: Binding, - onDragTabId: Binding, + expectedWidth: CGFloat, + item: Item, + index: Int, + draggingTabId: Item.ID?, + onDragTabId: Item.ID?, closeButtonGestureActive: Binding ) { - self._expectedWidth = expectedWidth + self.expectedWidth = expectedWidth self.item = item - self._draggingTabId = draggingTabId - self._onDragTabId = onDragTabId + self.index = index + self.draggingTabId = draggingTabId + self.onDragTabId = onDragTabId self._closeButtonGestureActive = closeButtonGestureActive } @@ -134,16 +145,17 @@ struct TabBarItemView: View { .padding(.top, isActive && prefs.preferences.general.tabBarStyle == .native ? 1.22 : 0) // Tab content (icon and text). HStack(alignment: .center, spacing: 5) { - item.icon + Image(systemName: item.systemImage) .resizable() .aspectRatio(contentMode: .fit) .foregroundColor( - prefs.preferences.general.fileIconStyle == .color && activeState != .inactive + prefs.preferences.general.fileIconStyle == .color + && activeState != .inactive && isActiveTabGroup ? item.iconColor : .secondary ) .frame(width: 12, height: 12) - Text(item.title) + Text(item.fileName) .font( isTemporary ? .system(size: 11.0).italic() @@ -177,16 +189,18 @@ struct TabBarItemView: View { // Using an invisible button to contain the keyboard shortcut is simply // because the keyboard shortcut has an unexpected bug when working with // custom buttonStyle. This is an workaround and it works as expected. - Button( - action: switchAction, - label: { EmptyView() } - ) - .frame(width: 0, height: 0) - .keyboardShortcut( - workspace.getTabKeyEquivalent(item: item), - modifiers: [.command] - ) - .hidden() + if index < 10 { + Button( + action: switchAction, + label: { EmptyView() } + ) + .frame(width: 0, height: 0) + .keyboardShortcut( + KeyEquivalent(Character(String(index))), + modifiers: [.command] + ) + .hidden() + } // Close Button TabBarItemCloseButton( isActive: isActive, @@ -221,7 +235,7 @@ struct TabBarItemView: View { .opacity(prefs.preferences.general.tabBarStyle == .native && !isActive ? 1 : 0) } .foregroundColor( - isActive + isActive && isActiveTabGroup ? ( prefs.preferences.general.tabBarStyle == .xcode && colorScheme != .dark ? Color(nsColor: .controlAccentColor) @@ -255,7 +269,7 @@ struct TabBarItemView: View { .background { if prefs.preferences.general.tabBarStyle == .xcode { TabBarItemBackground(isActive: isActive, isPressing: isPressing, isDragging: isDragging) - .animation(.easeInOut(duration: 0.08), value: isHovering) + .animation(.easeInOut(duration: 0.08), value: isHovering) } else { if isFullscreen && isActive { TabBarNativeActiveMaterial() @@ -286,7 +300,7 @@ struct TabBarItemView: View { TapGesture(count: 2) .onEnded { _ in if isTemporary { - workspace.convertTemporaryTab() + tabgroup.temporaryTab = nil } } ) @@ -294,11 +308,11 @@ struct TabBarItemView: View { // This padding is to avoid background color overlapping with top divider. .top, prefs.preferences.general.tabBarStyle == .xcode ? 1 : 0 ) - .offset( - x: isAppeared || prefs.preferences.general.tabBarStyle == .native ? 0 : -14, - y: 0 - ) - .opacity(isAppeared && onDragTabId != item.tabID ? 1.0 : 0.0) +// .offset( +// x: isAppeared || prefs.preferences.general.tabBarStyle == .native ? 0 : -14, +// y: 0 +// ) +// .opacity(isAppeared && onDragTabId != item.id ? 1.0 : 0.0) .zIndex( isActive ? (prefs.preferences.general.tabBarStyle == .native ? -1 : 2) @@ -313,31 +327,13 @@ struct TabBarItemView: View { ) ) .onAppear { - if (isTemporary && workspace.selectionState.previousTemporaryTab == nil) - || !(isTemporary && workspace.selectionState.previousTemporaryTab != item.tabID) { - withAnimation( - .easeOut(duration: prefs.preferences.general.tabBarStyle == .native ? 0.15 : 0.20) - ) { - isAppeared = true - } - } else { - withAnimation(.linear(duration: 0.0)) { - isAppeared = true - } + withAnimation( + .easeOut(duration: prefs.preferences.general.tabBarStyle == .native ? 0.15 : 0.20) + ) { +// isAppeared = true } } - .id(item.tabID) + .id(item.id) .tabBarContextMenu(item: item, isTemporary: isTemporary) } } -// swiftlint:enable type_body_length - -fileprivate extension WorkspaceDocument { - func getTabKeyEquivalent(item: TabBarItemRepresentable) -> KeyEquivalent { - for counter in 0..<9 where self.selectionState.openFileItems.count > counter && - self.selectionState.openFileItems[counter].tabID == item.tabID { - return KeyEquivalent.init(Character.init("\(counter + 1)")) - } - return "0" - } -} diff --git a/CodeEdit/Features/TabBar/Views/TabBarNative.swift b/CodeEdit/Features/Tabs/Views/TabBarNative.swift similarity index 100% rename from CodeEdit/Features/TabBar/Views/TabBarNative.swift rename to CodeEdit/Features/Tabs/Views/TabBarNative.swift diff --git a/CodeEdit/Features/TabBar/Views/TabBarView.swift b/CodeEdit/Features/Tabs/Views/TabBarView.swift similarity index 69% rename from CodeEdit/Features/TabBar/Views/TabBarView.swift rename to CodeEdit/Features/Tabs/Views/TabBarView.swift index 5e91fa1450..e35b74c6a8 100644 --- a/CodeEdit/Features/TabBar/Views/TabBarView.swift +++ b/CodeEdit/Features/Tabs/Views/TabBarView.swift @@ -13,6 +13,11 @@ import SwiftUI // swiftlint:disable file_length type_body_length // - TODO: TabBarItemView drop-outside event handler. struct TabBarView: View { + + @Environment(\.modifierKeys) var modifierKeys + + typealias TabID = WorkspaceClient.FileItem.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 @@ -27,6 +32,14 @@ struct TabBarView: View { @EnvironmentObject private var workspace: WorkspaceDocument + @EnvironmentObject + private var tabManager: TabManager + + @EnvironmentObject + private var tabgroup: TabGroupData + + @Environment(\.splitEditor) var splitEditor + /// The app preference. @StateObject private var prefs: AppPreferencesModel = .shared @@ -35,10 +48,10 @@ struct TabBarView: View { /// /// It will be `nil` when there is no tab dragged currently. @State - private var draggingTabId: TabBarItemID? + private var draggingTabId: TabID? @State - private var onDragTabId: TabBarItemID? + private var onDragTabId: TabID? /// The start location of dragging. /// @@ -60,7 +73,7 @@ struct TabBarView: View { /// I am making a copy of it because using state will hugely improve the dragging performance. /// Updating ObservedObject too often will generate lags. @State - private var openedTabs: [TabBarItemID] = [] + private var openedTabs: [TabID] = [] /// A map of tab width. /// @@ -68,20 +81,20 @@ struct TabBarView: View { /// This is used to be added on the offset of current dragging tab in order to make a smooth /// dragging experience. @State - private var tabWidth: [TabBarItemID: CGFloat] = [:] + private var tabWidth: [TabID: CGFloat] = [:] /// A map of tab location (CGRect). /// /// All locations are measured dynamically. /// This is used to compute when we should swap two tabs based on current cursor location. @State - private var tabLocations: [TabBarItemID: CGRect] = [:] + private var tabLocations: [TabID: CGRect] = [:] /// A map of tab offsets. /// /// This is used to determine the tab offset of every tab (by their tab id) while dragging. @State - private var tabOffsets: [TabBarItemID: CGFloat] = [:] + private var tabOffsets: [TabID: CGFloat] = [:] /// The expected tab width in native tab bar style. /// @@ -123,7 +136,7 @@ struct TabBarView: View { private func updateExpectedTabWidth(proxy: GeometryProxy) { expectedTabWidth = max( // Equally divided size of a native tab. - (proxy.size.width + 1) / CGFloat(workspace.selectionState.openedTabs.count) + 1, + (proxy.size.width + 1) / CGFloat(tabgroup.tabs.count) + 1, // Min size of a native tab. CGFloat(140) ) @@ -132,7 +145,7 @@ struct TabBarView: View { // Disable the rule because this function is implementing the drag gesture and its animations. // It is fairly complicated, so ignore the function body length limitation for now. // swiftlint:disable function_body_length cyclomatic_complexity - private func makeTabDragGesture(id: TabBarItemID) -> some Gesture { + private func makeTabDragGesture(id: TabID) -> some Gesture { return DragGesture(minimumDistance: 2, coordinateSpace: .global) .onChanged({ value in if closeButtonGestureActive { @@ -237,14 +250,18 @@ struct TabBarView: View { // In order to avoid the lag due to the update of workspace state. DispatchQueue.main.asyncAfter(deadline: .now() + 0.40) { if draggingStartLocation == nil { - workspace.reorderedTabs(openedTabs: openedTabs) + tabgroup.tabs = .init(openedTabs.compactMap { id in + tabgroup.tabs.first { $0.id == id } + }) + // workspace.reorderedTabs(openedTabs: openedTabs) + // TODO: Fix save state } } }) } // swiftlint:enable function_body_length cyclomatic_complexity - private func makeTabItemGeometryReader(id: TabBarItemID) -> some View { + private func makeTabItemGeometryReader(id: TabID) -> some View { GeometryReader { tabItemGeoReader in Rectangle() .foregroundColor(.clear) @@ -272,7 +289,7 @@ struct TabBarView: View { /// Called when the tab count changes or the temporary tab changes. /// - Parameter geometryProxy: The geometry proxy to calculate the new width using. private func updateForTabCountChange(geometryProxy: GeometryProxy) { - openedTabs = workspace.selectionState.openedTabs + openedTabs = tabgroup.tabs.map(\.id) // Only update the expected width when user is not hovering over tabs. // This should give users a better experience on closing multiple tabs continuously. @@ -295,15 +312,22 @@ struct TabBarView: View { alignment: .center, spacing: -1 // Negative spacing for overlapping the divider. ) { - ForEach(openedTabs, id: \.id) { id in - if let item = workspace.selectionState.getItemByTab(id: id) { + ForEach(Array(openedTabs.enumerated()), id: \.element) { index, id in + if let item = tabgroup.tabs.first(where: { $0.id == id }) { TabBarItemView( - expectedWidth: $expectedTabWidth, + expectedWidth: expectedTabWidth, item: item, - draggingTabId: $draggingTabId, - onDragTabId: $onDragTabId, + index: index, + draggingTabId: draggingTabId, + onDragTabId: onDragTabId, closeButtonGestureActive: $closeButtonGestureActive ) + .transition( + .asymmetric( + insertion: .offset(x: -14).combined(with: .opacity), + removal: .opacity + ) + ) .frame(height: TabBarView.height) .background(makeTabItemGeometryReader(id: id)) .offset(x: tabOffsets[id] ?? 0, y: 0) @@ -330,37 +354,42 @@ struct TabBarView: View { // This padding is to hide dividers at two ends under the accessory view divider. .padding(.horizontal, prefs.preferences.general.tabBarStyle == .native ? -1 : 0) .onAppear { - openedTabs = workspace.selectionState.openedTabs + openedTabs = tabgroup.tabs.map(\.id) // On view appeared, compute the initial expected width for tabs. updateExpectedTabWidth(proxy: geometryProxy) // On first tab appeared, jump to the corresponding position. - scrollReader.scrollTo(workspace.selectionState.selectedId) + scrollReader.scrollTo(tabgroup.selected) } - .onChange(of: workspace.selectionState.openedTabs) { _ in - DispatchQueue.main.asyncAfter( - deadline: .now() + .milliseconds( - prefs.preferences.general.tabBarStyle == .native ? 150 : 200 - ) - ) { - guard let selectedID = workspace.selectionState.selectedId else { return } - scrollReader.scrollTo(selectedID) + .onChange(of: tabgroup.tabs) { [tabs = tabgroup.tabs] newValue in + if tabs.count == newValue.count { + updateForTabCountChange(geometryProxy: geometryProxy) + } else { + withAnimation( + .easeOut(duration: prefs.preferences.general.tabBarStyle == .native ? 0.15 : 0.20) + ) { + updateForTabCountChange(geometryProxy: geometryProxy) + } + } + Task { + try? await Task.sleep(for: .milliseconds(300)) + withAnimation { + scrollReader.scrollTo(tabgroup.selected?.id) + } } } // When selected tab is changed, scroll to it if possible. - .onChange(of: workspace.selectionState.selectedId) { targetId in - guard let selectedId = targetId else { return } - scrollReader.scrollTo(selectedId) - } - // When tabs are changing, re-compute the expected tab width. - .onChange(of: workspace.selectionState.openedTabs.count) { _ in - updateForTabCountChange(geometryProxy: geometryProxy) + .onChange(of: tabgroup.selected) { newValue in + withAnimation { + scrollReader.scrollTo(newValue?.id) + } } - .onChange(of: workspace.selectionState.temporaryTab, perform: { _ in - updateForTabCountChange(geometryProxy: geometryProxy) - }) + // When window size changes, re-compute the expected tab width. .onChange(of: geometryProxy.size.width) { _ in updateExpectedTabWidth(proxy: geometryProxy) + withAnimation { + scrollReader.scrollTo(tabgroup.selected?.id) + } } // When user is not hovering anymore, re-compute the expected tab width immediately. .onHover { isHovering in @@ -373,18 +402,18 @@ struct TabBarView: View { } .frame(height: TabBarView.height) } + + // To fill up the parent space of tab bar. + .frame(maxWidth: .infinity) + .background { + if prefs.preferences.general.tabBarStyle == .native { + TabBarNativeInactiveBackground() + } + } } - // When there is no opened file, hide the scroll view, but keep the background. - .opacity( - workspace.selectionState.openedTabs.isEmpty && workspace.selectionState.temporaryTab == nil - ? 0.0 - : 1.0 - ) - // To fill up the parent space of tab bar. - .frame(maxWidth: .infinity) .background { if prefs.preferences.general.tabBarStyle == .native { - TabBarNativeInactiveBackground() + TabBarAccessoryNativeBackground(dividerAt: .none) } } } @@ -413,21 +442,89 @@ struct TabBarView: View { private var leadingAccessories: some View { HStack(spacing: 2) { - TabBarAccessoryIcon( - icon: .init(systemName: "chevron.left"), - action: {} // TODO: Implement - ) - .foregroundColor(.secondary) - .buttonStyle(.plain) - .help("Navigate back") - TabBarAccessoryIcon( - icon: .init(systemName: "chevron.right"), - action: {} // TODO: Implement - ) - .foregroundColor(.secondary) - .buttonStyle(.plain) - .help("Navigate forward") + if tabManager.tabGroups.findSomeTabGroup(except: tabgroup) != nil { + TabBarAccessoryIcon( + icon: .init(systemName: "multiply"), + action: { + tabgroup.close() + if tabManager.activeTabGroup == tabgroup { + tabManager.activeTabGroupHistory.removeAll { $0() == nil } + tabManager.activeTabGroup = tabManager.activeTabGroupHistory.removeFirst()()! + } + tabManager.flatten() + } + ) + .help("Close Tab Group") + + Divider() + .frame(height: 10) + .padding(.horizontal, 4) + } + + Group { + Menu { + ForEach( + Array(tabgroup.history.dropFirst(tabgroup.historyOffset+1).enumerated()), + id: \.offset + ) { index, tab in + Button { + tabManager.activeTabGroup = tabgroup + tabgroup.historyOffset += index + 1 + } label: { + HStack { + tab.icon + Text(tab.fileName) + } + } + } + } label: { + Image(systemName: "chevron.left") + .controlSize(.regular) + .opacity( + tabgroup.historyOffset == tabgroup.history.count-1 || tabgroup.history.isEmpty + ? 0.5 : 1.0 + ) + } primaryAction: { + tabManager.activeTabGroup = tabgroup + tabgroup.historyOffset += 1 + } + .disabled(tabgroup.historyOffset == tabgroup.history.count-1 || tabgroup.history.isEmpty) + .help("Navigate back") + + Menu { + ForEach( + Array(tabgroup.history.prefix(tabgroup.historyOffset).reversed().enumerated()), + id: \.offset + ) { index, tab in + Button { + tabManager.activeTabGroup = tabgroup + tabgroup.historyOffset -= index + 1 + } label: { + HStack { + tab.icon + Text(tab.fileName) + } + } + } + } label: { + Image(systemName: "chevron.right") + .controlSize(.regular) + .opacity(tabgroup.historyOffset == 0 ? 0.5 : 1.0) + } primaryAction: { + tabManager.activeTabGroup = tabgroup + tabgroup.historyOffset -= 1 + } + .disabled(tabgroup.historyOffset == 0) + .help("Navigate forward") + } + .controlSize(.small) + .font(TabBarAccessoryIcon.iconFont) + .frame(height: TabBarView.height - 2) + .padding(.horizontal, 4) + .contentShape(Rectangle()) } + .foregroundColor(.secondary) + .buttonStyle(.plain) .padding(.horizontal, 7) .opacity(activeState != .inactive ? 1.0 : 0.5) .frame(maxHeight: .infinity) // Fill out vertical spaces. @@ -454,13 +551,7 @@ struct TabBarView: View { .foregroundColor(.secondary) .buttonStyle(.plain) .help("Enable Code Review") - TabBarAccessoryIcon( - icon: .init(systemName: "square.split.2x1"), - action: {} // TODO: Implement - ) - .foregroundColor(.secondary) - .buttonStyle(.plain) - .help("Split View") + splitviewButton } .padding(.horizontal, 7) .opacity(activeState != .inactive ? 1.0 : 0.5) @@ -472,26 +563,57 @@ struct TabBarView: View { } } + var splitviewButton: some View { + Group { + switch (tabgroup.parent!.axis, modifierKeys.contains(.option)) { + case (.horizontal, true), (.vertical, false): + TabBarAccessoryIcon(icon: Image(systemName: "square.split.1x2")) { + split(edge: .bottom) + } + .help("Split Vertically") + + case (.vertical, true), (.horizontal, false): + TabBarAccessoryIcon(icon: Image(systemName: "square.split.2x1")) { + split(edge: .trailing) + } + .help("Split Horizontally") + } + } + .foregroundColor(.secondary) + .buttonStyle(.plain) + } + + func split(edge: Edge) { + let newTabgroup: TabGroupData + if let tab = tabgroup.selected { + newTabgroup = .init(files: [tab]) + } else { + newTabgroup = .init() + } + splitEditor(edge, newTabgroup) + tabManager.activeTabGroup = newTabgroup + } + private struct TabBarItemOnDropDelegate: DropDelegate { - private let currentTabId: TabBarItemID + private let currentTabId: TabID @Binding - private var openedTabs: [TabBarItemID] + private var openedTabs: [TabID] @Binding - private var onDragTabId: TabBarItemID? + private var onDragTabId: TabID? @Binding private var onDragLastLocation: CGPoint? @Binding private var isOnDragOverTabs: Bool @Binding - private var tabWidth: [TabBarItemID: CGFloat] + private var tabWidth: [TabID: CGFloat] public init( - currentTabId: TabBarItemID, - openedTabs: Binding<[TabBarItemID]>, - onDragTabId: Binding, + currentTabId: TabID, + openedTabs: Binding<[TabID]>, + onDragTabId: Binding, onDragLastLocation: Binding, isOnDragOverTabs: Binding, - tabWidth: Binding<[TabBarItemID: CGFloat]> + tabWidth: Binding<[TabID: CGFloat]> ) { self.currentTabId = currentTabId self._openedTabs = openedTabs diff --git a/CodeEdit/Features/TabBar/Views/TabBarXcode.swift b/CodeEdit/Features/Tabs/Views/TabBarXcode.swift similarity index 100% rename from CodeEdit/Features/TabBar/Views/TabBarXcode.swift rename to CodeEdit/Features/Tabs/Views/TabBarXcode.swift diff --git a/CodeEdit/Utils/WorkspaceClient/Model/FileItem.swift b/CodeEdit/Utils/WorkspaceClient/Model/FileItem.swift index 30e94993a0..7ad7f7e742 100644 --- a/CodeEdit/Utils/WorkspaceClient/Model/FileItem.swift +++ b/CodeEdit/Utils/WorkspaceClient/Model/FileItem.swift @@ -168,6 +168,8 @@ extension WorkspaceClient { FileIcon.iconColor(fileType: fileType) } + var fileDocument: CodeFileDocument? + // MARK: Statics /// The default `FileManager` instance @@ -214,6 +216,7 @@ extension WorkspaceClient { /// 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 ? diff --git a/CodeEdit/WindowObserver.swift b/CodeEdit/WindowObserver.swift index 3f14df1c81..7f7f5e6d12 100644 --- a/CodeEdit/WindowObserver.swift +++ b/CodeEdit/WindowObserver.swift @@ -22,8 +22,14 @@ struct WindowObserver: View { @StateObject private var prefs: AppPreferencesModel = .shared + @State var modifierFlags: NSEvent.ModifierFlags = [] + var body: some View { content + .environment(\.modifierKeys, modifierFlags.intersection(.deviceIndependentFlagsMask)) + .onReceive(NSEvent.publisher(scope: .local, matching: .flagsChanged)) { output in + modifierFlags = output.modifierFlags + } .environment(\.window, window) .environment(\.isFullscreen, isFullscreen) .onReceive(NotificationCenter.default.publisher(for: NSWindow.didEnterFullScreenNotification)) { _ in @@ -36,10 +42,8 @@ struct WindowObserver: View { .onChange(of: prefs.preferences.general.tabBarStyle) { newStyle in DispatchQueue.main.async { if newStyle == .native { - window.titlebarAppearsTransparent = true window.titlebarSeparatorStyle = .none } else { - window.titlebarAppearsTransparent = false window.titlebarSeparatorStyle = .automatic } } diff --git a/CodeEdit/WorkspaceView.swift b/CodeEdit/WorkspaceView.swift index 06d938fd1f..975dfd304c 100644 --- a/CodeEdit/WorkspaceView.swift +++ b/CodeEdit/WorkspaceView.swift @@ -9,15 +9,15 @@ import SwiftUI import AppKit struct WorkspaceView: View { - init(workspace: WorkspaceDocument) { - self.workspace = workspace - } let tabBarHeight = 28.0 private var path: String = "" - @ObservedObject - var workspace: WorkspaceDocument + @EnvironmentObject + private var workspace: WorkspaceDocument + + @EnvironmentObject + private var tabManager: TabManager @StateObject private var prefs: AppPreferencesModel = .shared @@ -30,74 +30,48 @@ struct WorkspaceView: View { @State private var showingAlert = false - @State - private var alertTitle = "" + @Environment(\.colorScheme) + private var colorScheme @State - private var alertMsg = "" + private var terminalCollapsed = true - @State - var showInspector = true + @FocusState + var focusedEditor: TabGroupData? - @Environment(\.colorScheme) var colorScheme + var body: some View { + if workspace.workspaceClient != nil { + VStack { + SplitViewReader { proxy in + SplitView(axis: .vertical) { - var noEditor: some View { - Text("No Editor") - .font(.system(size: 17)) - .foregroundColor(.secondary) - .frame(minHeight: 0) - .clipped() - } + EditorView(tabgroup: tabManager.tabGroups, focus: $focusedEditor) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .safeAreaInset(edge: .bottom, spacing: 0) { + StatusBarView(proxy: proxy, collapsed: $terminalCollapsed) + } - @ViewBuilder var tabContent: some View { - if let tabID = workspace.selectionState.selectedId { - switch tabID { - case .codeEditor: - WorkspaceCodeFileView() - case .extensionInstallation: - if let plugin = workspace.selectionState.selected as? Plugin { - ExtensionInstallationView(plugin: plugin) - .frame(alignment: .center) - } - } - } else { - noEditor - } - } + StatusBarDrawer() + .collapsable() + .collapsed($terminalCollapsed) + .frame(minHeight: 200, maxHeight: 400) - var body: some View { - ZStack { - if workspace.workspaceClient != nil, let model = workspace.statusBarModel { - ZStack { - tabContent - .animation(nil, value: UUID()) - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - .safeAreaInset(edge: .top, spacing: 0) { - VStack(spacing: 0) { - TabBarView() - TabBarBottomDivider() + } + + .edgesIgnoringSafeArea(.top) + .environmentObject(workspace.statusBarModel) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .onChange(of: focusedEditor) { newValue in + if let newValue { + tabManager.activeTabGroup = newValue + } + } + .onChange(of: tabManager.activeTabGroup) { newValue in + if newValue != focusedEditor { + focusedEditor = newValue + } } } - .safeAreaInset(edge: .bottom) { - StatusBarView() - .environmentObject(model) - } - } else { - EmptyView() - } - } - .environmentObject(workspace) - .background(EffectView(.contentBackground)) - .alert(alertTitle, isPresented: $showingAlert, actions: { - Button( - action: { showingAlert = false }, - label: { Text("OK") } - ) - }, message: { Text(alertMsg) }) - .onChange(of: workspace.selectionState.selectedId) { newValue in - if newValue == nil { - window.subtitle = "" } } }