diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index da9542be7..230c7e82c 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -152,7 +152,6 @@ 58798284292ED0FB0085B254 /* TerminalEmulatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58798280292ED0FB0085B254 /* TerminalEmulatorView.swift */; }; 58798285292ED0FB0085B254 /* TerminalEmulatorView+Coordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58798281292ED0FB0085B254 /* TerminalEmulatorView+Coordinator.swift */; }; 58798286292ED0FB0085B254 /* SwiftTerm+Color+Init.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58798283292ED0FB0085B254 /* SwiftTerm+Color+Init.swift */; }; - 5879828A292ED15F0085B254 /* SwiftTerm in Frameworks */ = {isa = PBXBuildFile; productRef = 58798289292ED15F0085B254 /* SwiftTerm */; }; 587B60F82934124200D5CD8F /* CEWorkspaceFileManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587B60F72934124100D5CD8F /* CEWorkspaceFileManagerTests.swift */; }; 587B61012934170A00D5CD8F /* UnitTests_Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587B61002934170A00D5CD8F /* UnitTests_Extensions.swift */; }; 587B612E293419B700D5CD8F /* CodeFileTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587B612D293419B700D5CD8F /* CodeFileTests.swift */; }; @@ -358,6 +357,9 @@ 6C049A372A49E2DB00D42923 /* DirectoryEventStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C049A362A49E2DB00D42923 /* DirectoryEventStream.swift */; }; 6C05A8AF284D0CA3007F4EAA /* WorkspaceDocument+Listeners.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C05A8AE284D0CA3007F4EAA /* WorkspaceDocument+Listeners.swift */; }; 6C0617D62BDB4432008C9C42 /* LogStream in Frameworks */ = {isa = PBXBuildFile; productRef = 6C0617D52BDB4432008C9C42 /* LogStream */; }; + 6C08249C2C556F7400A0751E /* TerminalCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C08249B2C556F7400A0751E /* TerminalCache.swift */; }; + 6C08249E2C55768400A0751E /* UtilityAreaTerminal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C08249D2C55768400A0751E /* UtilityAreaTerminal.swift */; }; + 6C0824A12C5C0C9700A0751E /* SwiftTerm in Frameworks */ = {isa = PBXBuildFile; productRef = 6C0824A02C5C0C9700A0751E /* SwiftTerm */; }; 6C092EDA2A53A58600489202 /* EditorLayout+StateRestoration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C092ED92A53A58600489202 /* EditorLayout+StateRestoration.swift */; }; 6C092EE02A53BFCF00489202 /* WorkspaceStateKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C092EDF2A53BFCF00489202 /* WorkspaceStateKey.swift */; }; 6C0D0C6829E861B000AE4D3F /* SettingsSidebarFix.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C0D0C6729E861B000AE4D3F /* SettingsSidebarFix.swift */; }; @@ -391,6 +393,8 @@ 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 */; }; + 6C4E37F62C73DA5200AEE7B5 /* UtilityAreaTerminalSidebar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C4E37F52C73DA5200AEE7B5 /* UtilityAreaTerminalSidebar.swift */; }; + 6C4E37FC2C73E00700AEE7B5 /* SwiftTerm in Frameworks */ = {isa = PBXBuildFile; productRef = 6C4E37FB2C73E00700AEE7B5 /* SwiftTerm */; }; 6C5228B529A868BD00AC48F6 /* Environment+ContentInsets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C5228B429A868BD00AC48F6 /* Environment+ContentInsets.swift */; }; 6C53AAD829A6C4FD00EE9ED6 /* SplitView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C53AAD729A6C4FD00EE9ED6 /* SplitView.swift */; }; 6C578D8129CD294800DC73B2 /* ExtensionActivatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C578D8029CD294800DC73B2 /* ExtensionActivatorView.swift */; }; @@ -450,6 +454,8 @@ 6CD0358A2C3461160091E1F4 /* KeyWindowControllerObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CD035892C3461160091E1F4 /* KeyWindowControllerObserver.swift */; }; 6CD03B6A29FC773F001BD1D0 /* SettingsInjector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CD03B6929FC773F001BD1D0 /* SettingsInjector.swift */; }; 6CDA84AD284C1BA000C1CC3A /* EditorTabBarContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CDA84AC284C1BA000C1CC3A /* EditorTabBarContextMenu.swift */; }; + 6CE21E812C643D8F0031B056 /* CETerminalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CE21E802C643D8F0031B056 /* CETerminalView.swift */; }; + 6CE21E872C650D2C0031B056 /* SwiftTerm in Frameworks */ = {isa = PBXBuildFile; productRef = 6CE21E862C650D2C0031B056 /* SwiftTerm */; }; 6CE622692A2A174A0013085C /* InspectorTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CE622682A2A174A0013085C /* InspectorTab.swift */; }; 6CE6226B2A2A1C730013085C /* UtilityAreaTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CE6226A2A2A1C730013085C /* UtilityAreaTab.swift */; }; 6CE6226E2A2A1CDE0013085C /* NavigatorTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CE6226D2A2A1CDE0013085C /* NavigatorTab.swift */; }; @@ -1011,6 +1017,8 @@ 66F370332BEE537B00D3B823 /* NonTextFileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonTextFileView.swift; sourceTree = ""; }; 6C049A362A49E2DB00D42923 /* DirectoryEventStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectoryEventStream.swift; sourceTree = ""; }; 6C05A8AE284D0CA3007F4EAA /* WorkspaceDocument+Listeners.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WorkspaceDocument+Listeners.swift"; sourceTree = ""; }; + 6C08249B2C556F7400A0751E /* TerminalCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalCache.swift; sourceTree = ""; }; + 6C08249D2C55768400A0751E /* UtilityAreaTerminal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UtilityAreaTerminal.swift; sourceTree = ""; }; 6C092ED92A53A58600489202 /* EditorLayout+StateRestoration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EditorLayout+StateRestoration.swift"; sourceTree = ""; }; 6C092EDF2A53BFCF00489202 /* WorkspaceStateKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceStateKey.swift; sourceTree = ""; }; 6C0D0C6729E861B000AE4D3F /* SettingsSidebarFix.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSidebarFix.swift; sourceTree = ""; }; @@ -1043,6 +1051,7 @@ 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 = ""; }; + 6C4E37F52C73DA5200AEE7B5 /* UtilityAreaTerminalSidebar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UtilityAreaTerminalSidebar.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 = ""; }; 6C578D8029CD294800DC73B2 /* ExtensionActivatorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExtensionActivatorView.swift; sourceTree = ""; }; @@ -1089,6 +1098,7 @@ 6CD035892C3461160091E1F4 /* KeyWindowControllerObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyWindowControllerObserver.swift; sourceTree = ""; }; 6CD03B6929FC773F001BD1D0 /* SettingsInjector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsInjector.swift; sourceTree = ""; }; 6CDA84AC284C1BA000C1CC3A /* EditorTabBarContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditorTabBarContextMenu.swift; sourceTree = ""; }; + 6CE21E802C643D8F0031B056 /* CETerminalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CETerminalView.swift; sourceTree = ""; }; 6CE622682A2A174A0013085C /* InspectorTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InspectorTab.swift; sourceTree = ""; }; 6CE6226A2A2A1C730013085C /* UtilityAreaTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UtilityAreaTab.swift; sourceTree = ""; }; 6CE6226D2A2A1CDE0013085C /* NavigatorTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigatorTab.swift; sourceTree = ""; }; @@ -1260,17 +1270,19 @@ 6C66C31329D05CDC00DE9ED2 /* GRDB in Frameworks */, 58F2EB1E292FB954004A9BDE /* Sparkle in Frameworks */, 6C147C4529A329350089B630 /* OrderedCollections in Frameworks */, + 6CE21E872C650D2C0031B056 /* SwiftTerm in Frameworks */, 6C0617D62BDB4432008C9C42 /* LogStream in Frameworks */, 6CC17B4F2C432AE000834E2C /* CodeEditSourceEditor in Frameworks */, 30CB64912C16CA8100CC8A9E /* LanguageServerProtocol in Frameworks */, + 6C4E37FC2C73E00700AEE7B5 /* SwiftTerm in Frameworks */, 6C6BD6F429CD142C00235D17 /* CollectionConcurrencyKit in Frameworks */, 6C85BB442C210EFD00EB5DEF /* SwiftUIIntrospect in Frameworks */, 6CB446402B6DFF3A00539ED0 /* CodeEditSourceEditor in Frameworks */, - 5879828A292ED15F0085B254 /* SwiftTerm in Frameworks */, 2816F594280CF50500DD548B /* CodeEditSymbols in Frameworks */, 30CB64942C16CA9100CC8A9E /* LanguageClient in Frameworks */, 6CC17B592C43F53700834E2C /* CodeEditSourceEditor in Frameworks */, 6C6BD6F829CD14D100235D17 /* CodeEditKit in Frameworks */, + 6C0824A12C5C0C9700A0751E /* SwiftTerm in Frameworks */, 6C81916B29B41DD300B75C92 /* DequeModule in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -2809,6 +2821,7 @@ 6C48B5DB2C0D664A001E9955 /* Model */ = { isa = PBXGroup; children = ( + 6C08249B2C556F7400A0751E /* TerminalCache.swift */, 6C48B5D92C0D5FC5001E9955 /* CurrentUser.swift */, 6C48B5CD2C0C1BE4001E9955 /* Shell.swift */, 6C48B5D02C0D0519001E9955 /* ShellIntegration.swift */, @@ -2819,6 +2832,7 @@ 6C48B5DC2C0D6654001E9955 /* Views */ = { isa = PBXGroup; children = ( + 6CE21E802C643D8F0031B056 /* CETerminalView.swift */, 58798280292ED0FB0085B254 /* TerminalEmulatorView.swift */, 58798281292ED0FB0085B254 /* TerminalEmulatorView+Coordinator.swift */, ); @@ -3259,6 +3273,7 @@ children = ( B62AEDB22A1FD95B009A9F52 /* UtilityAreaTerminalView.swift */, B62AEDBB2A210DBB009A9F52 /* UtilityAreaTerminalTab.swift */, + 6C4E37F52C73DA5200AEE7B5 /* UtilityAreaTerminalSidebar.swift */, ); path = TerminalUtility; sourceTree = ""; @@ -3285,6 +3300,7 @@ isa = PBXGroup; children = ( 6CE6226A2A2A1C730013085C /* UtilityAreaTab.swift */, + 6C08249D2C55768400A0751E /* UtilityAreaTerminal.swift */, ); path = Models; sourceTree = ""; @@ -3302,6 +3318,22 @@ path = Settings; sourceTree = ""; }; + B6966A262C2F673A00259C2D /* Views */ = { + isa = PBXGroup; + children = ( + B6BF41412C2C672A003AB4B3 /* SourceControlPushView.swift */, + B6966A272C2F683300259C2D /* SourceControlPullView.swift */, + B607184B2B17E037009CDAB4 /* SourceControlStashView.swift */, + B6966A292C2F687A00259C2D /* SourceControlFetchView.swift */, + B67431CB2C3E45F30047FCA6 /* SourceControlSwitchView.swift */, + B65B10F42B081A0C002852CF /* SourceControlAddExistingRemoteView.swift */, + 041FC6AC2AE437CE00C1F65A /* SourceControlNewBranchView.swift */, + B60718362B170638009CDAB4 /* SourceControlRenameBranchView.swift */, + B6966A2F2C33282200259C2D /* RemoteBranchPicker.swift */, + ); + path = Views; + sourceTree = ""; + }; B69D3EDC2C5E856F005CF43A /* Views */ = { isa = PBXGroup; children = ( @@ -3320,22 +3352,6 @@ path = Models; sourceTree = ""; }; - B6966A262C2F673A00259C2D /* Views */ = { - isa = PBXGroup; - children = ( - B6BF41412C2C672A003AB4B3 /* SourceControlPushView.swift */, - B6966A272C2F683300259C2D /* SourceControlPullView.swift */, - B607184B2B17E037009CDAB4 /* SourceControlStashView.swift */, - B6966A292C2F687A00259C2D /* SourceControlFetchView.swift */, - B67431CB2C3E45F30047FCA6 /* SourceControlSwitchView.swift */, - B65B10F42B081A0C002852CF /* SourceControlAddExistingRemoteView.swift */, - 041FC6AC2AE437CE00C1F65A /* SourceControlNewBranchView.swift */, - B60718362B170638009CDAB4 /* SourceControlRenameBranchView.swift */, - B6966A2F2C33282200259C2D /* RemoteBranchPicker.swift */, - ); - path = Views; - sourceTree = ""; - }; B6AB09AB2AAACBF70003A3A6 /* Tabs */ = { isa = PBXGroup; children = ( @@ -3560,7 +3576,6 @@ name = CodeEdit; packageProductDependencies = ( 2816F593280CF50500DD548B /* CodeEditSymbols */, - 58798289292ED15F0085B254 /* SwiftTerm */, 58F2EB1D292FB954004A9BDE /* Sparkle */, 6C147C4429A329350089B630 /* OrderedCollections */, 6C81916A29B41DD300B75C92 /* DequeModule */, @@ -3573,6 +3588,9 @@ 6C85BB432C210EFD00EB5DEF /* SwiftUIIntrospect */, 6CC17B4E2C432AE000834E2C /* CodeEditSourceEditor */, 6CC17B582C43F53700834E2C /* CodeEditSourceEditor */, + 6C0824A02C5C0C9700A0751E /* SwiftTerm */, + 6CE21E862C650D2C0031B056 /* SwiftTerm */, + 6C4E37FB2C73E00700AEE7B5 /* SwiftTerm */, ); productName = CodeEdit; productReference = B658FB2C27DA9E0F00EA4DBD /* CodeEdit.app */; @@ -3658,7 +3676,6 @@ packageReferences = ( 2816F592280CF50500DD548B /* XCRemoteSwiftPackageReference "CodeEditSymbols" */, 287136B1292A407E00E9F5F4 /* XCRemoteSwiftPackageReference "SwiftLintPlugin" */, - 58798288292ED15F0085B254 /* XCRemoteSwiftPackageReference "SwiftTerm" */, 58F2EB1C292FB954004A9BDE /* XCRemoteSwiftPackageReference "Sparkle" */, 583E529A29361BAB001AB554 /* XCRemoteSwiftPackageReference "swift-snapshot-testing" */, 6C147C4329A329350089B630 /* XCRemoteSwiftPackageReference "swift-collections" */, @@ -3670,6 +3687,7 @@ 303E88452C276FD100EEA8D9 /* XCRemoteSwiftPackageReference "LanguageClient" */, 303E88462C276FD600EEA8D9 /* XCRemoteSwiftPackageReference "LanguageServerProtocol" */, 6CC17B572C43F53700834E2C /* XCRemoteSwiftPackageReference "CodeEditSourceEditor" */, + 6C4E37FA2C73E00700AEE7B5 /* XCRemoteSwiftPackageReference "SwiftTerm" */, ); productRefGroup = B658FB2D27DA9E0F00EA4DBD /* Products */; projectDirPath = ""; @@ -3876,6 +3894,7 @@ 58A2E40C29C3975D005CB615 /* CEWorkspaceFileIcon.swift in Sources */, 587B9E8F29301D8F00AC7927 /* BitBucketUserRouter.swift in Sources */, B66A4E5129C917D5004573B4 /* AboutWindow.swift in Sources */, + 6C08249C2C556F7400A0751E /* TerminalCache.swift in Sources */, B6C4F2A62B3CABD200B2B140 /* HistoryInspectorItemView.swift in Sources */, B65B10FE2B08B07D002852CF /* SourceControlNavigatorChangesList.swift in Sources */, 58F2EB03292FB2B0004A9BDE /* Documentation.docc in Sources */, @@ -4165,6 +4184,7 @@ 58FD7608291EA1CB0051D6E4 /* QuickActionsViewModel.swift in Sources */, B65B11042B09DB1C002852CF /* GitClient+Fetch.swift in Sources */, 5878DA872918642F00DD95A3 /* AcknowledgementsViewModel.swift in Sources */, + 6C4E37F62C73DA5200AEE7B5 /* UtilityAreaTerminalSidebar.swift in Sources */, B6E41C7929DE02800088F9F4 /* AccountSelectionView.swift in Sources */, 6C48B5CE2C0C1BE4001E9955 /* Shell.swift in Sources */, 6CA1AE952B46950000378EAB /* EditorInstance.swift in Sources */, @@ -4258,6 +4278,7 @@ 58F2EAEC292FB2B0004A9BDE /* IgnoredFiles.swift in Sources */, 6CD03B6A29FC773F001BD1D0 /* SettingsInjector.swift in Sources */, 58798236292E30B90085B254 /* FeedbackType.swift in Sources */, + 6CE21E812C643D8F0031B056 /* CETerminalView.swift in Sources */, 587B9E6D29301D8F00AC7927 /* GitLabEventNote.swift in Sources */, 587B9E9129301D8F00AC7927 /* BitBucketOAuthRouter.swift in Sources */, B6FA3F882BF41C940023DE9C /* ThemeSettingsThemeToken.swift in Sources */, @@ -4294,6 +4315,7 @@ 611191FE2B08CCD200D4459B /* SearchIndexer+File.swift in Sources */, B69D3EE52C5F54B3005CF43A /* TasksPopoverMenuItem.swift in Sources */, 669A50532C380C8E00304CD8 /* Collection+subscript_safe.swift in Sources */, + 6C08249E2C55768400A0751E /* UtilityAreaTerminal.swift in Sources */, 5B241BF32B6DDBFF0016E616 /* IgnorePatternListItemView.swift in Sources */, 6CB52DC92AC8DC3E002E75B3 /* CEWorkspaceFileManager+FileManagement.swift in Sources */, 58F2EB0B292FB2B0004A9BDE /* AccountsSettings.swift in Sources */, @@ -5474,14 +5496,6 @@ minimumVersion = 1.14.2; }; }; - 58798288292ED15F0085B254 /* XCRemoteSwiftPackageReference "SwiftTerm" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/migueldeicaza/SwiftTerm.git"; - requirement = { - kind = exactVersion; - version = 1.2.0; - }; - }; 58F2EB1C292FB954004A9BDE /* XCRemoteSwiftPackageReference "Sparkle" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/sparkle-project/Sparkle.git"; @@ -5506,6 +5520,14 @@ minimumVersion = 1.0.0; }; }; + 6C4E37FA2C73E00700AEE7B5 /* XCRemoteSwiftPackageReference "SwiftTerm" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/migueldeicaza/SwiftTerm"; + requirement = { + kind = revision; + revision = 384776a4e24d08833ac7c6b8c6f6c7490323c845; + }; + }; 6C66C31129D05CC800DE9ED2 /* XCRemoteSwiftPackageReference "GRDB.swift" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/groue/GRDB.swift.git"; @@ -5569,11 +5591,6 @@ package = 583E529A29361BAB001AB554 /* XCRemoteSwiftPackageReference "swift-snapshot-testing" */; productName = SnapshotTesting; }; - 58798289292ED15F0085B254 /* SwiftTerm */ = { - isa = XCSwiftPackageProductDependency; - package = 58798288292ED15F0085B254 /* XCRemoteSwiftPackageReference "SwiftTerm" */; - productName = SwiftTerm; - }; 58F2EB1D292FB954004A9BDE /* Sparkle */ = { isa = XCSwiftPackageProductDependency; package = 58F2EB1C292FB954004A9BDE /* XCRemoteSwiftPackageReference "Sparkle" */; @@ -5584,11 +5601,20 @@ package = 6C0617D42BDB4432008C9C42 /* XCRemoteSwiftPackageReference "LogStream" */; productName = LogStream; }; + 6C0824A02C5C0C9700A0751E /* SwiftTerm */ = { + isa = XCSwiftPackageProductDependency; + productName = SwiftTerm; + }; 6C147C4429A329350089B630 /* OrderedCollections */ = { isa = XCSwiftPackageProductDependency; package = 6C147C4329A329350089B630 /* XCRemoteSwiftPackageReference "swift-collections" */; productName = OrderedCollections; }; + 6C4E37FB2C73E00700AEE7B5 /* SwiftTerm */ = { + isa = XCSwiftPackageProductDependency; + package = 6C4E37FA2C73E00700AEE7B5 /* XCRemoteSwiftPackageReference "SwiftTerm" */; + productName = SwiftTerm; + }; 6C66C31229D05CDC00DE9ED2 /* GRDB */ = { isa = XCSwiftPackageProductDependency; package = 6C66C31129D05CC800DE9ED2 /* XCRemoteSwiftPackageReference "GRDB.swift" */; @@ -5636,6 +5662,10 @@ package = 6CC17B572C43F53700834E2C /* XCRemoteSwiftPackageReference "CodeEditSourceEditor" */; productName = CodeEditSourceEditor; }; + 6CE21E862C650D2C0031B056 /* SwiftTerm */ = { + isa = XCSwiftPackageProductDependency; + productName = SwiftTerm; + }; /* 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 114f6ef0c..cef2615ca 100644 --- a/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "a33fcca819dee4c816b1474e19017510b1d62b170c921187042e0675d3f4b0b3", + "originHash" : "c1c6a3fce844bb0e9fb04272ffab26747869319dec6715e2d5d6ab10df59932a", "pins" : [ { "identity" : "anycodable", @@ -13,7 +13,7 @@ { "identity" : "codeeditkit", "kind" : "remoteSourceControl", - "location" : "https://github.com/CodeEditApp/CodeEditKit", + "location" : "https://github.com/CodeEditApp/CodeEditKit.git", "state" : { "revision" : "ad28213a968586abb0cb21a8a56a3587227895f1", "version" : "0.1.2" @@ -220,10 +220,9 @@ { "identity" : "swiftterm", "kind" : "remoteSourceControl", - "location" : "https://github.com/migueldeicaza/SwiftTerm.git", + "location" : "https://github.com/migueldeicaza/SwiftTerm", "state" : { - "revision" : "55e7cdbeb3f41c80cce7b8a29ce9d17e214b2e77", - "version" : "1.2.0" + "revision" : "384776a4e24d08833ac7c6b8c6f6c7490323c845" } }, { diff --git a/CodeEdit/Features/TerminalEmulator/Model/Shell.swift b/CodeEdit/Features/TerminalEmulator/Model/Shell.swift index 72200a7c3..ba556d46b 100644 --- a/CodeEdit/Features/TerminalEmulator/Model/Shell.swift +++ b/CodeEdit/Features/TerminalEmulator/Model/Shell.swift @@ -77,4 +77,23 @@ enum Shell: String, CaseIterable { // Run the process try process.run() } + + var defaultPath: String { + switch self { + case .bash: + "/bin/bash" + case .zsh: + "/bin/zsh" + } + } + + /// Gets the default shell from the current user and returns the string of the shell path. + /// + /// If getting the user's shell does not work, defaults to `zsh`, + static func autoDetectDefaultShell() -> String { + guard let currentUser = CurrentUser.getCurrentUser() else { + return Self.zsh.rawValue // macOS defaults to zsh + } + return currentUser.shell + } } diff --git a/CodeEdit/Features/TerminalEmulator/Model/TerminalCache.swift b/CodeEdit/Features/TerminalEmulator/Model/TerminalCache.swift new file mode 100644 index 000000000..9f5b4b069 --- /dev/null +++ b/CodeEdit/Features/TerminalEmulator/Model/TerminalCache.swift @@ -0,0 +1,43 @@ +// +// TerminalCache.swift +// CodeEdit +// +// Created by Khan Winter on 7/27/24. +// + +import Foundation +import SwiftTerm + +/// Stores a mapping of ID -> terminal view for reusing terminal views. +/// This allows terminal views to continue to receive data even when not in the view hierarchy. +final class TerminalCache { + static let shared: TerminalCache = TerminalCache() + + /// The cache of terminal views. + private var terminals: [UUID: CELocalProcessTerminalView] + + private init() { + terminals = [:] + } + + /// Get a cached terminal view. + /// - Parameter id: The ID of the terminal. + /// - Returns: The existing terminal, if it exists. + func getTerminalView(_ id: UUID) -> CELocalProcessTerminalView? { + terminals[id] + } + + /// Store a terminal view for reuse. + /// - Parameters: + /// - id: The ID of the terminal. + /// - view: The view representing the terminal's contents. + func cacheTerminalView(for id: UUID, view: CELocalProcessTerminalView) { + terminals[id] = view + } + + /// Remove any view associated with the terminal id. + /// - Parameter id: The ID of the terminal. + func removeCachedView(_ id: UUID) { + terminals[id] = nil + } +} diff --git a/CodeEdit/Features/TerminalEmulator/Views/CETerminalView.swift b/CodeEdit/Features/TerminalEmulator/Views/CETerminalView.swift new file mode 100644 index 000000000..3a0b8b05c --- /dev/null +++ b/CodeEdit/Features/TerminalEmulator/Views/CETerminalView.swift @@ -0,0 +1,160 @@ +// +// CETerminalView.swift +// CodeEdit +// +// Created by Khan Winter on 8/7/24. +// + +import AppKit +import SwiftTerm +import Foundation + +/// # Dev Note (please read) +/// +/// This entire file is a nearly 1:1 copy of SwiftTerm's `LocalProcessTerminalView`. The exception being the use of +/// `CETerminalView` over `TerminalView`. This change was made to fix the terminal clearing when the view was given a +/// frame of `0`. This enables terminals to keep running in the background, and allows them to be removed and added +/// back into the hierarchy for use in the utility area. +/// +/// If there is a bug here: **there probably isn't**. Look instead in ``TerminalEmulatorView``. + +class CETerminalView: TerminalView { + override var frame: NSRect { + get { + return super.frame + } + set(newValue) { + if newValue != .zero { + super.frame = newValue + } + } + } +} + +protocol CELocalProcessTerminalViewDelegate: AnyObject { + /// This method is invoked to notify that the terminal has been resized to the specified number of columns and rows + /// the user interface code might try to adjust the containing scroll view, or if it is a top level window, the + /// window itself + /// - Parameter source: the sending instance + /// - Parameter newCols: the new number of columns that should be shown + /// - Parameter newRow: the new number of rows that should be shown + func sizeChanged(source: CETerminalView, newCols: Int, newRows: Int) + + /// This method is invoked when the title of the terminal window should be updated to the provided title + /// - Parameter source: the sending instance + /// - Parameter title: the desired title + func setTerminalTitle(source: CETerminalView, title: String) + + /// Invoked when the OSC command 7 for "current directory has changed" command is sent + /// - Parameter source: the sending instance + /// - Parameter directory: the new working directory + func hostCurrentDirectoryUpdate (source: TerminalView, directory: String?) + + /// This method will be invoked when the child process started by `startProcess` has terminated. + /// - Parameter source: the local process that terminated + /// - Parameter exitCode: the exit code returned by the process, or nil if this was an error caused during + /// the IO reading/writing + func processTerminated (source: TerminalView, exitCode: Int32?) +} + +class CELocalProcessTerminalView: CETerminalView, TerminalViewDelegate, LocalProcessDelegate { + var process: LocalProcess! + + override public init (frame: CGRect) { + super.init(frame: frame) + setup() + } + + public required init? (coder: NSCoder) { + super.init(coder: coder) + setup() + } + + func setup () { + terminalDelegate = self + process = LocalProcess(delegate: self) + } + + /// The `processDelegate` is used to deliver messages and information relevant to the execution of the terminal. + public weak var processDelegate: CELocalProcessTerminalViewDelegate? + + /// This method is invoked to notify the client of the new columsn and rows that have been set by the UI + public func sizeChanged(source: TerminalView, newCols: Int, newRows: Int) { + guard process.running else { + return + } + var size = getWindowSize() + _ = PseudoTerminalHelpers.setWinSize(masterPtyDescriptor: process.childfd, windowSize: &size) + + processDelegate?.sizeChanged(source: self, newCols: newCols, newRows: newRows) + } + + public func clipboardCopy(source: TerminalView, content: Data) { + if let str = String(bytes: content, encoding: .utf8) { + let pasteBoard = NSPasteboard.general + pasteBoard.clearContents() + pasteBoard.writeObjects([str as NSString]) + } + } + + public func rangeChanged(source: TerminalView, startY: Int, endY: Int) { } + + /// Invoke this method to notify the processDelegate of the new title for the terminal window + public func setTerminalTitle(source: TerminalView, title: String) { + processDelegate?.setTerminalTitle(source: self, title: title) + } + + public func hostCurrentDirectoryUpdate(source: TerminalView, directory: String?) { + processDelegate?.hostCurrentDirectoryUpdate(source: source, directory: directory) + } + + /// This method is invoked when input from the user needs to be sent to the client + public func send(source: TerminalView, data: ArraySlice) { + process.send(data: data) + } + + /// Use this method to toggle the logging of data coming from the host, or pass nil to stop + public func setHostLogging (directory: String?) { + process.setHostLogging(directory: directory) + } + + public func scrolled(source: TerminalView, position: Double) { } + + /// Launches a child process inside a pseudo-terminal. + /// - Parameter executable: The executable to launch inside the pseudo terminal, defaults to /bin/bash + /// - Parameter args: an array of strings that is passed as the arguments to the underlying process + /// - Parameter environment: an array of environment variables to pass to the child process, if this is null, + /// this picks a good set of defaults from `Terminal.getEnvironmentVariables`. + /// - Parameter execName: If provided, this is used as the Unix argv[0] parameter, + /// otherwise, the executable is used as the args [0], this is used when + /// the intent is to set a different process name than the file that backs it. + public func startProcess( + executable: String = "/bin/bash", + args: [String] = [], + environment: [String]? = nil, + execName: String? = nil + ) { + process.startProcess(executable: executable, args: args, environment: environment, execName: execName) + } + + /// Implements the LocalProcessDelegate method. + public func processTerminated(_ source: LocalProcess, exitCode: Int32?) { + processDelegate?.processTerminated(source: self, exitCode: exitCode) + } + + /// Implements the LocalProcessDelegate.dataReceived method + public func dataReceived(slice: ArraySlice) { + feed(byteArray: slice) + } + + /// Implements the LocalProcessDelegate.getWindowSize method + public func getWindowSize() -> winsize { + let frame: CGRect = self.frame + return winsize( + ws_row: UInt16(getTerminal().rows), + ws_col: UInt16(getTerminal().cols), + ws_xpixel: UInt16(frame.width), + ws_ypixel: UInt16(frame.height) + ) + } +} diff --git a/CodeEdit/Features/TerminalEmulator/Views/TerminalEmulatorView+Coordinator.swift b/CodeEdit/Features/TerminalEmulator/Views/TerminalEmulatorView+Coordinator.swift index 1c51eb422..ae73b830d 100644 --- a/CodeEdit/Features/TerminalEmulator/Views/TerminalEmulatorView+Coordinator.swift +++ b/CodeEdit/Features/TerminalEmulator/Views/TerminalEmulatorView+Coordinator.swift @@ -9,23 +9,21 @@ import SwiftUI import SwiftTerm extension TerminalEmulatorView { - final class Coordinator: NSObject, LocalProcessTerminalViewDelegate { - - @State private var url: URL - + final class Coordinator: NSObject, CELocalProcessTerminalViewDelegate { + private let terminalID: UUID public var onTitleChange: (_ title: String) -> Void - init(url: URL, onTitleChange: @escaping (_ title: String) -> Void) { - self._url = .init(wrappedValue: url) + init(terminalID: UUID, onTitleChange: @escaping (_ title: String) -> Void) { + self.terminalID = terminalID self.onTitleChange = onTitleChange super.init() } func hostCurrentDirectoryUpdate(source: TerminalView, directory: String?) {} - func sizeChanged(source: LocalProcessTerminalView, newCols: Int, newRows: Int) {} + func sizeChanged(source: CETerminalView, newCols: Int, newRows: Int) {} - func setTerminalTitle(source: LocalProcessTerminalView, title: String) { + func setTerminalTitle(source: CETerminalView, title: String) { onTitleChange(title) } @@ -35,7 +33,7 @@ extension TerminalEmulatorView { } source.feed(text: "Exit code: \(exitCode)\n\r\n") source.feed(text: "To open a new session, create a new terminal tab.") - TerminalEmulatorView.lastTerminal[url.path] = nil + TerminalCache.shared.removeCachedView(terminalID) } } } diff --git a/CodeEdit/Features/TerminalEmulator/Views/TerminalEmulatorView.swift b/CodeEdit/Features/TerminalEmulator/Views/TerminalEmulatorView.swift index e134f1104..871445be5 100644 --- a/CodeEdit/Features/TerminalEmulator/Views/TerminalEmulatorView.swift +++ b/CodeEdit/Features/TerminalEmulator/Views/TerminalEmulatorView.swift @@ -15,6 +15,8 @@ import SwiftTerm /// Wraps a `LocalProcessTerminalView` from `SwiftTerm` inside a `NSViewRepresentable` /// for use in SwiftUI. /// +/// Caches the view in the ``TerminalCache`` to keep terminal state when the view is removed from the hierarchy. +/// struct TerminalEmulatorView: NSViewRepresentable { @AppSettings(\.terminal) var terminalSettings @@ -23,10 +25,6 @@ struct TerminalEmulatorView: NSViewRepresentable { @StateObject private var themeModel: ThemeModel = .shared - static var lastTerminal: [String: LocalProcessTerminalView] = [:] - - @State var terminal: LocalProcessTerminalView - private var font: NSFont { if terminalSettings.useTextEditorFont { return fontSettings.current @@ -35,27 +33,35 @@ struct TerminalEmulatorView: NSViewRepresentable { } } + private let terminalID: UUID private var url: URL - public var shellType: String - + public var shellType: Shell? public var onTitleChange: (_ title: String) -> Void - init(url: URL, shellType: String? = nil, onTitleChange: @escaping (_ title: String) -> Void) { + /// Create an emulator view + /// - Parameters: + /// - url: The URL the emulator should start at. + /// - terminalID: The ID of the terminal. Used to restore state when switching away from the view. + /// - shellType: The type of shell to use. Overrides any settings or auto-detection. + /// - onTitleChange: A callback used when the terminal updates it's title. + init(url: URL, terminalID: UUID, shellType: Shell? = nil, onTitleChange: @escaping (_ title: String) -> Void) { self.url = url - self.shellType = shellType ?? "" + self.terminalID = terminalID + self.shellType = shellType self.onTitleChange = onTitleChange - self._terminal = State(initialValue: TerminalEmulatorView.lastTerminal[url.path] ?? .init(frame: .zero)) } + // MARK: - Settings + /// Returns a string of a shell path to use private func getShell() -> String { - if shellType != "" { - return shellType + if let shellType { + return shellType.defaultPath } switch terminalSettings.shell { case .system: - return autoDetectDefaultShell() + return Shell.autoDetectDefaultShell() case .bash: return "/bin/bash" case .zsh: @@ -75,14 +81,6 @@ struct TerminalEmulatorView: NSViewRepresentable { } } - /// Gets the default shell from the current user and returns the string of the shell path. - private func autoDetectDefaultShell() -> String { - guard let currentUser = CurrentUser.getCurrentUser() else { - return "/bin/bash" - } - return currentUser.shell - } - /// Returns true if the `option` key should be treated as the `meta` key. private var optionAsMeta: Bool { terminalSettings.optionAsMeta @@ -148,70 +146,81 @@ struct TerminalEmulatorView: NSViewRepresentable { return nil } - /// Inherited from NSViewRepresentable.makeNSView(context:). - func makeNSView(context: Context) -> LocalProcessTerminalView { - terminal.processDelegate = context.coordinator - do { - try setupSession() - } catch { - terminal.feed(text: "Failed to start a terminal session: \(error.localizedDescription)") - } - return terminal - } + // MARK: - NSViewRepresentable - func setupSession() throws { - terminal.getTerminal().silentLog = true - if TerminalEmulatorView.lastTerminal[url.path] == nil { - // changes working directory to project root - // TODO: Get rid of FileManager shared instance to prevent problems - // using shared instance of FileManager might lead to problems when using - // multiple workspaces. This works for now but most probably will need - // to be changed later on - FileManager.default.changeCurrentDirectoryPath(url.path) - - var terminalEnvironment: [String] = Terminal.getEnvironmentVariables() - terminalEnvironment.append("TERM_PROGRAM=CodeEditApp_Terminal") - - let shellPath = getShell() - guard let shell = Shell(rawValue: NSString(string: shellPath).lastPathComponent) else { - return - } - onTitleChange(shell.rawValue) - - let shellArgs: [String] - if terminalSettings.useShellIntegration { - shellArgs = try ShellIntegration.setUpIntegration( - for: shell, - environment: &terminalEnvironment, - useLogin: terminalSettings.useLoginShell - ) - } else { - shellArgs = [] + /// Inherited from NSViewRepresentable.makeNSView(context:). + func makeNSView(context: Context) -> CELocalProcessTerminalView { + let terminalExists = TerminalCache.shared.getTerminalView(terminalID) != nil + let view = TerminalCache.shared.getTerminalView(terminalID) ?? CELocalProcessTerminalView(frame: .zero) + view.processDelegate = context.coordinator + if !terminalExists { // New terminal, start the shell process. + do { + try setupSession(view) + } catch { + view.feed(text: "Failed to start a terminal session: \(error.localizedDescription)") } - - terminal.startProcess( - executable: shellPath, - args: shellArgs, - environment: terminalEnvironment, - execName: shell.rawValue + configureView(view) + } + TerminalCache.shared.cacheTerminalView(for: terminalID, view: view) + return view + } + + /// Setup a new shell process. + /// - Parameter terminal: The terminal view to set up. + func setupSession(_ terminal: CELocalProcessTerminalView) throws { + // changes working directory to project root + // TODO: Get rid of FileManager shared instance to prevent problems + // using shared instance of FileManager might lead to problems when using + // multiple workspaces. This works for now but most probably will need + // to be changed later on + FileManager.default.changeCurrentDirectoryPath(url.path) + + var terminalEnvironment: [String] = Terminal.getEnvironmentVariables() + terminalEnvironment.append("TERM_PROGRAM=CodeEditApp_Terminal") + + let shellPath = getShell() + guard let shell = Shell(rawValue: NSString(string: shellPath).lastPathComponent) else { + return + } + onTitleChange(shell.rawValue) + + let shellArgs: [String] + if terminalSettings.useShellIntegration { + shellArgs = try ShellIntegration.setUpIntegration( + for: shell, + environment: &terminalEnvironment, + useLogin: terminalSettings.useLoginShell ) - terminal.font = font - terminal.configureNativeColors() - terminal.installColors(self.colors) - terminal.caretColor = cursorColor - terminal.selectedTextBackgroundColor = selectionColor - terminal.nativeForegroundColor = textColor - terminal.nativeBackgroundColor = terminalSettings.useThemeBackground ? backgroundColor : .clear - terminal.cursorStyleChanged(source: terminal.getTerminal(), newStyle: getTerminalCursor()) - terminal.layer?.backgroundColor = .clear - terminal.optionAsMetaKey = optionAsMeta + } else { + shellArgs = [] } + + terminal.startProcess( + executable: shellPath, + args: shellArgs, + environment: terminalEnvironment, + execName: shell.rawValue + ) + terminal.font = font + terminal.configureNativeColors() + terminal.installColors(self.colors) + terminal.caretColor = cursorColor.withAlphaComponent(0.5) + terminal.caretTextColor = cursorColor.withAlphaComponent(0.5) + terminal.selectedTextBackgroundColor = selectionColor + terminal.nativeForegroundColor = textColor + terminal.nativeBackgroundColor = terminalSettings.useThemeBackground ? backgroundColor : .clear + terminal.cursorStyleChanged(source: terminal.getTerminal(), newStyle: getTerminalCursor()) + terminal.layer?.backgroundColor = .clear + terminal.optionAsMetaKey = optionAsMeta + } + + func configureView(_ terminal: CELocalProcessTerminalView) { + terminal.getTerminal().silentLog = true terminal.appearance = colorAppearance - scroller?.isHidden = true - TerminalEmulatorView.lastTerminal[url.path] = terminal + scroller(terminal)?.isHidden = true } - private var scroller: NSScroller? { + private func scroller(_ terminal: CELocalProcessTerminalView) -> NSScroller? { for subView in terminal.subviews { if let scroller = subView as? NSScroller { return scroller @@ -220,13 +229,11 @@ struct TerminalEmulatorView: NSViewRepresentable { return nil } - func updateNSView(_ view: LocalProcessTerminalView, context: Context) { - if view.font != font { // Fixes Memory leak - view.font = font - } + func updateNSView(_ view: CELocalProcessTerminalView, context: Context) { view.configureNativeColors() view.installColors(self.colors) - view.caretColor = cursorColor + view.caretColor = cursorColor.withAlphaComponent(0.5) + view.caretTextColor = cursorColor.withAlphaComponent(0.5) view.selectedTextBackgroundColor = selectionColor view.nativeForegroundColor = textColor view.nativeBackgroundColor = terminalSettings.useThemeBackground ? backgroundColor : .clear @@ -234,14 +241,11 @@ struct TerminalEmulatorView: NSViewRepresentable { view.optionAsMetaKey = optionAsMeta view.cursorStyleChanged(source: view.getTerminal(), newStyle: getTerminalCursor()) view.appearance = colorAppearance - if TerminalEmulatorView.lastTerminal[url.path] != nil { - TerminalEmulatorView.lastTerminal[url.path] = view - } view.getTerminal().softReset() view.feed(text: "") // send empty character to force colors to be redrawn } func makeCoordinator() -> Coordinator { - Coordinator(url: url, onTitleChange: onTitleChange) + Coordinator(terminalID: terminalID, onTitleChange: onTitleChange) } } diff --git a/CodeEdit/Features/UtilityArea/Models/UtilityAreaTerminal.swift b/CodeEdit/Features/UtilityArea/Models/UtilityAreaTerminal.swift new file mode 100644 index 000000000..abf9d780d --- /dev/null +++ b/CodeEdit/Features/UtilityArea/Models/UtilityAreaTerminal.swift @@ -0,0 +1,30 @@ +// +// UtilityAreaTerminal.swift +// CodeEdit +// +// Created by Khan Winter on 7/27/24. +// + +import Foundation + +final class UtilityAreaTerminal: ObservableObject, Identifiable, Equatable { + let id: UUID + @Published var url: URL + @Published var title: String + @Published var terminalTitle: String + @Published var shell: Shell? + @Published var customTitle: Bool + + init(id: UUID, url: URL, title: String, shell: Shell?) { + self.id = id + self.title = title + self.terminalTitle = title + self.url = url + self.shell = shell + self.customTitle = false + } + + static func == (lhs: UtilityAreaTerminal, rhs: UtilityAreaTerminal) -> Bool { + lhs.id == rhs.id + } +} diff --git a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalSidebar.swift b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalSidebar.swift new file mode 100644 index 000000000..6f1b0817d --- /dev/null +++ b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalSidebar.swift @@ -0,0 +1,78 @@ +// +// UtilityAreaTerminalSidebar.swift +// CodeEdit +// +// Created by Khan Winter on 8/19/24. +// + +import SwiftUI + +/// The view that displays the list of available terminals in the utility area. +/// See ``UtilityAreaTerminalView`` for use. +struct UtilityAreaTerminalSidebar: View { + @EnvironmentObject private var workspace: WorkspaceDocument + @EnvironmentObject private var utilityAreaViewModel: UtilityAreaViewModel + + var body: some View { + List(selection: $utilityAreaViewModel.selectedTerminals) { + ForEach(utilityAreaViewModel.terminals, id: \.self.id) { terminal in + UtilityAreaTerminalTab( + terminal: terminal, + removeTerminals: utilityAreaViewModel.removeTerminals, + isSelected: utilityAreaViewModel.selectedTerminals.contains(terminal.id), + selectedIDs: utilityAreaViewModel.selectedTerminals + ) + .tag(terminal.id) + .listRowSeparator(.hidden) + } + .onMove { [weak utilityAreaViewModel] (source, destination) in + utilityAreaViewModel?.moveItems(from: source, to: destination) + } + } + .focusedObject(utilityAreaViewModel) + .listStyle(.automatic) + .accentColor(.secondary) + .contextMenu { + Button("New Terminal") { + utilityAreaViewModel.addTerminal(workspace: workspace) + } + Menu("New Terminal With Profile") { + Button("Default") { + utilityAreaViewModel.addTerminal(workspace: workspace) + } + Divider() + ForEach(Shell.allCases, id: \.self) { shell in + Button(shell.rawValue) { + utilityAreaViewModel.addTerminal(shell: shell, workspace: workspace) + } + } + } + } + .onChange(of: utilityAreaViewModel.terminals) { newValue in + if newValue.isEmpty { + utilityAreaViewModel.addTerminal(workspace: workspace) + } + } + .paneToolbar { + PaneToolbarSection { + Button { + utilityAreaViewModel.addTerminal(workspace: workspace) + } label: { + Image(systemName: "plus") + } + Button { + utilityAreaViewModel.removeTerminals(utilityAreaViewModel.selectedTerminals) + } label: { + Image(systemName: "minus") + } + .disabled(utilityAreaViewModel.terminals.count <= 1) + .opacity(utilityAreaViewModel.terminals.count <= 1 ? 0.5 : 1) + } + Spacer() + } + } +} + +#Preview { + UtilityAreaTerminalSidebar() +} diff --git a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalView.swift b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalView.swift index 7110bfd00..bce73acc3 100644 --- a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalView.swift +++ b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalView.swift @@ -8,28 +8,6 @@ import SwiftUI import Cocoa -final class UtilityAreaTerminal: ObservableObject, Identifiable, Equatable { - let id: UUID - @Published var url: URL? - @Published var title: String - @Published var terminalTitle: String - @Published var shell: String - @Published var customTitle: Bool - - init(id: UUID, url: URL, title: String, shell: String) { - self.id = id - self.title = title - self.terminalTitle = title - self.url = url - self.shell = shell - self.customTitle = false - } - - static func == (lhs: UtilityAreaTerminal, rhs: UtilityAreaTerminal) -> Bool { - lhs.id == rhs.id - } -} - struct UtilityAreaTerminalView: View { @AppSettings(\.theme.matchAppearance) private var matchAppearance @@ -63,40 +41,6 @@ struct UtilityAreaTerminalView: View { useTextEditorFont == true ? textEditingFont.current : terminalFont.current } - private func initializeTerminals() { - let id = UUID() - - utilityAreaViewModel.terminals = [ - UtilityAreaTerminal( - id: id, - url: workspace.workspaceFileManager?.folderUrl ?? URL(filePath: "/"), - title: "terminal", - shell: "" - ) - ] - - utilityAreaViewModel.selectedTerminals = [id] - } - - private func addTerminal(shell: String? = nil) { - let id = UUID() - - utilityAreaViewModel.terminals.append( - UtilityAreaTerminal( - id: id, - url: URL(filePath: "\(id)"), - title: "terminal", - shell: shell ?? "" - ) - ) - - utilityAreaViewModel.selectedTerminals = [id] - } - - private func getTerminal(_ id: UUID) -> UtilityAreaTerminal? { - return utilityAreaViewModel.terminals.first(where: { $0.id == id }) ?? nil - } - /// Returns the `background` color of the selected theme private var backgroundColor: NSColor { if let selectedTheme = matchAppearance && darkAppearance @@ -108,53 +52,75 @@ struct UtilityAreaTerminalView: View { return .windowBackgroundColor } - func moveItems(from source: IndexSet, to destination: Int) { - utilityAreaViewModel.terminals.move(fromOffsets: source, toOffset: destination) + /// Decides the color scheme used in the terminal. + /// + /// Decision list: + /// - If there is no selection, use the system color scheme ``UtilityAreaTerminalView/colorScheme`` + /// - If the match appearance and dark appearance settings are true, return dark if the selected dark theme is dark. + /// - Otherwise, return dark if the selected theme is dark. + private var terminalColorScheme: ColorScheme { + return if utilityAreaViewModel.selectedTerminals.isEmpty { + colorScheme + } else if matchAppearance && darkAppearance { + themeModel.selectedDarkTheme?.appearance == .dark ? .dark : .light + } else { + themeModel.selectedTheme?.appearance == .dark ? .dark : .light + } + } + + /// Finds the selected terminal. + /// - Returns: The selected terminal. + private func getSelectedTerminal() -> UtilityAreaTerminal? { + guard let selectedTerminalID = utilityAreaViewModel.selectedTerminals.first else { + return nil + } + return utilityAreaViewModel.terminals.first(where: { $0.id == selectedTerminalID }) } + /// Estimate the font's height for keeping the terminal aligned with the bottom. + /// - Parameter nsFont: The font being used in the terminal. + /// - Returns: The height in pixels of the font. func fontTotalHeight(nsFont: NSFont) -> CGFloat { let ctFont = nsFont as CTFont let ascent = CTFontGetAscent(ctFont) let descent = CTFontGetDescent(ctFont) let leading = CTFontGetLeading(ctFont) - return ascent + descent + leading } var body: some View { UtilityAreaTabView(model: utilityAreaViewModel.tabViewModel) { tabState in ZStack { - if utilityAreaViewModel.selectedTerminals.isEmpty { - CEContentUnavailableView("No Selection") - } else { + // Keeps the sidebar from changing sizes because TerminalEmulatorView takes a µs to load in + HStack { Spacer() } + + if let selectedTerminal = getSelectedTerminal() { GeometryReader { geometry in let containerHeight = geometry.size.height let totalFontHeight = fontTotalHeight(nsFont: font).rounded(.up) let constrainedHeight = containerHeight - containerHeight.truncatingRemainder( dividingBy: totalFontHeight ) - ForEach(utilityAreaViewModel.terminals) { terminal in - VStack(spacing: 0) { - Spacer(minLength: 0) - .frame(minHeight: 0) - TerminalEmulatorView( - url: terminal.url!, - shellType: terminal.shell, - onTitleChange: { [weak terminal] newTitle in - guard let id = terminal?.id else { return } - // This can be called whenever, even in a view update - // so it needs to be dispatched. - DispatchQueue.main.async { [weak utilityAreaViewModel] in - utilityAreaViewModel?.updateTerminal(id, title: newTitle) - } + VStack(spacing: 0) { + Spacer(minLength: 0).frame(minHeight: 0) + TerminalEmulatorView( + url: selectedTerminal.url, + terminalID: selectedTerminal.id, + shellType: selectedTerminal.shell, + onTitleChange: { [weak selectedTerminal] newTitle in + guard let id = selectedTerminal?.id else { return } + // This can be called whenever, even in a view update so it needs to be dispatched. + DispatchQueue.main.async { [weak utilityAreaViewModel] in + utilityAreaViewModel?.updateTerminal(id, title: newTitle) } - ) - .frame(height: constrainedHeight - totalFontHeight + 1) - } - .disabled(terminal.id != utilityAreaViewModel.selectedTerminals.first) - .opacity(terminal.id == utilityAreaViewModel.selectedTerminals.first ? 1 : 0) + } + ) + .frame(height: constrainedHeight - 1) + .id(selectedTerminal.id) } } + } else { + CEContentUnavailableView("No Selection") } } .padding(.horizontal, 10) @@ -169,95 +135,48 @@ struct UtilityAreaTerminalView: View { Spacer() PaneToolbarSection { Button { - // clear logs + guard let terminal = getSelectedTerminal() else { + return + } + utilityAreaViewModel.addTerminal(shell: nil, workspace: workspace, replacing: terminal.id) } label: { Image(systemName: "trash") } + .help("Reset the terminal") + .disabled(getSelectedTerminal() == nil) Button { // split terminal } label: { Image(systemName: "square.split.2x1") } + .help("Implementation Needed") + .disabled(true) } } .background { - if utilityAreaViewModel.selectedTerminals.isEmpty { - EffectView(.contentBackground) - } else if useThemeBackground { - Color(nsColor: backgroundColor) - } else { - if colorScheme == .dark { - EffectView(.underPageBackground) - } else { - EffectView(.contentBackground) - } - } + backgroundEffectView } - .colorScheme( - utilityAreaViewModel.selectedTerminals.isEmpty - ? colorScheme - : matchAppearance && darkAppearance - ? themeModel.selectedDarkTheme?.appearance == .dark ? .dark : .light - : themeModel.selectedTheme?.appearance == .dark ? .dark : .light - ) + .colorScheme(terminalColorScheme) } leadingSidebar: { _ in - List(selection: $utilityAreaViewModel.selectedTerminals) { - ForEach(utilityAreaViewModel.terminals, id: \.self.id) { terminal in - UtilityAreaTerminalTab( - terminal: terminal, - removeTerminals: utilityAreaViewModel.removeTerminals, - isSelected: utilityAreaViewModel.selectedTerminals.contains(terminal.id), - selectedIDs: utilityAreaViewModel.selectedTerminals - ) - .tag(terminal.id) - .listRowSeparator(.hidden) - } - .onMove(perform: moveItems) - } - .focusedObject(utilityAreaViewModel) - .listStyle(.automatic) - .accentColor(.secondary) - .contextMenu { - Button("New Terminal") { - addTerminal() - } - Menu("New Terminal With Profile") { - Button("Default") { - addTerminal() - } - Divider() - Button("Bash") { - addTerminal(shell: "/bin/bash") - } - Button("ZSH") { - addTerminal(shell: "/bin/zsh") - } - } - } - .onChange(of: utilityAreaViewModel.terminals) { newValue in - if newValue.isEmpty { - addTerminal() - } - } - .paneToolbar { - PaneToolbarSection { - Button { - addTerminal() - } label: { - Image(systemName: "plus") - } - Button { - utilityAreaViewModel.removeTerminals(utilityAreaViewModel.selectedTerminals) - } label: { - Image(systemName: "minus") - } - .disabled(utilityAreaViewModel.terminals.count <= 1) - .opacity(utilityAreaViewModel.terminals.count <= 1 ? 0.5 : 1) - } - Spacer() + UtilityAreaTerminalSidebar() + } + .onAppear { + utilityAreaViewModel.initializeTerminals(workspace) + } + } + + @ViewBuilder var backgroundEffectView: some View { + if utilityAreaViewModel.selectedTerminals.isEmpty { + EffectView(.contentBackground) + } else if useThemeBackground { + Color(nsColor: backgroundColor) + } else { + if colorScheme == .dark { + EffectView(.underPageBackground) + } else { + EffectView(.contentBackground) } } - .onAppear(perform: initializeTerminals) } } diff --git a/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift b/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift index 72f7d790b..47143a180 100644 --- a/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift +++ b/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift @@ -34,9 +34,11 @@ class UtilityAreaViewModel: ObservableObject { @Published var tabViewModel = UtilityAreaTabViewModel() func removeTerminals(_ ids: Set) { - terminals.removeAll(where: { terminal in - ids.contains(terminal.id) - }) + for (idx, terminal) in terminals.lazy.reversed().enumerated() + where ids.contains(terminal.id) { + TerminalCache.shared.removeCachedView(terminal.id) + terminals.remove(at: idx) + } selectedTerminals = [terminals.last?.id ?? UUID()] } @@ -58,6 +60,8 @@ class UtilityAreaViewModel: ObservableObject { self.isCollapsed.toggle() } + // MARK: - Terminal Management + /// Update a terminal's title. /// - Parameters: /// - id: The id of the terminal to update. @@ -74,4 +78,59 @@ class UtilityAreaViewModel: ObservableObject { terminal.customTitle = false } } + + /// Create a new terminal if there are no existing terminals. + /// Will not perform any action if terminals exist in the ``terminals`` array. + /// - Parameter workspace: The workspace to use to find the default path. + func initializeTerminals(_ workspace: WorkspaceDocument) { + guard terminals.isEmpty else { return } + addTerminal(shell: nil, workspace: workspace) + } + + /// Add a new terminal to the workspace and selects it. Optionally replaces an existing terminal + /// + /// Terminals being replaced will have the `SIGKILL` signal sent to the running shell. The new terminal will + /// inherit the same `url` and `shell` parameters from the old one, in case they were specified. + /// + /// - Parameters: + /// - shell: The shell to use, `nil` if auto-detect the default shell. + /// - workspace: The workspace to use to find the default path. + /// - replacing: The ID of a terminal to replace with a new terminal. If left `nil`, will ignore. + func addTerminal(shell: Shell? = nil, workspace: WorkspaceDocument, replacing: UUID? = nil) { + let id = UUID() + + if let replacing, let index = terminals.firstIndex(where: { $0.id == replacing }) { + let url = terminals[index].url + let shell = terminals[index].shell + if let shellPid = TerminalCache.shared.getTerminalView(replacing)?.process.shellPid { + kill(shellPid, SIGKILL) + } + terminals[index] = UtilityAreaTerminal( + id: id, + url: url, + title: "terminal", + shell: shell + ) + TerminalCache.shared.removeCachedView(replacing) + } else { + terminals.append( + UtilityAreaTerminal( + id: id, + url: workspace.workspaceFileManager?.folderUrl ?? URL(filePath: "/"), + title: "terminal", + shell: shell + ) + ) + } + + selectedTerminals = [id] + } + + /// Reorders terminals in the ``utilityAreaViewModel``. + /// - Parameters: + /// - source: The source indices. + /// - destination: The destination indices. + func moveItems(from source: IndexSet, to destination: Int) { + terminals.move(fromOffsets: source, toOffset: destination) + } } diff --git a/CodeEdit/Features/UtilityArea/Views/View+paneToolbar.swift b/CodeEdit/Features/UtilityArea/Views/View+paneToolbar.swift index 000329cc3..9a66bc05d 100644 --- a/CodeEdit/Features/UtilityArea/Views/View+paneToolbar.swift +++ b/CodeEdit/Features/UtilityArea/Views/View+paneToolbar.swift @@ -8,6 +8,8 @@ import SwiftUI extension View { + /// Clips and adds a bottom toolbar to the view. + /// - Parameter content: The content of the toolbar. func paneToolbar(@ViewBuilder content: () -> Content) -> some View { self .clipped()