diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index f6a278454..de24e6f7a 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -260,7 +260,6 @@ 58D01C9A293167DC00C5B6B4 /* CodeEditKeychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58D01C90293167DC00C5B6B4 /* CodeEditKeychain.swift */; }; 58D01C9B293167DC00C5B6B4 /* CodeEditKeychainConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58D01C91293167DC00C5B6B4 /* CodeEditKeychainConstants.swift */; }; 58D01C9D293167DC00C5B6B4 /* KeychainSwiftAccessOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58D01C93293167DC00C5B6B4 /* KeychainSwiftAccessOptions.swift */; }; - 58F2EAEC292FB2B0004A9BDE /* IgnoredFiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F2EAAA292FB2B0004A9BDE /* IgnoredFiles.swift */; }; 58F2EAEF292FB2B0004A9BDE /* ThemeSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F2EAAF292FB2B0004A9BDE /* ThemeSettingsView.swift */; }; 58F2EB03292FB2B0004A9BDE /* Documentation.docc in Sources */ = {isa = PBXBuildFile; fileRef = 58F2EACE292FB2B0004A9BDE /* Documentation.docc */; }; 58F2EB04292FB2B0004A9BDE /* SourceControlSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F2EAD1292FB2B0004A9BDE /* SourceControlSettings.swift */; }; @@ -277,7 +276,6 @@ 58FD7608291EA1CB0051D6E4 /* QuickActionsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FD7605291EA1CB0051D6E4 /* QuickActionsViewModel.swift */; }; 58FD7609291EA1CB0051D6E4 /* QuickActionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FD7607291EA1CB0051D6E4 /* QuickActionsView.swift */; }; 5994B6DA2BD6B408006A4C5F /* Editor+TabSwitch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5994B6D92BD6B408006A4C5F /* Editor+TabSwitch.swift */; }; - 5B241BF32B6DDBFF0016E616 /* IgnorePatternListItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B241BF22B6DDBFF0016E616 /* IgnorePatternListItemView.swift */; }; 5B698A0A2B262FA000DE9392 /* SearchSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B698A092B262FA000DE9392 /* SearchSettingsView.swift */; }; 5B698A0D2B26327800DE9392 /* SearchSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B698A0C2B26327800DE9392 /* SearchSettings.swift */; }; 5B698A0F2B2636A700DE9392 /* SearchSettingsIgnoreGlobPatternItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B698A0E2B2636A700DE9392 /* SearchSettingsIgnoreGlobPatternItemView.swift */; }; @@ -559,6 +557,12 @@ B67DB0F62AFC2A7A002DC647 /* FindNavigatorToolbarBottom.swift in Sources */ = {isa = PBXBuildFile; fileRef = B67DB0F52AFC2A7A002DC647 /* FindNavigatorToolbarBottom.swift */; }; B67DB0F92AFDF638002DC647 /* IconButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = B67DB0F82AFDF638002DC647 /* IconButtonStyle.swift */; }; B67DB0FC2AFDF71F002DC647 /* IconToggleStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = B67DB0FB2AFDF71F002DC647 /* IconToggleStyle.swift */; }; + B67DBB882CD51C55007F4F18 /* Limiter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B67DBB872CD51C51007F4F18 /* Limiter.swift */; }; + B67DBB8A2CD5D8F7007F4F18 /* IgnoredFilesListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B67DBB892CD5D8E4007F4F18 /* IgnoredFilesListView.swift */; }; + B67DBB8C2CD5D9CB007F4F18 /* IgnorePatternModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B67DBB8B2CD5D9B4007F4F18 /* IgnorePatternModel.swift */; }; + B67DBB902CD5EA77007F4F18 /* GlobPattern.swift in Sources */ = {isa = PBXBuildFile; fileRef = B67DBB8F2CD5EA71007F4F18 /* GlobPattern.swift */; }; + B67DBB922CD5EAAB007F4F18 /* GlobPatternList.swift in Sources */ = {isa = PBXBuildFile; fileRef = B67DBB912CD5EAA4007F4F18 /* GlobPatternList.swift */; }; + B67DBB942CD5FC08007F4F18 /* GlobPatternListItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = B67DBB932CD5FBE2007F4F18 /* GlobPatternListItem.swift */; }; B68108042C60287F008B27C1 /* StartTaskToolbarButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B68108032C60287F008B27C1 /* StartTaskToolbarButton.swift */; }; B685DE7929CC9CCD002860C8 /* StatusBarIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = B685DE7829CC9CCD002860C8 /* StatusBarIcon.swift */; }; B6966A282C2F683300259C2D /* SourceControlPullView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6966A272C2F683300259C2D /* SourceControlPullView.swift */; }; @@ -567,6 +571,7 @@ B6966A302C33282200259C2D /* RemoteBranchPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6966A2F2C33282200259C2D /* RemoteBranchPicker.swift */; }; B6966A322C3348D300259C2D /* WorkspaceSheets.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6966A312C3348D300259C2D /* WorkspaceSheets.swift */; }; B6966A342C34996B00259C2D /* SourceControlManager+GitClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6966A332C34996B00259C2D /* SourceControlManager+GitClient.swift */; }; + B696A7E62CFE20C40048CFE1 /* FeatureIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = B696A7E52CFE20C40048CFE1 /* FeatureIcon.swift */; }; B697937A29FF5668002027EC /* AccountsSettingsAccountLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = B697937929FF5668002027EC /* AccountsSettingsAccountLink.swift */; }; B69BFDC72B0686910050D9A6 /* GitClient+Initiate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B69BFDC62B0686910050D9A6 /* GitClient+Initiate.swift */; }; B69D3EDE2C5E85A2005CF43A /* StopTaskToolbarButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B69D3EDD2C5E85A2005CF43A /* StopTaskToolbarButton.swift */; }; @@ -578,6 +583,8 @@ B6AB09A32AAABFEC0003A3A6 /* EditorTabBarLeadingAccessories.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6AB09A22AAABFEC0003A3A6 /* EditorTabBarLeadingAccessories.swift */; }; B6AB09A52AAAC00F0003A3A6 /* EditorTabBarTrailingAccessories.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6AB09A42AAAC00F0003A3A6 /* EditorTabBarTrailingAccessories.swift */; }; B6AB09B32AB919CF0003A3A6 /* View+actionBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6AB09B22AB919CF0003A3A6 /* View+actionBar.swift */; }; + B6B2D79F2CE8794E00379967 /* GitConfigRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6B2D79E2CE8794200379967 /* GitConfigRepresentable.swift */; }; + B6B2D7A12CE8797B00379967 /* GitConfigExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6B2D7A02CE8797400379967 /* GitConfigExtensions.swift */; }; B6BF41422C2C672A003AB4B3 /* SourceControlPushView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6BF41412C2C672A003AB4B3 /* SourceControlPushView.swift */; }; B6C4F2A12B3CA37500B2B140 /* SourceControlNavigatorHistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6C4F2A02B3CA37500B2B140 /* SourceControlNavigatorHistoryView.swift */; }; B6C4F2A32B3CA74800B2B140 /* CommitDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6C4F2A22B3CA74800B2B140 /* CommitDetailsView.swift */; }; @@ -591,6 +598,7 @@ B6D7EA592971078500301FAC /* InspectorSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6D7EA582971078500301FAC /* InspectorSection.swift */; }; B6D7EA5C297107DD00301FAC /* InspectorField.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6D7EA5B297107DD00301FAC /* InspectorField.swift */; }; B6DCDAC62CCDE2B90099FBF9 /* InstantPopoverModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6DCDAC52CCDE2B90099FBF9 /* InstantPopoverModifier.swift */; }; + B6E38E022CD3E63A00F4E65A /* GitConfigClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E38E012CD3E62E00F4E65A /* GitConfigClient.swift */; }; B6E41C7029DD157F0088F9F4 /* AccountsSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E41C6F29DD157F0088F9F4 /* AccountsSettingsView.swift */; }; B6E41C7429DD40010088F9F4 /* View+HideSidebarToggle.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E41C7329DD40010088F9F4 /* View+HideSidebarToggle.swift */; }; B6E41C7929DE02800088F9F4 /* AccountSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E41C7829DE02800088F9F4 /* AccountSelectionView.swift */; }; @@ -944,7 +952,6 @@ 58D01C90293167DC00C5B6B4 /* CodeEditKeychain.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CodeEditKeychain.swift; sourceTree = ""; }; 58D01C91293167DC00C5B6B4 /* CodeEditKeychainConstants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CodeEditKeychainConstants.swift; sourceTree = ""; }; 58D01C93293167DC00C5B6B4 /* KeychainSwiftAccessOptions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeychainSwiftAccessOptions.swift; sourceTree = ""; }; - 58F2EAAA292FB2B0004A9BDE /* IgnoredFiles.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IgnoredFiles.swift; sourceTree = ""; }; 58F2EAAF292FB2B0004A9BDE /* ThemeSettingsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThemeSettingsView.swift; sourceTree = ""; }; 58F2EACE292FB2B0004A9BDE /* Documentation.docc */ = {isa = PBXFileReference; lastKnownFileType = folder.documentationcatalog; path = Documentation.docc; sourceTree = ""; }; 58F2EAD1292FB2B0004A9BDE /* SourceControlSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SourceControlSettings.swift; sourceTree = ""; }; @@ -960,7 +967,6 @@ 58FD7605291EA1CB0051D6E4 /* QuickActionsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QuickActionsViewModel.swift; sourceTree = ""; }; 58FD7607291EA1CB0051D6E4 /* QuickActionsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QuickActionsView.swift; sourceTree = ""; }; 5994B6D92BD6B408006A4C5F /* Editor+TabSwitch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Editor+TabSwitch.swift"; sourceTree = ""; }; - 5B241BF22B6DDBFF0016E616 /* IgnorePatternListItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IgnorePatternListItemView.swift; sourceTree = ""; }; 5B698A092B262FA000DE9392 /* SearchSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchSettingsView.swift; sourceTree = ""; }; 5B698A0C2B26327800DE9392 /* SearchSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchSettings.swift; sourceTree = ""; }; 5B698A0E2B2636A700DE9392 /* SearchSettingsIgnoreGlobPatternItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchSettingsIgnoreGlobPatternItemView.swift; sourceTree = ""; }; @@ -1227,6 +1233,12 @@ B67DB0F52AFC2A7A002DC647 /* FindNavigatorToolbarBottom.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FindNavigatorToolbarBottom.swift; sourceTree = ""; }; B67DB0F82AFDF638002DC647 /* IconButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconButtonStyle.swift; sourceTree = ""; }; B67DB0FB2AFDF71F002DC647 /* IconToggleStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconToggleStyle.swift; sourceTree = ""; }; + B67DBB872CD51C51007F4F18 /* Limiter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Limiter.swift; sourceTree = ""; }; + B67DBB892CD5D8E4007F4F18 /* IgnoredFilesListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IgnoredFilesListView.swift; sourceTree = ""; }; + B67DBB8B2CD5D9B4007F4F18 /* IgnorePatternModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IgnorePatternModel.swift; sourceTree = ""; }; + B67DBB8F2CD5EA71007F4F18 /* GlobPattern.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobPattern.swift; sourceTree = ""; }; + B67DBB912CD5EAA4007F4F18 /* GlobPatternList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobPatternList.swift; sourceTree = ""; }; + B67DBB932CD5FBE2007F4F18 /* GlobPatternListItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobPatternListItem.swift; sourceTree = ""; }; B68108032C60287F008B27C1 /* StartTaskToolbarButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartTaskToolbarButton.swift; sourceTree = ""; }; B685DE7829CC9CCD002860C8 /* StatusBarIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusBarIcon.swift; sourceTree = ""; }; B6966A272C2F683300259C2D /* SourceControlPullView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceControlPullView.swift; sourceTree = ""; }; @@ -1235,6 +1247,7 @@ B6966A2F2C33282200259C2D /* RemoteBranchPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteBranchPicker.swift; sourceTree = ""; }; B6966A312C3348D300259C2D /* WorkspaceSheets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceSheets.swift; sourceTree = ""; }; B6966A332C34996B00259C2D /* SourceControlManager+GitClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceControlManager+GitClient.swift"; sourceTree = ""; }; + B696A7E52CFE20C40048CFE1 /* FeatureIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureIcon.swift; sourceTree = ""; }; B697937929FF5668002027EC /* AccountsSettingsAccountLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsSettingsAccountLink.swift; sourceTree = ""; }; B69BFDC62B0686910050D9A6 /* GitClient+Initiate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GitClient+Initiate.swift"; sourceTree = ""; }; B69D3EDD2C5E85A2005CF43A /* StopTaskToolbarButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StopTaskToolbarButton.swift; sourceTree = ""; }; @@ -1246,6 +1259,8 @@ B6AB09A22AAABFEC0003A3A6 /* EditorTabBarLeadingAccessories.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditorTabBarLeadingAccessories.swift; sourceTree = ""; }; B6AB09A42AAAC00F0003A3A6 /* EditorTabBarTrailingAccessories.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditorTabBarTrailingAccessories.swift; sourceTree = ""; }; B6AB09B22AB919CF0003A3A6 /* View+actionBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+actionBar.swift"; sourceTree = ""; }; + B6B2D79E2CE8794200379967 /* GitConfigRepresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GitConfigRepresentable.swift; sourceTree = ""; }; + B6B2D7A02CE8797400379967 /* GitConfigExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GitConfigExtensions.swift; sourceTree = ""; }; B6BF41412C2C672A003AB4B3 /* SourceControlPushView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceControlPushView.swift; sourceTree = ""; }; B6C4F2A02B3CA37500B2B140 /* SourceControlNavigatorHistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceControlNavigatorHistoryView.swift; sourceTree = ""; }; B6C4F2A22B3CA74800B2B140 /* CommitDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommitDetailsView.swift; sourceTree = ""; }; @@ -1259,6 +1274,7 @@ B6D7EA582971078500301FAC /* InspectorSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InspectorSection.swift; sourceTree = ""; }; B6D7EA5B297107DD00301FAC /* InspectorField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InspectorField.swift; sourceTree = ""; }; B6DCDAC52CCDE2B90099FBF9 /* InstantPopoverModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPopoverModifier.swift; sourceTree = ""; }; + B6E38E012CD3E62E00F4E65A /* GitConfigClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GitConfigClient.swift; sourceTree = ""; }; B6E41C6F29DD157F0088F9F4 /* AccountsSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsSettingsView.swift; sourceTree = ""; }; B6E41C7329DD40010088F9F4 /* View+HideSidebarToggle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+HideSidebarToggle.swift"; sourceTree = ""; }; B6E41C7829DE02800088F9F4 /* AccountSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSelectionView.swift; sourceTree = ""; }; @@ -2267,6 +2283,9 @@ isa = PBXGroup; children = ( 58A5DF7F29325B5A00D1BD5D /* GitClient.swift */, + B6E38E012CD3E62E00F4E65A /* GitConfigClient.swift */, + B6B2D79E2CE8794200379967 /* GitConfigRepresentable.swift */, + B6B2D7A02CE8797400379967 /* GitConfigExtensions.swift */, 04BA7C182AE2D7C600584E1C /* GitClient+Branches.swift */, 04BA7C1D2AE2D8A000584E1C /* GitClient+Clone.swift */, 04BA7C1B2AE2D84100584E1C /* GitClient+Commit.swift */, @@ -2450,6 +2469,7 @@ 5831E3C92933E83400D5A6D2 /* Protocols */, 5875680E29316BDC00C965A3 /* ShellClient */, 6C5C891A2A3F736500A94FE1 /* FocusedValues.swift */, + B67DBB872CD51C51007F4F18 /* Limiter.swift */, ); path = Utils; sourceTree = ""; @@ -2507,7 +2527,7 @@ isa = PBXGroup; children = ( 58F2EAD1292FB2B0004A9BDE /* SourceControlSettings.swift */, - 58F2EAAA292FB2B0004A9BDE /* IgnoredFiles.swift */, + B67DBB8B2CD5D9B4007F4F18 /* IgnorePatternModel.swift */, ); path = Models; sourceTree = ""; @@ -2555,7 +2575,6 @@ children = ( 5B698A0B2B26326000DE9392 /* Models */, 5B698A092B262FA000DE9392 /* SearchSettingsView.swift */, - 5B241BF22B6DDBFF0016E616 /* IgnorePatternListItemView.swift */, 5B698A0E2B2636A700DE9392 /* SearchSettingsIgnoreGlobPatternItemView.swift */, ); path = SearchSettings; @@ -3204,6 +3223,7 @@ B640A9A329E218E300715F20 /* Models */ = { isa = PBXGroup; children = ( + B67DBB8F2CD5EA71007F4F18 /* GlobPattern.swift */, 58F2EADB292FB2B0004A9BDE /* SettingsData.swift */, 58F2EAD2292FB2B0004A9BDE /* Settings.swift */, 6C5FDF7929E6160000BC08C0 /* AppSettings.swift */, @@ -3540,6 +3560,9 @@ B6CF632A29E5436C0085880A /* Views */ = { isa = PBXGroup; children = ( + B696A7E52CFE20C40048CFE1 /* FeatureIcon.swift */, + B67DBB932CD5FBE2007F4F18 /* GlobPatternListItem.swift */, + B67DBB912CD5EAA4007F4F18 /* GlobPatternList.swift */, B6041F4C29D7A4E9000F3454 /* SettingsPageView.swift */, B640A99D29E2184700715F20 /* SettingsForm.swift */, B6EA1FFF29DB7966001BF195 /* SettingsColorPicker.swift */, @@ -3614,6 +3637,7 @@ B6F0517A29D9E46400D72287 /* SourceControlSettingsView.swift */, B6F0517629D9E3AD00D72287 /* SourceControlGeneralView.swift */, B6F0517829D9E3C900D72287 /* SourceControlGitView.swift */, + B67DBB892CD5D8E4007F4F18 /* IgnoredFilesListView.swift */, ); path = SourceControlSettings; sourceTree = ""; @@ -4011,6 +4035,7 @@ 30B0880D2C0D53080063A882 /* LanguageServer+References.swift in Sources */, 77A01E2E2BB4261200F0EA38 /* CEWorkspaceSettings.swift in Sources */, 6C4104E9297C970F00F472BA /* AboutDefaultView.swift in Sources */, + B696A7E62CFE20C40048CFE1 /* FeatureIcon.swift in Sources */, 587B9E6F29301D8F00AC7927 /* GitLabProjectAccess.swift in Sources */, 587B9E6929301D8F00AC7927 /* GitLabEvent.swift in Sources */, B63F6A7B2C561BCB003B4342 /* RegexFormatter.swift in Sources */, @@ -4051,6 +4076,7 @@ 58798233292E30B90085B254 /* FeedbackToolbar.swift in Sources */, 6CC17B5B2C44258700834E2C /* WindowControllerPropertyWrapper.swift in Sources */, 587B9E6829301D8F00AC7927 /* GitLabAccountModel.swift in Sources */, + B67DBB882CD51C55007F4F18 /* Limiter.swift in Sources */, 5878DAA7291AE76700DD95A3 /* OpenQuicklyViewModel.swift in Sources */, 6CFF967429BEBCC300182D6F /* FindCommands.swift in Sources */, 587B9E6529301D8F00AC7927 /* GitLabGroupAccess.swift in Sources */, @@ -4067,6 +4093,7 @@ 77A01E2A2BB424EA00F0EA38 /* CEWorkspaceSettingsData+ProjectSettings.swift in Sources */, 852C7E332A587279006BA599 /* SearchableSettingsPage.swift in Sources */, 587B9E5F29301D8F00AC7927 /* GitLabProjectRouter.swift in Sources */, + B6B2D7A12CE8797B00379967 /* GitConfigExtensions.swift in Sources */, 587B9E7329301D8F00AC7927 /* GitRouter.swift in Sources */, 6C2C156129B4F52F00EA60A5 /* SplitViewModifiers.swift in Sources */, 61A53A812B4449F00093BF8A /* WorkspaceDocument+Index.swift in Sources */, @@ -4118,6 +4145,7 @@ 30B088112C0D53080063A882 /* LanguageServer+SignatureHelp.swift in Sources */, 6C578D8929CD36E400DC73B2 /* Commands+ForEach.swift in Sources */, 611192082B08CCFD00D4459B /* SearchIndexer+Terms.swift in Sources */, + B67DBB902CD5EA77007F4F18 /* GlobPattern.swift in Sources */, 28B8F884280FFE4600596236 /* NSTableView+Background.swift in Sources */, 6CBA0D512A1BF524002C6FAA /* SegmentedControlImproved.swift in Sources */, 58F2EB06292FB2B0004A9BDE /* KeybindingsSettings.swift in Sources */, @@ -4144,6 +4172,7 @@ 6C5C891B2A3F736500A94FE1 /* FocusedValues.swift in Sources */, 611192062B08CCF600D4459B /* SearchIndexer+Add.swift in Sources */, B62423302C21EE280096668B /* ThemeModel+CRUD.swift in Sources */, + B67DBB942CD5FC08007F4F18 /* GlobPatternListItem.swift in Sources */, B62AEDD72A27B3D0009A9F52 /* UtilityAreaTabViewModel.swift in Sources */, 85773E1E2A3E0A1F00C5D926 /* SettingsSearchResult.swift in Sources */, B66A4E4F29C917B8004573B4 /* WelcomeWindow.swift in Sources */, @@ -4171,7 +4200,9 @@ 587B9E7B29301D8F00AC7927 /* GitHubRouter.swift in Sources */, B68108042C60287F008B27C1 /* StartTaskToolbarButton.swift in Sources */, 201169E22837B3D800F92B46 /* SourceControlNavigatorChangesView.swift in Sources */, + B67DBB922CD5EAAB007F4F18 /* GlobPatternList.swift in Sources */, 850C631029D6B01D00E1444C /* SettingsView.swift in Sources */, + B67DBB8A2CD5D8F7007F4F18 /* IgnoredFilesListView.swift in Sources */, 77A01E6D2BC3EA2A00F0EA38 /* NSWindow+Child.swift in Sources */, 77EF6C0D2C60E23400984B69 /* CEWorkspaceFileManager+DirectoryEvents.swift in Sources */, 581550CF29FBD30400684881 /* StandardTableViewCell.swift in Sources */, @@ -4182,6 +4213,7 @@ 587B9E5C29301D8F00AC7927 /* Parameters.swift in Sources */, 61538B932B11201900A88846 /* String+Character.swift in Sources */, 613DF55E2B08DD5D00E9D902 /* FileHelper.swift in Sources */, + B67DBB8C2CD5D9CB007F4F18 /* IgnorePatternModel.swift in Sources */, 58798235292E30B90085B254 /* FeedbackModel.swift in Sources */, 04C3255C2801F86900C8DA2D /* ProjectNavigatorMenu.swift in Sources */, 6CC17B512C43311900834E2C /* ProjectNavigatorViewController+NSOutlineViewDataSource.swift in Sources */, @@ -4270,6 +4302,7 @@ 581550D429FBD37D00684881 /* ProjectNavigatorToolbarBottom.swift in Sources */, 66AF6CE72BF17FFB00D83C9D /* UpdateStatusBarInfo.swift in Sources */, 587B9E7E29301D8F00AC7927 /* GitHubGistRouter.swift in Sources */, + B6E38E022CD3E63A00F4E65A /* GitConfigClient.swift in Sources */, 6C3E12D62CC8388000DD12F1 /* URL+componentCompare.swift in Sources */, B6AB09A52AAAC00F0003A3A6 /* EditorTabBarTrailingAccessories.swift in Sources */, 04BA7C0B2AE2A2D100584E1C /* GitBranch.swift in Sources */, @@ -4332,6 +4365,7 @@ B6E41C7929DE02800088F9F4 /* AccountSelectionView.swift in Sources */, 6C48B5CE2C0C1BE4001E9955 /* Shell.swift in Sources */, 6CA1AE952B46950000378EAB /* EditorInstance.swift in Sources */, + B6B2D79F2CE8794E00379967 /* GitConfigRepresentable.swift in Sources */, 30AB4EBB2BF718A100ED4431 /* DeveloperSettings.swift in Sources */, B6C4F2A92B3CB00100B2B140 /* CommitDetailsHeaderView.swift in Sources */, 30B088012C0D53080063A882 /* LanguageServer+Definition.swift in Sources */, @@ -4422,7 +4456,6 @@ 04BC1CDE2AD9B4B000A83EA5 /* EditorFileTabCloseButton.swift in Sources */, 6C6BD70129CD172700235D17 /* ExtensionsListView.swift in Sources */, 043C321627E3201F006AE443 /* WorkspaceDocument.swift in Sources */, - 58F2EAEC292FB2B0004A9BDE /* IgnoredFiles.swift in Sources */, 6CD03B6A29FC773F001BD1D0 /* SettingsInjector.swift in Sources */, 58798236292E30B90085B254 /* FeedbackType.swift in Sources */, 6CE21E812C643D8F0031B056 /* CETerminalView.swift in Sources */, @@ -4464,7 +4497,6 @@ 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 */, 5882252A292C280D00E83CDE /* StatusBarToggleUtilityAreaButton.swift in Sources */, diff --git a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager+DirectoryEvents.swift b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager+DirectoryEvents.swift index fd1482267..3a81f3488 100644 --- a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager+DirectoryEvents.swift +++ b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager+DirectoryEvents.swift @@ -30,7 +30,7 @@ extension CEWorkspaceFileManager { // Can be ignored for now, these I think not related to tree changes continue case .rootChanged: - // TODO: Handle workspace root changing. + // TODO: #1880 - Handle workspace root changing. continue case .itemCreated, .itemCloned, .itemRemoved, .itemRenamed: for fileItem in fileItems { @@ -48,7 +48,10 @@ extension CEWorkspaceFileManager { self.notifyObservers(updatedItems: files) } - self.handleGitEvents(events: events) + if Settings.shared.preferences.sourceControl.general.sourceControlIsEnabled && + Settings.shared.preferences.sourceControl.general.refreshStatusLocally { + self.handleGitEvents(events: events) + } } } diff --git a/CodeEdit/Features/CodeEditUI/Views/ToolbarBranchPicker.swift b/CodeEdit/Features/CodeEditUI/Views/ToolbarBranchPicker.swift index db50b1b63..b199e7b20 100644 --- a/CodeEdit/Features/CodeEditUI/Views/ToolbarBranchPicker.swift +++ b/CodeEdit/Features/CodeEditUI/Views/ToolbarBranchPicker.swift @@ -71,8 +71,10 @@ struct ToolbarBranchPicker: View { isHovering = active } .onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)) { (_) in - Task { - await sourceControlManager?.refreshCurrentBranch() + if self.currentBranch != nil { + Task { + await sourceControlManager?.refreshCurrentBranch() + } } } .onReceive( @@ -82,8 +84,10 @@ struct ToolbarBranchPicker: View { self.currentBranch = branch } .task { - await self.sourceControlManager?.refreshCurrentBranch() - await self.sourceControlManager?.refreshBranches() + if Settings.shared.preferences.sourceControl.general.sourceControlIsEnabled { + await self.sourceControlManager?.refreshCurrentBranch() + await self.sourceControlManager?.refreshBranches() + } } } diff --git a/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorModel.swift b/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorModel.swift index 728a3d6c0..bb5ff4b8b 100644 --- a/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorModel.swift +++ b/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorModel.swift @@ -40,7 +40,11 @@ final class HistoryInspectorModel: ObservableObject { do { let commitHistory = try await sourceControlManager .gitClient - .getCommitHistory(maxCount: 40, fileLocalPath: fileURL) + .getCommitHistory( + maxCount: 40, + fileLocalPath: fileURL, + showMergeCommits: Settings.shared.preferences.sourceControl.git.showMergeCommitsPerFileLog + ) await setCommitHistory(commitHistory) } catch { await setCommitHistory([]) diff --git a/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorView.swift b/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorView.swift index 9bc871f50..280c00ffb 100644 --- a/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorView.swift +++ b/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorView.swift @@ -7,6 +7,9 @@ import SwiftUI struct HistoryInspectorView: View { + @AppSettings(\.sourceControl.git.showMergeCommitsPerFileLog) + var showMergeCommitsPerFileLog + @EnvironmentObject private var workspace: WorkspaceDocument @EnvironmentObject private var editorManager: EditorManager @@ -60,5 +63,10 @@ struct HistoryInspectorView: View { await model.setWorkspace(sourceControlManager: workspace.sourceControlManager) await model.setFile(url: editorManager.activeEditor.selectedTab?.file.url.path) } + .onChange(of: showMergeCommitsPerFileLog) { _ in + Task { + await model.updateCommitHistory() + } + } } } diff --git a/CodeEdit/Features/InspectorArea/Views/InspectorAreaView.swift b/CodeEdit/Features/InspectorArea/Views/InspectorAreaView.swift index 2baa2f081..aaecbd0a4 100644 --- a/CodeEdit/Features/InspectorArea/Views/InspectorAreaView.swift +++ b/CodeEdit/Features/InspectorArea/Views/InspectorAreaView.swift @@ -1,10 +1,3 @@ -// -// InspectorAreaView.swift -// CodeEdit -// -// Created by Austin Condiff on 3/21/22. -// - import SwiftUI struct InspectorAreaView: View { @@ -15,6 +8,9 @@ struct InspectorAreaView: View { @EnvironmentObject private var editorManager: EditorManager + @AppSettings(\.sourceControl.general.sourceControlIsEnabled) + private var sourceControlIsEnabled: Bool + @AppSettings(\.general.inspectorTabBarPosition) var sidebarPosition: SettingsData.SidebarTabBarPosition @@ -22,19 +18,7 @@ struct InspectorAreaView: View { init(viewModel: InspectorAreaViewModel) { self.viewModel = viewModel - - viewModel.tabItems = [.file, .gitHistory] - viewModel.tabItems += extensionManager - .extensions - .map { ext in - ext.availableFeatures.compactMap { - if case .sidebarItem(let data) = $0, data.kind == .inspector { - return InspectorTab.uiExtension(endpoint: ext.endpoint, data: data) - } - return nil - } - } - .joined() + updateTabItems() } func getExtension(_ id: String) -> ExtensionInfo? { @@ -73,5 +57,28 @@ struct InspectorAreaView: View { .formStyle(.grouped) .accessibilityElement(children: .contain) .accessibilityLabel("inspector") + .onChange(of: sourceControlIsEnabled) { _ in + updateTabItems() + } + } + + private func updateTabItems() { + viewModel.tabItems = [.file] + + (sourceControlIsEnabled ? [.gitHistory] : []) + + extensionManager + .extensions + .flatMap { ext in + ext.availableFeatures.compactMap { + if case .sidebarItem(let data) = $0, data.kind == .inspector { + return InspectorTab.uiExtension(endpoint: ext.endpoint, data: data) + } + return nil + } + } + if let selectedTab = selection, + !viewModel.tabItems.isEmpty && + !viewModel.tabItems.contains(selectedTab) { + selection = viewModel.tabItems[0] + } } } diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/SourceControlNavigatorHistoryView.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/SourceControlNavigatorHistoryView.swift index c3a429923..b615763aa 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/SourceControlNavigatorHistoryView.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/SourceControlNavigatorHistoryView.swift @@ -15,6 +15,9 @@ struct SourceControlNavigatorHistoryView: View { case error(error: Error) } + @AppSettings(\.sourceControl.git.showMergeCommitsPerFileLog) + var showMergeCommitsPerFileLog + @EnvironmentObject var sourceControlManager: SourceControlManager @State var commitHistoryStatus: Status = .loading @@ -28,7 +31,10 @@ struct SourceControlNavigatorHistoryView: View { commitHistoryStatus = .loading let commits = try await sourceControlManager .gitClient - .getCommitHistory(branchName: sourceControlManager.currentBranch?.name) + .getCommitHistory( + branchName: sourceControlManager.currentBranch?.name, + showMergeCommits: Settings.shared.preferences.sourceControl.git.showMergeCommitsPerFileLog + ) await MainActor.run { commitHistory = commits commitHistoryStatus = .ready @@ -102,5 +108,10 @@ struct SourceControlNavigatorHistoryView: View { .task { await updateCommitHistory() } + .onChange(of: showMergeCommitsPerFileLog) { _ in + Task { + await updateCommitHistory() + } + } } } diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/SourceControlNavigatorView.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/SourceControlNavigatorView.swift index 31660395b..31363fc91 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/SourceControlNavigatorView.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/SourceControlNavigatorView.swift @@ -10,6 +10,9 @@ import SwiftUI struct SourceControlNavigatorView: View { @EnvironmentObject private var workspace: WorkspaceDocument + @AppSettings(\.sourceControl.general.fetchRefreshServerStatus) + var fetchRefreshServerStatus + var body: some View { if let sourceControlManager = workspace.workspaceFileManager?.sourceControlManager { VStack(spacing: 0) { @@ -18,7 +21,9 @@ struct SourceControlNavigatorView: View { .task { do { while true { - try await sourceControlManager.fetch() + if fetchRefreshServerStatus { + try await sourceControlManager.fetch() + } try await Task.sleep(for: .seconds(10)) } } catch { diff --git a/CodeEdit/Features/NavigatorArea/Views/NavigatorAreaView.swift b/CodeEdit/Features/NavigatorArea/Views/NavigatorAreaView.swift index 8feffa6ce..5f2e605c8 100644 --- a/CodeEdit/Features/NavigatorArea/Views/NavigatorAreaView.swift +++ b/CodeEdit/Features/NavigatorArea/Views/NavigatorAreaView.swift @@ -15,22 +15,13 @@ struct NavigatorAreaView: View { @AppSettings(\.general.navigatorTabBarPosition) var sidebarPosition: SettingsData.SidebarTabBarPosition + @AppSettings(\.sourceControl.general.sourceControlIsEnabled) + private var sourceControlIsEnabled: Bool + init(workspace: WorkspaceDocument, viewModel: NavigatorSidebarViewModel) { self.workspace = workspace self.viewModel = viewModel - - viewModel.tabItems = [.project, .sourceControl, .search] + - extensionManager - .extensions - .map { ext in - ext.availableFeatures.compactMap { - if case .sidebarItem(let data) = $0, data.kind == .navigator { - return NavigatorTab.uiExtension(endpoint: ext.endpoint, data: data) - } - return nil - } - } - .joined() + updateTabItems() } var body: some View { @@ -63,5 +54,29 @@ struct NavigatorAreaView: View { .environmentObject(workspace) .accessibilityElement(children: .contain) .accessibilityLabel("navigator") + .onChange(of: sourceControlIsEnabled) { _ in + updateTabItems() + } + } + + private func updateTabItems() { + viewModel.tabItems = [.project] + + (sourceControlIsEnabled ? [.sourceControl] : []) + + [.search] + + extensionManager + .extensions + .flatMap { ext in + ext.availableFeatures.compactMap { + if case .sidebarItem(let data) = $0, data.kind == .navigator { + return NavigatorTab.uiExtension(endpoint: ext.endpoint, data: data) + } + return nil + } + } + if let selectedTab = viewModel.selectedTab, + !viewModel.tabItems.isEmpty && + !viewModel.tabItems.contains(selectedTab) { + viewModel.selectedTab = viewModel.tabItems[0] + } } } diff --git a/CodeEdit/Features/Settings/Models/GlobPattern.swift b/CodeEdit/Features/Settings/Models/GlobPattern.swift new file mode 100644 index 000000000..c843b79a6 --- /dev/null +++ b/CodeEdit/Features/Settings/Models/GlobPattern.swift @@ -0,0 +1,16 @@ +// +// GlobPattern.swift +// CodeEdit +// +// Created by Austin Condiff on 11/2/24. +// + +import Foundation + +struct GlobPattern: Identifiable, Hashable, Decodable, Encodable { + /// Ephimeral UUID used to track its representation in the UI + var id = UUID() + + /// The Glob Pattern to render + var value: String +} diff --git a/CodeEdit/Features/Settings/Pages/SearchSettings/Models/SearchSettingsModel.swift b/CodeEdit/Features/Settings/Pages/SearchSettings/Models/SearchSettingsModel.swift index 9bd6cd831..41a798a5d 100644 --- a/CodeEdit/Features/Settings/Pages/SearchSettings/Models/SearchSettingsModel.swift +++ b/CodeEdit/Features/Settings/Pages/SearchSettings/Models/SearchSettingsModel.swift @@ -7,14 +7,6 @@ import SwiftUI -struct GlobPattern: Identifiable, Hashable, Decodable, Encodable { - /// Ephimeral UUID used to track its representation in the UI - var id = UUID() - - /// The Glob Pattern to render - var value: String -} - /// The Search Settings View Model. Accessible via the singleton "``SearchSettings/shared``". /// /// **Usage:** @@ -55,6 +47,9 @@ final class SearchSettingsModel: ObservableObject { baseURL.appendingPathComponent("settings.json", isDirectory: true) } + /// Selected patterns + @Published var selection: Set = [] + /// Stores the new values from the Search Settings Model into the settings.json whenever /// `ignoreGlobPatterns` is updated @Published var ignoreGlobPatterns: [GlobPattern] { @@ -64,4 +59,18 @@ final class SearchSettingsModel: ObservableObject { } } } + + func getPattern(for id: UUID) -> GlobPattern? { + return ignoreGlobPatterns.first(where: { $0.id == id }) + } + + func addPattern() { + ignoreGlobPatterns.append(GlobPattern(value: "")) + } + + func removePatterns(_ selection: Set? = nil) { + let patternsToRemove = selection?.compactMap { getPattern(for: $0) } ?? [] + ignoreGlobPatterns.removeAll { patternsToRemove.contains($0) } + self.selection.removeAll() + } } diff --git a/CodeEdit/Features/Settings/Pages/SearchSettings/SearchSettingsView.swift b/CodeEdit/Features/Settings/Pages/SearchSettings/SearchSettingsView.swift index cebc9f65c..b5c6ceb6f 100644 --- a/CodeEdit/Features/Settings/Pages/SearchSettings/SearchSettingsView.swift +++ b/CodeEdit/Features/Settings/Pages/SearchSettings/SearchSettingsView.swift @@ -24,109 +24,15 @@ struct SearchSettingsView: View { } struct ExcludedGlobPatternList: View { - @ObservedObject private var searchSettingsModel: SearchSettingsModel = .shared - - @FocusState private var focusedField: String? - - @State private var selection: GlobPattern? + @ObservedObject private var model: SearchSettingsModel = .shared var body: some View { - List(selection: $selection) { - ForEach( - Array(searchSettingsModel.ignoreGlobPatterns.enumerated()), - id: \.element - ) { index, ignorePattern in - IgnorePatternListItem( - pattern: $searchSettingsModel.ignoreGlobPatterns[index], - selectedPattern: $selection, - addPattern: addPattern, - removePattern: removePattern, - focusedField: $focusedField, - isLast: searchSettingsModel.ignoreGlobPatterns.count == index+1 - ) - .onAppear { - if ignorePattern.value.isEmpty { - focusedField = ignorePattern.id.uuidString - } - } - } - .onMove { fromOffsets, toOffset in - searchSettingsModel.ignoreGlobPatterns.move(fromOffsets: fromOffsets, toOffset: toOffset) - } - } - .frame(minHeight: 96) - .contextMenu( - forSelectionType: GlobPattern.self, - menu: { selection in - if let pattern = selection.first { - Button("Edit") { - focusedField = pattern.id.uuidString - } - Button("Add") { - addPattern() - } - Divider() - Button("Remove") { - if !searchSettingsModel.ignoreGlobPatterns.isEmpty { - removePattern(pattern) - } - } - } - }, - primaryAction: { selection in - if let pattern = selection.first { - focusedField = pattern.id.uuidString - } - } + GlobPatternList( + patterns: $model.ignoreGlobPatterns, + selection: $model.selection, + addPattern: model.addPattern, + removePatterns: model.removePatterns, + emptyMessage: "No excluded glob patterns" ) - .overlay { - if searchSettingsModel.ignoreGlobPatterns.isEmpty { - Text("No excluded glob patterns") - .foregroundStyle(Color(.secondaryLabelColor)) - } - } - .actionBar { - Button { - addPattern() - } label: { - Image(systemName: "plus") - } - Divider() - Button { - if let pattern = selection { - removePattern(pattern) - } - } label: { - Image(systemName: "minus") - } - .disabled(selection == nil) - } - .onDeleteCommand { - removePattern(selection) - } - } - - func addPattern() { - searchSettingsModel.ignoreGlobPatterns.append(GlobPattern(value: "")) - } - - func removePattern(_ pattern: GlobPattern?) { - let selectedIndex = searchSettingsModel.ignoreGlobPatterns.firstIndex { - $0 == selection - } - - let removeIndex = searchSettingsModel.ignoreGlobPatterns.firstIndex { - $0 == selection - } - - searchSettingsModel.ignoreGlobPatterns.removeAll { - pattern == $0 - } - - if selectedIndex == removeIndex && !searchSettingsModel.ignoreGlobPatterns.isEmpty && selectedIndex != nil { - selection = searchSettingsModel.ignoreGlobPatterns[ - selectedIndex == 0 ? 0 : (selectedIndex ?? 1) - 1 - ] - } } } diff --git a/CodeEdit/Features/Settings/Pages/SourceControlSettings/IgnoredFilesListView.swift b/CodeEdit/Features/Settings/Pages/SourceControlSettings/IgnoredFilesListView.swift new file mode 100644 index 000000000..fd8f230dd --- /dev/null +++ b/CodeEdit/Features/Settings/Pages/SourceControlSettings/IgnoredFilesListView.swift @@ -0,0 +1,22 @@ +// +// IgnoredFilesListView.swift +// CodeEdit +// +// Created by Austin Condiff on 11/1/24. +// + +import SwiftUI + +struct IgnoredFilesListView: View { + @StateObject private var model = IgnorePatternModel() + + var body: some View { + GlobPatternList( + patterns: $model.patterns, + selection: $model.selection, + addPattern: model.addPattern, + removePatterns: model.removePatterns, + emptyMessage: "No ignored files" + ) + } +} diff --git a/CodeEdit/Features/Settings/Pages/SourceControlSettings/Models/IgnorePatternModel.swift b/CodeEdit/Features/Settings/Pages/SourceControlSettings/Models/IgnorePatternModel.swift new file mode 100644 index 000000000..aab221e6c --- /dev/null +++ b/CodeEdit/Features/Settings/Pages/SourceControlSettings/Models/IgnorePatternModel.swift @@ -0,0 +1,283 @@ +// +// IgnorePatternModel.swift +// CodeEdit +// +// Created by Austin Condiff on 11/1/24. +// + +import Foundation + +class IgnorePatternModel: ObservableObject { + @Published var loadingPatterns: Bool = false + @Published var patterns: [GlobPattern] = [] { + didSet { + if !loadingPatterns { + savePatterns() + } else { + loadingPatterns = false + } + } + } + @Published var selection: Set = [] + + private let gitConfig = GitConfigClient(shellClient: currentWorld.shellClient) + private var fileMonitor: DispatchSourceFileSystemObject? + + init() { + Task { + try? await startFileMonitor() + await loadPatterns() + } + } + + deinit { + stopFileMonitor() + } + + private func gitIgnoreURL() async throws -> URL { + let excludesfile = try await gitConfig.get(key: "core.excludesfile") ?? "" + if !excludesfile.isEmpty { + if excludesfile.starts(with: "~/") { + let relativePath = String(excludesfile.dropFirst(2)) // Remove "~/" + return FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent(relativePath) + } else if excludesfile.starts(with: "/") { + return URL(fileURLWithPath: excludesfile) // Absolute path + } else { + return FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent(excludesfile) + } + } else { + let defaultPath = ".gitignore_global" + let fileURL = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent(defaultPath) + await gitConfig.set(key: "core.excludesfile", value: "~/\(defaultPath)", global: true) + return fileURL + } + } + + private func startFileMonitor() async throws { + let fileURL = try await gitIgnoreURL() + let fileDescriptor = open(fileURL.path, O_EVTONLY) + guard fileDescriptor != -1 else { return } + + let source = DispatchSource.makeFileSystemObjectSource( + fileDescriptor: fileDescriptor, + eventMask: .write, + queue: DispatchQueue.main + ) + + source.setEventHandler { + Task { + await self.loadPatterns() + } + } + + source.setCancelHandler { + close(fileDescriptor) + } + + fileMonitor?.cancel() + fileMonitor = source + source.resume() + } + + private func stopFileMonitor() { + fileMonitor?.cancel() + fileMonitor = nil + } + + func loadPatterns() async { + await MainActor.run { loadingPatterns = true } // Ensure `loadingPatterns` is updated on the main thread + + do { + let fileURL = try await gitIgnoreURL() + guard FileManager.default.fileExists(atPath: fileURL.path) else { + await MainActor.run { + patterns = [] + loadingPatterns = false // Update on the main thread + } + return + } + + if let content = try? String(contentsOf: fileURL) { + let parsedPatterns = content.split(separator: "\n") + .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } + .filter { !$0.isEmpty && !$0.starts(with: "#") } + .map { GlobPattern(value: String($0)) } + + await MainActor.run { + patterns = parsedPatterns // Update `patterns` on the main thread + loadingPatterns = false // Ensure `loadingPatterns` is updated on the main thread + } + } else { + await MainActor.run { + patterns = [] + loadingPatterns = false + } + } + } catch { + print("Error loading patterns: \(error)") + await MainActor.run { + patterns = [] + loadingPatterns = false + } + } + } + + func getPattern(for id: UUID) -> GlobPattern? { + return patterns.first(where: { $0.id == id }) + } + + func savePatterns() { + Task { + stopFileMonitor() + defer { Task { try? await startFileMonitor() } } + + do { + let fileURL = try await gitIgnoreURL() + guard let fileContent = try? String(contentsOf: fileURL) else { + writeAllPatterns() + return + } + + let lines = fileContent.split(separator: "\n", omittingEmptySubsequences: false).map(String.init) + let (patternToLineIndex, nonPatternLines) = mapLines(lines) + let globalCommentLines = extractGlobalComments(nonPatternLines, patternToLineIndex) + + var reorderedLines = reorderPatterns(globalCommentLines, patternToLineIndex, nonPatternLines, lines) + + // Ensure single blank line at the end + reorderedLines = cleanUpWhitespace(in: reorderedLines) + + // Write the updated content back to the file + let updatedContent = reorderedLines.joined(separator: "\n") + try updatedContent.write(to: fileURL, atomically: true, encoding: .utf8) + } catch { + print("Error saving patterns: \(error)") + } + } + } + + private func mapLines(_ lines: [String]) -> ([String: Int], [(line: String, index: Int)]) { + var patternToLineIndex: [String: Int] = [:] + var nonPatternLines: [(line: String, index: Int)] = [] + + for (index, line) in lines.enumerated() { + let trimmedLine = line.trimmingCharacters(in: .whitespaces) + if !trimmedLine.isEmpty && !trimmedLine.hasPrefix("#") { + patternToLineIndex[trimmedLine] = index + } else if index != lines.count - 1 { + nonPatternLines.append((line: line, index: index)) + } + } + + return (patternToLineIndex, nonPatternLines) + } + + private func extractGlobalComments( + _ nonPatternLines: [(line: String, index: Int)], + _ patternToLineIndex: [String: Int] + ) -> [String] { + let globalComments = nonPatternLines.filter { $0.index < (patternToLineIndex.values.min() ?? Int.max) } + return globalComments.map(\.line) + } + + private func reorderPatterns( + _ globalCommentLines: [String], + _ patternToLineIndex: [String: Int], + _ nonPatternLines: [(line: String, index: Int)], + _ lines: [String] + ) -> [String] { + var reorderedLines: [String] = globalCommentLines + var usedNonPatternLines = Set() + var usedPatterns = Set() + + for pattern in patterns { + let value = pattern.value + + // Insert the pattern + reorderedLines.append(value) + usedPatterns.insert(value) + + // Preserve associated non-pattern lines + if let currentIndex = patternToLineIndex[value] { + for nextIndex in (currentIndex + 1).. [String] { + var cleanedLines: [String] = [] + var previousLineWasBlank = false + + for line in lines { + let isBlank = line.trimmingCharacters(in: .whitespaces).isEmpty + if !(isBlank && previousLineWasBlank) { + cleanedLines.append(line) + } + previousLineWasBlank = isBlank + } + + // Trim extra blank lines at the end, ensuring only a single blank line + while let lastLine = cleanedLines.last, lastLine.trimmingCharacters(in: .whitespaces).isEmpty { + cleanedLines.removeLast() + } + cleanedLines.append("") // Ensure exactly one blank line at the end + + // Trim whitespace at the top of the file + while let firstLine = cleanedLines.first, firstLine.trimmingCharacters(in: .whitespaces).isEmpty { + cleanedLines.removeFirst() + } + + return cleanedLines + } + + @MainActor + func addPattern() { + patterns.append(GlobPattern(value: "")) + } + + @MainActor + func removePatterns(_ selection: Set? = nil) { + let patternsToRemove = selection?.compactMap { getPattern(for: $0) } ?? [] + patterns.removeAll { patternsToRemove.contains($0) } + self.selection.removeAll() + } +} diff --git a/CodeEdit/Features/Settings/Pages/SourceControlSettings/Models/IgnoredFiles.swift b/CodeEdit/Features/Settings/Pages/SourceControlSettings/Models/IgnoredFiles.swift deleted file mode 100644 index 1895b91d1..000000000 --- a/CodeEdit/Features/Settings/Pages/SourceControlSettings/Models/IgnoredFiles.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// IgnoredFiles.swift -// CodeEditModules/Settings -// -// Created by Nanashi Li on 2022/04/08. -// - -import Foundation - -struct IgnoredFiles: Codable, Identifiable, Hashable { - var id: String - var name: String -} diff --git a/CodeEdit/Features/Settings/Pages/SourceControlSettings/Models/SourceControlSettings.swift b/CodeEdit/Features/Settings/Pages/SourceControlSettings/Models/SourceControlSettings.swift index 097825782..309bdf029 100644 --- a/CodeEdit/Features/Settings/Pages/SourceControlSettings/Models/SourceControlSettings.swift +++ b/CodeEdit/Features/Settings/Pages/SourceControlSettings/Models/SourceControlSettings.swift @@ -52,33 +52,34 @@ extension SettingsData { struct SourceControlGeneral: Codable, Hashable { /// Indicates whether or not the source control is active - var enableSourceControl: Bool = true - /// Indicates whether or not we should include the upstream changes - var refreshStatusLocally: Bool = false - /// Indicates whether or not we should include the upstream changes - var fetchRefreshServerStatus: Bool = false - /// Indicates whether or not we should include the upstream changes - var addRemoveAutomatically: Bool = false - /// Indicates whether or not we should include the upstream changes - var selectFilesToCommit: Bool = false + var sourceControlIsEnabled: Bool = true + /// Indicates whether the status should be refreshed locally without fetching updates from the server. + var refreshStatusLocally: Bool = true + /// Indicates whether the application should automatically fetch updates from the server and refresh the status. + var fetchRefreshServerStatus: Bool = true + /// Indicates whether new and deleted files should be automatically staged for commit. + var addRemoveAutomatically: Bool = true + /// Indicates whether the application should automatically select files to commit. + var selectFilesToCommit: Bool = true /// Indicates whether or not to show the source control changes var showSourceControlChanges: Bool = true /// Indicates whether or not we should include the upstream - var includeUpstreamChanges: Bool = false + var includeUpstreamChanges: Bool = true /// Indicates whether or not we should open the reported feedback in the browser var openFeedbackInBrowser: Bool = true /// The selected value of the comparison view var revisionComparisonLayout: RevisionComparisonLayout = .localLeft /// The selected value of the control navigator var controlNavigatorOrder: ControlNavigatorOrder = .sortByName - /// The name of the default branch - var defaultBranchName: String = "main" /// Default initializer init() {} /// Explicit decoder init for setting default values when key is not present in `JSON` init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - self.enableSourceControl = try container.decodeIfPresent(Bool.self, forKey: .enableSourceControl) ?? true + self.sourceControlIsEnabled = try container.decodeIfPresent( + Bool.self, + forKey: .sourceControlIsEnabled + ) ?? true self.refreshStatusLocally = try container.decodeIfPresent(Bool.self, forKey: .refreshStatusLocally) ?? true self.fetchRefreshServerStatus = try container.decodeIfPresent( Bool.self, @@ -109,7 +110,6 @@ extension SettingsData { ControlNavigatorOrder.self, forKey: .controlNavigatorOrder ) ?? .sortByName - self.defaultBranchName = try container.decodeIfPresent(String.self, forKey: .defaultBranchName) ?? "main" } } @@ -130,31 +130,13 @@ extension SettingsData { } struct SourceControlGit: Codable, Hashable { - /// The author name - var authorName: String = "" - /// The author email - var authorEmail: String = "" - /// Indicates what files should be ignored when committing - var ignoredFiles: [IgnoredFiles] = [] /// Indicates whether we should rebase when pulling commits - var preferRebaseWhenPulling: Bool = false - /// Indicates whether we should show commits per file log var showMergeCommitsPerFileLog: Bool = false /// Default initializer init() {} /// Explicit decoder init for setting default values when key is not present in `JSON` init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - self.authorName = try container.decodeIfPresent(String.self, forKey: .authorName) ?? "" - self.authorEmail = try container.decodeIfPresent(String.self, forKey: .authorEmail) ?? "" - self.ignoredFiles = try container.decodeIfPresent( - [IgnoredFiles].self, - forKey: .ignoredFiles - ) ?? [] - self.preferRebaseWhenPulling = try container.decodeIfPresent( - Bool.self, - forKey: .preferRebaseWhenPulling - ) ?? false self.showMergeCommitsPerFileLog = try container.decodeIfPresent( Bool.self, forKey: .showMergeCommitsPerFileLog diff --git a/CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlGeneralView.swift b/CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlGeneralView.swift index 2ef8c3b94..652094d7e 100644 --- a/CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlGeneralView.swift +++ b/CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlGeneralView.swift @@ -11,38 +11,29 @@ struct SourceControlGeneralView: View { @AppSettings(\.sourceControl.general) var settings - @State private var text: String = "main" + let gitConfig = GitConfigClient(shellClient: currentWorld.shellClient) var body: some View { - SettingsForm { - Section { - enableSourceControl + Group { + Section("Source Control") { refreshLocalStatusAuto fetchRefreshStatusAuto addRemoveFilesAuto selectFilesToCommitAuto } - Section { + Section("Text Editing") { showSourceControlChanges includeUpstreamChanges } Section { comparisonView sourceControlNavigator - defaultBranchName } } } } private extension SourceControlGeneralView { - private var enableSourceControl: some View { - Toggle( - "Enable source control", - isOn: $settings.enableSourceControl - ) - } - private var refreshLocalStatusAuto: some View { Toggle( "Refresh local status automatically", @@ -83,6 +74,7 @@ private extension SourceControlGeneralView { "Include upstream changes", isOn: $settings.includeUpstreamChanges ) + .disabled(!settings.showSourceControlChanges) } private var comparisonView: some View { @@ -108,11 +100,4 @@ private extension SourceControlGeneralView { .tag(SettingsData.ControlNavigatorOrder.sortByDate) } } - - private var defaultBranchName: some View { - TextField(text: $text) { - Text("Default branch name") - Text("Cannot contain spaces, backslashes, or other symbols") - } - } } diff --git a/CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlGitView.swift b/CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlGitView.swift index 504da5766..ce3c87d3b 100644 --- a/CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlGitView.swift +++ b/CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlGitView.swift @@ -11,36 +11,119 @@ struct SourceControlGitView: View { @AppSettings(\.sourceControl.git) var git - @State var ignoredFileSelection: IgnoredFiles.ID? + let gitConfig = GitConfigClient(shellClient: currentWorld.shellClient) + + @State private var authorName: String = "" + @State private var authorEmail: String = "" + @State private var defaultBranch: String = "" + @State private var preferRebaseWhenPulling: Bool = false + @State private var hasAppeared: Bool = false + @State private var resolvedGitIgnorePath: String = "~/.gitignore_global" var body: some View { - SettingsForm { + Group { Section { gitAuthorName gitEmail + } header: { + Text("Git Configuration") + Text(""" + Applied globally to all repositories on your Mac. \ + [Learn more...](https://git-scm.com/docs/git-config) + """) } Section { + defaultBranchName preferToRebaseWhenPulling showMergeCommitsInPerFileLog } + Section { + gitConfigEditor + } + Section { + IgnoredFilesListView() + } header: { + Text("Ignored Files") + Text(""" + Patterns for files and folders that Git should ignore and not track. \ + Applied globally to all repositories on your Mac. \ + [Learn more...](https://git-scm.com/docs/gitignore) + """) + } + Section { + gitIgnoreEditor + } + } + .onAppear { + Task { + authorName = try await gitConfig.get(key: "user.name", global: true) ?? "" + authorEmail = try await gitConfig.get(key: "user.email", global: true) ?? "" + defaultBranch = try await gitConfig.get(key: "init.defaultBranch", global: true) ?? "" + preferRebaseWhenPulling = try await gitConfig.get(key: "pull.rebase", global: true) ?? false + try? await Task.sleep(for: .milliseconds(0)) + hasAppeared = true + } } } } private extension SourceControlGitView { private var gitAuthorName: some View { - TextField("Author Name", text: $git.authorName) + TextField("Author Name", text: $authorName) + .onChange(of: authorName) { newValue in + if hasAppeared { + Limiter.debounce(id: "authorNameDebouncer", duration: 0.5) { + Task { + await gitConfig.set(key: "user.name", value: newValue, global: true) + } + } + } + } } private var gitEmail: some View { - TextField("Author Email", text: $git.authorEmail) + TextField("Author Email", text: $authorEmail) + .onChange(of: authorEmail) { newValue in + if hasAppeared { + Limiter.debounce(id: "authorEmailDebouncer", duration: 0.5) { + Task { + await gitConfig.set(key: "user.email", value: newValue, global: true) + } + } + } + } + } + + private var defaultBranchName: some View { + TextField(text: $defaultBranch) { + Text("Default branch name") + Text("Cannot contain spaces, backslashes, or other symbols") + } + .onChange(of: defaultBranch) { newValue in + if hasAppeared { + Limiter.debounce(id: "defaultBranchDebouncer", duration: 0.5) { + Task { + await gitConfig.set(key: "init.defaultBranch", value: newValue, global: true) + } + } + } + } } private var preferToRebaseWhenPulling: some View { Toggle( "Prefer to rebase when pulling", - isOn: $git.preferRebaseWhenPulling + isOn: $preferRebaseWhenPulling ) + .onChange(of: preferRebaseWhenPulling) { newValue in + if hasAppeared { + Limiter.debounce(id: "pullRebaseDebouncer", duration: 0.5) { + Task { + await gitConfig.set(key: "pull.rebase", value: newValue, global: true) + } + } + } + } } private var showMergeCommitsInPerFileLog: some View { @@ -50,19 +133,98 @@ private extension SourceControlGitView { ) } - private var bottomToolbar: some View { - HStack(spacing: 12) { - Button {} label: { - Image(systemName: "plus") - .foregroundColor(Color.secondary) + private var gitConfigEditor: some View { + HStack { + Text("Git configuration is stored in \"~/.gitconfig\".") + .font(.subheadline) + .foregroundStyle(.secondary) + .frame(maxWidth: .infinity, alignment: .leading) + Button("Open in Editor...", action: openGitConfigFile) + } + .frame(maxWidth: .infinity) + } + + private var gitIgnoreEditor: some View { + HStack { + Text("Ignored file patterns are stored in \"\(resolvedGitIgnorePath)\".") + .font(.subheadline) + .foregroundStyle(.secondary) + .frame(maxWidth: .infinity, alignment: .leading) + Button("Open in Editor...", action: openGitIgnoreFile) + } + .frame(maxWidth: .infinity) + .onAppear { + Task { + resolvedGitIgnorePath = await gitIgnorePath() + } + } + } + + private var gitIgnoreURL: URL { + get async throws { + if let excludesfile: String = try await gitConfig.get( + key: "core.excludesfile", + global: true + ), !excludesfile.isEmpty { + if excludesfile.starts(with: "~/") { + let relativePath = String(excludesfile.dropFirst(2)) + return FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent(relativePath) + } else if excludesfile.starts(with: "/") { + return URL(fileURLWithPath: excludesfile) + } else { + return FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent(excludesfile) + } + } else { + let defaultURL = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent( + ".gitignore_global" + ) + await gitConfig.set(key: "core.excludesfile", value: "~/\(defaultURL.lastPathComponent)", global: true) + return defaultURL + } + } + } + + private func gitIgnorePath() async -> String { + do { + let url = try await gitIgnoreURL + return url.path.replacingOccurrences(of: FileManager.default.homeDirectoryForCurrentUser.path, with: "~") + } catch { + return "~/.gitignore_global" + } + } + + private func openGitConfigFile() { + let fileURL = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent(".gitconfig") + + if !FileManager.default.fileExists(atPath: fileURL.path) { + FileManager.default.createFile(atPath: fileURL.path, contents: nil) + } + + NSDocumentController.shared.openDocument( + withContentsOf: fileURL, + display: true + ) { _, _, error in + if let error = error { + print("Failed to open document: \(error.localizedDescription)") } - .buttonStyle(.plain) - Button {} label: { - Image(systemName: "minus") + } + } + + private func openGitIgnoreFile() { + Task { + do { + let fileURL = try await gitIgnoreURL + + // Ensure the file exists + if !FileManager.default.fileExists(atPath: fileURL.path) { + FileManager.default.createFile(atPath: fileURL.path, contents: nil) + } + + // Open the file in the editor + try await NSDocumentController.shared.openDocument(withContentsOf: fileURL, display: true) + } catch { + print("Failed to open document: \(error.localizedDescription)") } - .disabled(true) - .buttonStyle(.plain) - Spacer() } } } diff --git a/CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlSettingsView.swift b/CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlSettingsView.swift index 120ead4a1..22f0304ca 100644 --- a/CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlSettingsView.swift +++ b/CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlSettingsView.swift @@ -8,29 +8,55 @@ import SwiftUI struct SourceControlSettingsView: View { + @AppSettings(\.sourceControl.general) + var settings + @State var selectedTab: String = "general" var body: some View { - Group { - switch selectedTab { - case "general": - SourceControlGeneralView() - case "git": - SourceControlGitView() - default: - SourceControlGeneralView() + SettingsForm { + Section { + sourceControlIsEnabled + } footer: { + if settings.sourceControlIsEnabled { + Picker("", selection: $selectedTab) { + Text("General").tag("general") + Text("Git").tag("git") + } + .pickerStyle(.segmented) + .labelsHidden() + .padding(.top, 10) + } } - } - .safeAreaInset(edge: .top, spacing: 0) { - Picker("", selection: $selectedTab) { - Text("General").tag("general") - Text("Git").tag("git") + if settings.sourceControlIsEnabled { + switch selectedTab { + case "general": + SourceControlGeneralView() + case "git": + SourceControlGitView() + default: + SourceControlGeneralView() + } } - .pickerStyle(.segmented) - .labelsHidden() - .padding(.horizontal, 20) - .padding(.bottom, 20) + } + } + private var sourceControlIsEnabled: some View { + Toggle( + isOn: $settings.sourceControlIsEnabled + ) { + Label { + Text("Source Control") + Text(""" + Back up your files, collaborate with others, and tag your releases. \ + [Learn more...](https://developer.apple.com/documentation/xcode/source-control-management) + """) + .font(.callout) + } icon: { + FeatureIcon(symbol: Image(symbol: "vault"), color: Color(.systemBlue), size: 26) + } } + .controlSize(.large) } + } diff --git a/CodeEdit/Features/Settings/Views/FeatureIcon.swift b/CodeEdit/Features/Settings/Views/FeatureIcon.swift new file mode 100644 index 000000000..b7ca98e4e --- /dev/null +++ b/CodeEdit/Features/Settings/Views/FeatureIcon.swift @@ -0,0 +1,44 @@ +// +// FeatureIcon.swift +// CodeEdit +// +// Created by Austin Condiff on 12/2/24. +// + +import SwiftUI + +struct FeatureIcon: View { + private let symbol: Image + private let color: Color + private let size: CGFloat + + init( + symbol: Image?, + color: Color?, + size: CGFloat? + ) { + self.symbol = symbol ?? Image(systemName: "exclamationmark.triangle") + self.color = color ?? .white + self.size = size ?? 20 + } + + var body: some View { + Group { + symbol + .resizable() + .aspectRatio(contentMode: .fit) + } + .shadow(color: Color(NSColor.black).opacity(0.25), radius: size / 40, y: size / 40) + .padding(size / 8) + .foregroundColor(.white) + .frame(width: size, height: size) + .background( + RoundedRectangle( + cornerRadius: size / 4, + style: .continuous + ) + .fill(color.gradient) + .shadow(color: Color(NSColor.black).opacity(0.25), radius: size / 40, y: size / 40) + ) + } +} diff --git a/CodeEdit/Features/Settings/Views/GlobPatternList.swift b/CodeEdit/Features/Settings/Views/GlobPatternList.swift new file mode 100644 index 000000000..80f216a4b --- /dev/null +++ b/CodeEdit/Features/Settings/Views/GlobPatternList.swift @@ -0,0 +1,86 @@ +// +// GlobPatternList.swift +// CodeEdit +// +// Created by Austin Condiff on 11/2/24. +// + +import SwiftUI + +struct GlobPatternList: View { + @Binding var patterns: [GlobPattern] + @Binding var selection: Set + let addPattern: () -> Void + let removePatterns: (_ selection: Set?) -> Void + let emptyMessage: String + + @FocusState private var focusedField: String? + + var body: some View { + List(selection: $selection) { + ForEach(Array(patterns.enumerated()), id: \.element.id) { index, pattern in + GlobPatternListItem( + pattern: $patterns[index], + selection: $selection, + addPattern: addPattern, + removePatterns: removePatterns, + focusedField: $focusedField, + isLast: patterns.count == index + 1 + ) + .onAppear { + if pattern.value.isEmpty { + focusedField = pattern.id.uuidString + } + } + } + .onMove { fromOffsets, toOffset in + patterns.move(fromOffsets: fromOffsets, toOffset: toOffset) + } + .onDelete { indexSet in + let patternIDs = indexSet.compactMap { patterns[$0].id } + removePatterns(Set(patternIDs)) + } + } + .frame(minHeight: 96) + .contextMenu(forSelectionType: UUID.self, menu: { selection in + if let patternID = selection.first, let pattern = patterns.first(where: { $0.id == patternID }) { + Button("Edit") { + focusedField = pattern.id.uuidString + } + Button("Add") { + addPattern() + } + Divider() + Button("Remove") { + removePatterns(selection) + } + } + }, primaryAction: { selection in + if let patternID = selection.first, let pattern = patterns.first(where: { $0.id == patternID }) { + focusedField = pattern.id.uuidString + } + }) + .overlay { + if patterns.isEmpty { + Text(emptyMessage) + .foregroundStyle(Color(.secondaryLabelColor)) + } + } + .actionBar { + Button(action: addPattern) { + Image(systemName: "plus") + } + Divider() + Button { + removePatterns(selection) + } label: { + Image(systemName: "minus") + .opacity(selection.isEmpty ? 0.5 : 1) + } + .disabled(selection.isEmpty) + } + .onDeleteCommand { + removePatterns(selection) + } + } +} diff --git a/CodeEdit/Features/Settings/Pages/SearchSettings/IgnorePatternListItemView.swift b/CodeEdit/Features/Settings/Views/GlobPatternListItem.swift similarity index 61% rename from CodeEdit/Features/Settings/Pages/SearchSettings/IgnorePatternListItemView.swift rename to CodeEdit/Features/Settings/Views/GlobPatternListItem.swift index 0ba3f4ffd..40e97a2da 100644 --- a/CodeEdit/Features/Settings/Pages/SearchSettings/IgnorePatternListItemView.swift +++ b/CodeEdit/Features/Settings/Views/GlobPatternListItem.swift @@ -1,5 +1,5 @@ // -// IgnorePatternListItemView.swift +// GlobPatternListItem.swift // CodeEdit // // Created by Esteban on 2/2/24. @@ -7,11 +7,11 @@ import SwiftUI -struct IgnorePatternListItem: View { +struct GlobPatternListItem: View { @Binding var pattern: GlobPattern - @Binding var selectedPattern: GlobPattern? + @Binding var selection: Set var addPattern: () -> Void - var removePattern: (GlobPattern) -> Void + var removePatterns: (_ selection: Set?) -> Void var focusedField: FocusState.Binding var isLast: Bool @@ -21,18 +21,19 @@ struct IgnorePatternListItem: View { init( pattern: Binding, - selectedPattern: Binding, + selection: Binding>, addPattern: @escaping () -> Void, - removePattern: @escaping (GlobPattern) -> Void, + removePatterns: @escaping (_ selection: Set?) -> Void, focusedField: FocusState.Binding, isLast: Bool ) { self._pattern = pattern - self._selectedPattern = selectedPattern + self._selection = selection self.addPattern = addPattern - self.removePattern = removePattern + self.removePatterns = removePatterns self.focusedField = focusedField self.isLast = isLast + self._value = State(initialValue: pattern.wrappedValue.value) } @@ -44,21 +45,21 @@ struct IgnorePatternListItem: View { .autocorrectionDisabled() .labelsHidden() .onSubmit { - if !value.isEmpty && isLast { - addPattern() + if !value.isEmpty { + if isLast { + addPattern() + } } } .onChange(of: isFocused) { newIsFocused in if newIsFocused { - if selectedPattern != pattern { - selectedPattern = pattern - } - } else { - if value.isEmpty { - removePattern(pattern) - } else { - pattern.value = value + if !selection.contains(pattern.id) { + selection = [pattern.id] } + } else if value.isEmpty { + removePatterns(selection) + } else if pattern.value != value { + pattern.value = value } } } diff --git a/CodeEdit/Features/Settings/Views/SettingsPageView.swift b/CodeEdit/Features/Settings/Views/SettingsPageView.swift index a8f2fa011..e1c96460b 100644 --- a/CodeEdit/Features/Settings/Views/SettingsPageView.swift +++ b/CodeEdit/Features/Settings/Views/SettingsPageView.swift @@ -16,41 +16,25 @@ struct SettingsPageView: View { self.searchText = searchText } + var symbol: Image? { + switch page.icon { + case .system(let name): + Image(systemName: name) + case .symbol(let name): + Image(symbol: name) + case .asset(let name): + Image(name) + case .none: nil + } + } + var body: some View { NavigationLink(value: page) { Label { page.name.rawValue.highlightOccurrences(self.searchText) .padding(.leading, 2) } icon: { - Group { - switch page.icon { - case .system(let name): - Image(systemName: name) - .resizable() - .aspectRatio(contentMode: .fit) - case .symbol(let name): - Image(symbol: name) - .resizable() - .aspectRatio(contentMode: .fit) - case .asset(let name): - Image(name) - .resizable() - .aspectRatio(contentMode: .fit) - case .none: EmptyView() - } - } - .shadow(color: Color(NSColor.black).opacity(0.25), radius: 0.5, y: 0.5) - .padding(2.5) - .foregroundColor(.white) - .frame(width: 20, height: 20) - .background( - RoundedRectangle( - cornerRadius: 5, - style: .continuous - ) - .fill((page.baseColor ?? .white).gradient) - .shadow(color: Color(NSColor.black).opacity(0.25), radius: 0.5, y: 0.5) - ) + FeatureIcon(symbol: symbol, color: page.baseColor, size: 20) } } } diff --git a/CodeEdit/Features/Settings/Views/View+NavigationBarBackButtonVisible.swift b/CodeEdit/Features/Settings/Views/View+NavigationBarBackButtonVisible.swift index f778e0e79..325a0b123 100644 --- a/CodeEdit/Features/Settings/Views/View+NavigationBarBackButtonVisible.swift +++ b/CodeEdit/Features/Settings/Views/View+NavigationBarBackButtonVisible.swift @@ -21,6 +21,7 @@ struct NavigationBarBackButtonVisible: ViewModifier { self.presentationMode.wrappedValue.dismiss() } label: { Image(systemName: "chevron.left") + .frame(width: 23) } } } diff --git a/CodeEdit/Features/SourceControl/Client/GitClient+CommitHistory.swift b/CodeEdit/Features/SourceControl/Client/GitClient+CommitHistory.swift index 574e65034..6245fe773 100644 --- a/CodeEdit/Features/SourceControl/Client/GitClient+CommitHistory.swift +++ b/CodeEdit/Features/SourceControl/Client/GitClient+CommitHistory.swift @@ -17,7 +17,8 @@ extension GitClient { func getCommitHistory( branchName: String? = nil, maxCount: Int? = nil, - fileLocalPath: String? = nil + fileLocalPath: String? = nil, + showMergeCommits: Bool = false ) async throws -> [GitCommit] { let branchString = branchName != nil ? "\"\(branchName ?? "")\"" : "" let fileString = fileLocalPath != nil ? "\"\(fileLocalPath ?? "")\"" : "" @@ -30,8 +31,11 @@ extension GitClient { dateFormatter.dateFormat = "EEE, dd MMM yyyy HH:mm:ss Z" let output = try await run( - "log -z --pretty=%h¦%H¦%s¦%aN¦%ae¦%cn¦%ce¦%aD¦%b¦%D¦ \(countString) \(branchString) -- \(fileString)" - .trimmingCharacters(in: .whitespacesAndNewlines) + """ + log \(showMergeCommits ? "" : "--no-merges") -z \ + --pretty=%h¦%H¦%s¦%aN¦%ae¦%cn¦%ce¦%aD¦%b¦%D¦ \ + \(countString) \(branchString) -- \(fileString) + """.trimmingCharacters(in: .whitespacesAndNewlines) ) let remoteURL = try await getRemoteURL() diff --git a/CodeEdit/Features/SourceControl/Client/GitClient+Pull.swift b/CodeEdit/Features/SourceControl/Client/GitClient+Pull.swift index 3954feba7..d5f8c9ca6 100644 --- a/CodeEdit/Features/SourceControl/Client/GitClient+Pull.swift +++ b/CodeEdit/Features/SourceControl/Client/GitClient+Pull.swift @@ -10,16 +10,12 @@ import Foundation extension GitClient { /// Pull changes from remote func pullFromRemote(remote: String? = nil, branch: String? = nil, rebase: Bool = false) async throws { - var command = "pull" + var command = "pull \(rebase ? "--rebase" : "--no-rebase")" if let remote = remote, let branch = branch { command += " \(remote) \(branch)" } - if rebase { - command += " --rebase" - } - _ = try await self.run(command) } } diff --git a/CodeEdit/Features/SourceControl/Client/GitClient.swift b/CodeEdit/Features/SourceControl/Client/GitClient.swift index 6dcf210c7..f221d6a16 100644 --- a/CodeEdit/Features/SourceControl/Client/GitClient.swift +++ b/CodeEdit/Features/SourceControl/Client/GitClient.swift @@ -38,9 +38,20 @@ class GitClient { internal let directoryURL: URL internal let shellClient: ShellClient + private let configClient: GitConfigClient + init(directoryURL: URL, shellClient: ShellClient) { self.directoryURL = directoryURL self.shellClient = shellClient + self.configClient = GitConfigClient(projectURL: directoryURL, shellClient: shellClient) + } + + func getConfig(key: String) async throws -> T? { + return try await configClient.get(key: key, global: false) + } + + func setConfig(key: String, value: T) async { + await configClient.set(key: key, value: value, global: false) } /// Runs a git command, it will prepend the command with `cd ;git`, diff --git a/CodeEdit/Features/SourceControl/Client/GitConfigClient.swift b/CodeEdit/Features/SourceControl/Client/GitConfigClient.swift new file mode 100644 index 000000000..6928ba60a --- /dev/null +++ b/CodeEdit/Features/SourceControl/Client/GitConfigClient.swift @@ -0,0 +1,78 @@ +// +// GitConfigClient.swift +// CodeEdit +// +// Created by Austin Condiff on 10/31/24. +// + +import Foundation + +/// A client for managing Git configuration settings. +/// Provides methods to read and write Git configuration values at both +/// project and global levels. +class GitConfigClient { + private let projectURL: URL? + private let shellClient: ShellClient + + /// Initializes a new GitConfigClient. + /// - Parameters: + /// - projectURL: The project directory URL (if any). + /// - shellClient: The client responsible for executing shell commands. + init(projectURL: URL? = nil, shellClient: ShellClient) { + self.projectURL = projectURL + self.shellClient = shellClient + } + + /// Runs a Git configuration command. + /// - Parameters: + /// - command: The Git command to execute. + /// - global: Whether to apply the command globally or locally. + /// - Returns: The command output as a string. + private func runConfigCommand(_ command: String, global: Bool) async throws -> String { + var fullCommand = "git config" + + if global { + fullCommand += " --global" + } else if let projectURL = projectURL { + fullCommand = "cd \(projectURL.relativePath.escapedWhiteSpaces()); " + fullCommand + } + + fullCommand += " \(command)" + return try shellClient.run(fullCommand) + } + + /// Retrieves a Git configuration value. + /// - Parameters: + /// - key: The configuration key to retrieve. + /// - global: Whether to retrieve the value globally or locally. + /// - Returns: The value as a type conforming to `GitConfigRepresentable`, or `nil` if not found. + func get(key: String, global: Bool = false) async throws -> T? { + let output = try await runConfigCommand(key, global: global) + let trimmedOutput = output.trimmingCharacters(in: .whitespacesAndNewlines) + return T(configValue: trimmedOutput) + } + + /// Sets a Git configuration value. + /// - Parameters: + /// - key: The configuration key to set. + /// - value: The value to set, conforming to `GitConfigRepresentable`. + /// - global: Whether to set the value globally or locally. + func set(key: String, value: T, global: Bool = false) async { + let shouldUnset: Bool + if let boolValue = value as? Bool { + shouldUnset = !boolValue + } else if let stringValue = value as? String { + shouldUnset = stringValue.isEmpty + } else { + shouldUnset = false + } + + let commandString = shouldUnset ? "--unset \(key)" : "\(key) \(value.asConfigValue)" + + do { + _ = try await runConfigCommand(commandString, global: global) + } catch { + print("Failed to set \(key): \(error)") + } + } +} diff --git a/CodeEdit/Features/SourceControl/Client/GitConfigExtensions.swift b/CodeEdit/Features/SourceControl/Client/GitConfigExtensions.swift new file mode 100644 index 000000000..c3a98de0e --- /dev/null +++ b/CodeEdit/Features/SourceControl/Client/GitConfigExtensions.swift @@ -0,0 +1,40 @@ +// +// GitConfigExtensions.swift +// CodeEdit +// +// Created by Austin Condiff on 11/16/24. +// + +import Foundation + +/// Conformance of `Bool` to `GitConfigRepresentable` +/// +/// This enables `Bool` values to be represented in Git configuration as +/// `true` or `false`. +extension Bool: GitConfigRepresentable { + public init?(configValue: String) { + switch configValue.lowercased() { + case "true": self = true + case "false": self = false + default: return nil + } + } + + public var asConfigValue: String { + self ? "true" : "false" + } +} + +/// Conformance of `String` to `GitConfigRepresentable` +/// +/// This enables `String` values to be represented in Git configuration, +/// automatically escaping them with quotes. +extension String: GitConfigRepresentable { + public init?(configValue: String) { + self = configValue + } + + public var asConfigValue: String { + "\"\(self)\"" + } +} diff --git a/CodeEdit/Features/SourceControl/Client/GitConfigRepresentable.swift b/CodeEdit/Features/SourceControl/Client/GitConfigRepresentable.swift new file mode 100644 index 000000000..1eb55f502 --- /dev/null +++ b/CodeEdit/Features/SourceControl/Client/GitConfigRepresentable.swift @@ -0,0 +1,19 @@ +// +// GitConfigRepresentable.swift +// CodeEdit +// +// Created by Austin Condiff on 11/16/24. +// + +/// A protocol that provides a mechanism to represent and parse Git configuration values. +/// +/// Conforming types must be able to initialize from a Git configuration string +/// and convert their value back to a Git-compatible string representation. +protocol GitConfigRepresentable { + /// Initializes a new instance from a Git configuration value string. + /// - Parameter configValue: The configuration value string. + init?(configValue: String) + + /// Converts the value to a Git-compatible configuration string. + var asConfigValue: String { get } +} diff --git a/CodeEdit/Features/SourceControl/Views/SourceControlPullView.swift b/CodeEdit/Features/SourceControl/Views/SourceControlPullView.swift index 678863127..45d0a8f97 100644 --- a/CodeEdit/Features/SourceControl/Views/SourceControlPullView.swift +++ b/CodeEdit/Features/SourceControl/Views/SourceControlPullView.swift @@ -13,8 +13,12 @@ struct SourceControlPullView: View { @EnvironmentObject var sourceControlManager: SourceControlManager + let gitConfig = GitConfigClient(shellClient: currentWorld.shellClient) + @State var loading: Bool = false + @State var preferRebaseWhenPulling: Bool = false + var body: some View { VStack(spacing: 0) { Form { @@ -35,6 +39,14 @@ struct SourceControlPullView: View { .formStyle(.grouped) .scrollDisabled(true) .scrollContentBackground(.hidden) + .onAppear { + Task { + preferRebaseWhenPulling = try await gitConfig.get(key: "pull.rebase", global: true) ?? false + if preferRebaseWhenPulling { + sourceControlManager.operationRebase = true + } + } + } HStack { if loading { HStack(spacing: 7.5) { diff --git a/CodeEdit/Features/WindowCommands/CodeEditCommands.swift b/CodeEdit/Features/WindowCommands/CodeEditCommands.swift index c081201a2..ef5714559 100644 --- a/CodeEdit/Features/WindowCommands/CodeEditCommands.swift +++ b/CodeEdit/Features/WindowCommands/CodeEditCommands.swift @@ -8,13 +8,16 @@ import SwiftUI struct CodeEditCommands: Commands { + @AppSettings(\.sourceControl.general.sourceControlIsEnabled) + private var sourceControlIsEnabled + var body: some Commands { MainCommands() FileCommands() ViewCommands() FindCommands() NavigateCommands() - SourceControlCommands() + if sourceControlIsEnabled { SourceControlCommands() } ExtensionCommands() WindowCommands() HelpCommands() diff --git a/CodeEdit/Utils/Limiter.swift b/CodeEdit/Utils/Limiter.swift new file mode 100644 index 000000000..154a82757 --- /dev/null +++ b/CodeEdit/Utils/Limiter.swift @@ -0,0 +1,51 @@ +// +// Limiter.swift +// CodeEdit +// +// Created by Austin Condiff on 11/1/24. +// + +import Combine +import Foundation + +// TODO: Look into improving this API by using async by default so `Task` isn't needed when used. +enum Limiter { + // Keep track of debounce timers and throttle states + private static var debounceTimers: [AnyHashable: AnyCancellable] = [:] + private static var throttleLastExecution: [AnyHashable: Date] = [:] + + /// Debounces an action with a specified duration and identifier. + /// - Parameters: + /// - id: A unique identifier for the debounced action. + /// - duration: The debounce duration in seconds. + /// - action: The action to be executed after the debounce period. + static func debounce(id: AnyHashable, duration: TimeInterval, action: @escaping () -> Void) { + // Cancel any existing debounce timer for the given ID + debounceTimers[id]?.cancel() + + // Start a new debounce timer for the given ID + debounceTimers[id] = Timer.publish(every: duration, on: .main, in: .common) + .autoconnect() + .first() + .sink { _ in + action() + debounceTimers[id] = nil + } + } + + /// Throttles an action with a specified duration and identifier. + /// - Parameters: + /// - id: A unique identifier for the throttled action. + /// - duration: The throttle duration in seconds. + /// - action: The action to be executed after the throttle period. + static func throttle(id: AnyHashable, duration: TimeInterval, action: @escaping () -> Void) { + // Check the time of the last execution for the given ID + if let lastExecution = throttleLastExecution[id], Date().timeIntervalSince(lastExecution) < duration { + return // Skip this call if it's within the throttle duration + } + + // Update the last execution time and perform the action + throttleLastExecution[id] = Date() + action() + } +} diff --git a/CodeEdit/WorkspaceView.swift b/CodeEdit/WorkspaceView.swift index 8e3c9f224..6782a8cf6 100644 --- a/CodeEdit/WorkspaceView.swift +++ b/CodeEdit/WorkspaceView.swift @@ -19,6 +19,9 @@ struct WorkspaceView: View { @AppSettings(\.theme.matchAppearance) var matchAppearance + @AppSettings(\.sourceControl.general.sourceControlIsEnabled) + var sourceControlIsEnabled + @EnvironmentObject private var workspace: WorkspaceDocument @EnvironmentObject private var editorManager: EditorManager @EnvironmentObject private var utilityAreaViewModel: UtilityAreaViewModel @@ -130,6 +133,15 @@ struct WorkspaceView: View { : themeModel.selectedLightTheme } } + .onChange(of: sourceControlIsEnabled) { newValue in + if !newValue { + Task { + await sourceControlManager.refreshCurrentBranch() + } + } else { + sourceControlManager.currentBranch = nil + } + } .onChange(of: focusedEditor) { newValue in /// Update active tab group only if the new one is not the same with it. if let newValue, editorManager.activeEditor != newValue {