diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index ac27f59038..2608f11b53 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -64,6 +64,9 @@ 3000516A2BBD3A8200A98562 /* ServiceType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 300051692BBD3A8200A98562 /* ServiceType.swift */; }; 3000516C2BBD3A9500A98562 /* ServiceWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3000516B2BBD3A9500A98562 /* ServiceWrapper.swift */; }; 3026F50F2AC006C80061227E /* InspectorAreaViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3026F50E2AC006C80061227E /* InspectorAreaViewModel.swift */; }; + 30AB4EBB2BF718A100ED4431 /* DeveloperSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30AB4EBA2BF718A100ED4431 /* DeveloperSettings.swift */; }; + 30AB4EBD2BF71CA800ED4431 /* DeveloperSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30AB4EBC2BF71CA800ED4431 /* DeveloperSettingsView.swift */; }; + 30AB4EC22BF7253200ED4431 /* KeyValueTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30AB4EC12BF7253200ED4431 /* KeyValueTable.swift */; }; 30E6D0012A6E505200A58B20 /* NavigatorSidebarViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30E6D0002A6E505200A58B20 /* NavigatorSidebarViewModel.swift */; }; 3E0196732A3921AC002648D8 /* codeedit_shell_integration.zsh in Resources */ = {isa = PBXBuildFile; fileRef = 3E0196722A3921AC002648D8 /* codeedit_shell_integration.zsh */; }; 3E01967A2A392B45002648D8 /* codeedit_shell_integration.bash in Resources */ = {isa = PBXBuildFile; fileRef = 3E0196792A392B45002648D8 /* codeedit_shell_integration.bash */; }; @@ -630,6 +633,9 @@ 300051692BBD3A8200A98562 /* ServiceType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceType.swift; sourceTree = ""; }; 3000516B2BBD3A9500A98562 /* ServiceWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceWrapper.swift; sourceTree = ""; }; 3026F50E2AC006C80061227E /* InspectorAreaViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InspectorAreaViewModel.swift; sourceTree = ""; }; + 30AB4EBA2BF718A100ED4431 /* DeveloperSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperSettings.swift; sourceTree = ""; }; + 30AB4EBC2BF71CA800ED4431 /* DeveloperSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperSettingsView.swift; sourceTree = ""; }; + 30AB4EC12BF7253200ED4431 /* KeyValueTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyValueTable.swift; sourceTree = ""; }; 30E6D0002A6E505200A58B20 /* NavigatorSidebarViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigatorSidebarViewModel.swift; sourceTree = ""; }; 3E0196722A3921AC002648D8 /* codeedit_shell_integration.zsh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = codeedit_shell_integration.zsh; sourceTree = ""; }; 3E0196792A392B45002648D8 /* codeedit_shell_integration.bash */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = codeedit_shell_integration.bash; sourceTree = ""; }; @@ -1321,6 +1327,23 @@ path = ViewModels; sourceTree = ""; }; + 30AB4EB72BF7170B00ED4431 /* DeveloperSettings */ = { + isa = PBXGroup; + children = ( + 30AB4EB92BF7189300ED4431 /* Models */, + 30AB4EBC2BF71CA800ED4431 /* DeveloperSettingsView.swift */, + ); + path = DeveloperSettings; + sourceTree = ""; + }; + 30AB4EB92BF7189300ED4431 /* Models */ = { + isa = PBXGroup; + children = ( + 30AB4EBA2BF718A100ED4431 /* DeveloperSettings.swift */, + ); + path = Models; + sourceTree = ""; + }; 3E0196712A392170002648D8 /* ShellIntegration */ = { isa = PBXGroup; children = ( @@ -1761,6 +1784,7 @@ 587B9D8629300ABD00AC7927 /* Views */ = { isa = PBXGroup; children = ( + 30AB4EC12BF7253200ED4431 /* KeyValueTable.swift */, B62AEDA92A1FCBE5009A9F52 /* AreaTabBar.swift */, B65B10EB2B073913002852CF /* CEContentUnavailableView.swift */, B65B10FA2B08B054002852CF /* Divided.swift */, @@ -2538,16 +2562,17 @@ B61DA9DD29D929BF00BF4A43 /* Pages */ = { isa = PBXGroup; children = ( - B664C3AD2B965F4500816B4E /* NavigationSettings */, - B61DA9E129D929F900BF4A43 /* GeneralSettings */, B6E41C6E29DD15540088F9F4 /* AccountsSettings */, - 58F2EAAE292FB2B0004A9BDE /* ThemeSettings */, - B6EA1FF329DA37D3001BF195 /* TextEditingSettings */, - 5B698A082B262F8400DE9392 /* SearchSettings */, + 30AB4EB72BF7170B00ED4431 /* DeveloperSettings */, + B61DA9E129D929F900BF4A43 /* GeneralSettings */, B6CF632629E5417C0085880A /* Keybindings */, B6F0516E29D9E35300D72287 /* LocationsSettings */, + B664C3AD2B965F4500816B4E /* NavigationSettings */, + 5B698A082B262F8400DE9392 /* SearchSettings */, B6F0516D29D9E34200D72287 /* SourceControlSettings */, B6F0516C29D9E32700D72287 /* TerminalSettings */, + B6EA1FF329DA37D3001BF195 /* TextEditingSettings */, + 58F2EAAE292FB2B0004A9BDE /* ThemeSettings */, ); path = Pages; sourceTree = ""; @@ -3433,6 +3458,7 @@ 5882252D292C280D00E83CDE /* StatusBarSplitTerminalButton.swift in Sources */, 58798238292E30B90085B254 /* FeedbackWindowController.swift in Sources */, 587B9E6C29301D8F00AC7927 /* GitLabNamespace.swift in Sources */, + 30AB4EC22BF7253200ED4431 /* KeyValueTable.swift in Sources */, 6C48D8F22972DAFC00D6D205 /* Env+IsFullscreen.swift in Sources */, 587B9E8729301D8F00AC7927 /* GitHubRepositories.swift in Sources */, 6CE6226B2A2A1C730013085C /* UtilityAreaTab.swift in Sources */, @@ -3557,6 +3583,7 @@ 04BA7C0E2AE2A76E00584E1C /* SourceControlNavigatorChangesCommitView.swift in Sources */, 615AA21A2B0CFD480013FCCC /* LazyStringLoader.swift in Sources */, 6CAAF68A29BC9C2300A1F48A /* (null) in Sources */, + 30AB4EBD2BF71CA800ED4431 /* DeveloperSettingsView.swift in Sources */, 6C6BD6EF29CD12E900235D17 /* ExtensionManagerWindow.swift in Sources */, 6CFF967629BEBCD900182D6F /* FileCommands.swift in Sources */, B60718462B17DC15009CDAB4 /* RepoOutlineGroupItem.swift in Sources */, @@ -3579,6 +3606,7 @@ 5878DA872918642F00DD95A3 /* AcknowledgementsViewModel.swift in Sources */, B6E41C7929DE02800088F9F4 /* AccountSelectionView.swift in Sources */, 6CA1AE952B46950000378EAB /* EditorInstance.swift in Sources */, + 30AB4EBB2BF718A100ED4431 /* DeveloperSettings.swift in Sources */, B6C4F2A92B3CB00100B2B140 /* CommitDetailsHeaderView.swift in Sources */, B6EA1FFB29DB78F6001BF195 /* ThemeSettingsThemeDetails.swift in Sources */, 587B9E7029301D8F00AC7927 /* GitLabUser.swift in Sources */, diff --git a/CodeEdit/Features/CodeEditUI/Views/KeyValueTable.swift b/CodeEdit/Features/CodeEditUI/Views/KeyValueTable.swift new file mode 100644 index 0000000000..46ce8d3bce --- /dev/null +++ b/CodeEdit/Features/CodeEditUI/Views/KeyValueTable.swift @@ -0,0 +1,172 @@ +// +// KeyValueTable.swift +// CodeEdit +// +// Created by Abe Malla on 5/16/24. +// + +import SwiftUI + +struct KeyValueItem: Identifiable, Equatable { + let id = UUID() + let key: String + let value: String +} + +private struct NewListTableItemView: View { + @Environment(\.dismiss) + var dismiss + + @State private var key = "" + @State private var value = "" + + let keyColumnName: String + let valueColumnName: String + let newItemInstruction: String + let headerView: AnyView? + var completion: (String, String) -> Void + + init( + _ keyColumnName: String, + _ valueColumnName: String, + _ newItemInstruction: String, + headerView: AnyView? = nil, + completion: @escaping (String, String) -> Void + ) { + self.keyColumnName = keyColumnName + self.valueColumnName = valueColumnName + self.newItemInstruction = newItemInstruction + self.headerView = headerView + self.completion = completion + } + + var body: some View { + VStack(spacing: 0) { + Form { + Section { + TextField(keyColumnName, text: $key) + .textFieldStyle(.plain) + TextField(valueColumnName, text: $value) + .textFieldStyle(.plain) + } header: { + headerView + } + } + .formStyle(.grouped) + .scrollDisabled(true) + .scrollContentBackground(.hidden) + .onSubmit { + if !key.isEmpty && !value.isEmpty { + completion(key, value) + } + } + + HStack { + Spacer() + Button("Cancel") { + dismiss() + } + Button("Add") { + if !key.isEmpty && !value.isEmpty { + completion(key, value) + } + } + .buttonStyle(.borderedProminent) + .disabled(key.isEmpty || value.isEmpty) + } + .padding(.horizontal, 20) +// .padding(.top, 2) + .padding(.bottom, 20) + } + .frame(maxWidth: 480) + } +} + +struct KeyValueTable: View { + @Binding var items: [String: String] + + let keyColumnName: String + let valueColumnName: String + let newItemInstruction: String + let header: () -> Header + + @State private var showingModal = false + @State private var selection: UUID? + @State private var tableItems: [KeyValueItem] = [] + + init( + items: Binding<[String: String]>, + keyColumnName: String, + valueColumnName: String, + newItemInstruction: String, + @ViewBuilder header: @escaping () -> Header = { EmptyView() } + ) { + self._items = items + self.keyColumnName = keyColumnName + self.valueColumnName = valueColumnName + self.newItemInstruction = newItemInstruction + self.header = header + } + + var body: some View { + VStack { + Table(tableItems, selection: $selection) { + TableColumn(keyColumnName) { item in + Text(item.key) + } + TableColumn(valueColumnName) { item in + Text(item.value) + } + } + .frame(height: 200) + .actionBar { + HStack(spacing: 2) { + Button { + showingModal = true + } label: { + Image(systemName: "plus") + } + + Divider() + .frame(minHeight: 15) + + Button { + removeItem() + } label: { + Image(systemName: "minus") + } + .disabled(selection == nil) + .opacity(selection == nil ? 0.5 : 1) + } + Spacer() + } + .sheet(isPresented: $showingModal) { + NewListTableItemView( + keyColumnName, + valueColumnName, + newItemInstruction, + headerView: AnyView(header()) + ) { key, value in + items[key] = value + updateTableItems() + showingModal = false + } + } + .cornerRadius(6) + .onAppear(perform: updateTableItems) + } + } + + private func updateTableItems() { + tableItems = items.map { KeyValueItem(key: $0.key, value: $0.value) } + } + + private func removeItem() { + guard let selectedId = selection else { return } + if let selectedItem = tableItems.first(where: { $0.id == selectedId }) { + items.removeValue(forKey: selectedItem.key) + updateTableItems() + } + selection = nil + } +} diff --git a/CodeEdit/Features/Settings/Models/SettingsData.swift b/CodeEdit/Features/Settings/Models/SettingsData.swift index 1e04636737..7ba65b6998 100644 --- a/CodeEdit/Features/Settings/Models/SettingsData.swift +++ b/CodeEdit/Features/Settings/Models/SettingsData.swift @@ -47,9 +47,12 @@ struct SettingsData: Codable, Hashable { /// The global settings for keybindings var keybindings: KeybindingsSettings = .init() - /// Searh Settings + /// Search Settings var search: SearchSettings = .init() + /// Developer settings for CodeEdit developers + var developerSettings: DeveloperSettings = .init() + /// Default initializer init() {} @@ -71,6 +74,9 @@ struct SettingsData: Codable, Hashable { KeybindingsSettings.self, forKey: .keybindings ) ?? .init() + self.developerSettings = try container.decodeIfPresent( + DeveloperSettings.self, forKey: .developerSettings + ) ?? .init() } // swiftlint:disable cyclomatic_complexity @@ -96,6 +102,8 @@ struct SettingsData: Codable, Hashable { sourceControl.searchKeys.forEach { settings.append(.init(name, isSetting: true, settingName: $0)) } case .location: LocationsSettings().searchKeys.forEach { settings.append(.init(name, isSetting: true, settingName: $0)) } + case .developer: + developerSettings.searchKeys.forEach { settings.append(.init(name, isSetting: true, settingName: $0)) } case .behavior: return [.init(name, settingName: "Error")] case .components: return [.init(name, settingName: "Error")] case .keybindings: return [.init(name, settingName: "Error")] diff --git a/CodeEdit/Features/Settings/Models/SettingsPage.swift b/CodeEdit/Features/Settings/Models/SettingsPage.swift index d8caed9933..ff45c21a03 100644 --- a/CodeEdit/Features/Settings/Models/SettingsPage.swift +++ b/CodeEdit/Features/Settings/Models/SettingsPage.swift @@ -32,6 +32,7 @@ struct SettingsPage: Hashable, Equatable, Identifiable { case components = "Components" case location = "Locations" case advanced = "Advanced" + case developer = "Developer" } let id: UUID = UUID() diff --git a/CodeEdit/Features/Settings/Pages/AccountsSettings/Models/AccountsSettings.swift b/CodeEdit/Features/Settings/Pages/AccountsSettings/Models/AccountsSettings.swift index b0220a2fc9..d565d9d84a 100644 --- a/CodeEdit/Features/Settings/Pages/AccountsSettings/Models/AccountsSettings.swift +++ b/CodeEdit/Features/Settings/Pages/AccountsSettings/Models/AccountsSettings.swift @@ -11,7 +11,7 @@ extension SettingsData { /// The global settings for text editing struct AccountsSettings: Codable, Hashable, SearchableSettingsPage { - /// An integer indicating how many spaces a `tab` will generate + /// The list of git accounts the user has saved var sourceControlAccounts: GitAccounts = .init() /// The search keys diff --git a/CodeEdit/Features/Settings/Pages/DeveloperSettings/DeveloperSettingsView.swift b/CodeEdit/Features/Settings/Pages/DeveloperSettings/DeveloperSettingsView.swift new file mode 100644 index 0000000000..315c5e1c5a --- /dev/null +++ b/CodeEdit/Features/Settings/Pages/DeveloperSettings/DeveloperSettingsView.swift @@ -0,0 +1,35 @@ +// +// DeveloperSettingsView.swift +// CodeEdit +// +// Created by Abe Malla on 5/16/24. +// + +import SwiftUI + +/// A view that implements the Developer settings section +struct DeveloperSettingsView: View { + @AppSettings(\.developerSettings.lspBinaries) + var lspBinaries + + var body: some View { + SettingsForm { + Section { + KeyValueTable( + items: $lspBinaries, + keyColumnName: "Language", + valueColumnName: "Language Server Path", + newItemInstruction: "Add a language server" + ) { + Text("Add a language server") + Text( + "Specify the absolute path to your LSP binary and its associated language." + ) + } + } header: { + Text("LSP Binaries") + Text("Specify the language and the absolute path to the language server binary.") + } + } + } +} diff --git a/CodeEdit/Features/Settings/Pages/DeveloperSettings/Models/DeveloperSettings.swift b/CodeEdit/Features/Settings/Pages/DeveloperSettings/Models/DeveloperSettings.swift new file mode 100644 index 0000000000..cd142f36bd --- /dev/null +++ b/CodeEdit/Features/Settings/Pages/DeveloperSettings/Models/DeveloperSettings.swift @@ -0,0 +1,39 @@ +// +// DeveloperSettings.swift +// CodeEdit +// +// Created by Abe Malla on 5/15/24. +// + +import Foundation + +extension SettingsData { + struct DeveloperSettings: Codable, Hashable, SearchableSettingsPage { + + /// The search keys + var searchKeys: [String] { + [ + "Developer", + "Language Server Protocol", + "LSP Binaries" + ] + .map { NSLocalizedString($0, comment: "") } + } + + /// A dictionary that stores a file type and a path to an LSP binary + var lspBinaries: [String: String] = [:] + + /// 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.lspBinaries = try container.decodeIfPresent( + [String: String].self, + forKey: .lspBinaries + ) ?? [:] + } + } +} diff --git a/CodeEdit/Features/Settings/SettingsView.swift b/CodeEdit/Features/Settings/SettingsView.swift index 8bc583efd6..7eaa55eafa 100644 --- a/CodeEdit/Features/Settings/SettingsView.swift +++ b/CodeEdit/Features/Settings/SettingsView.swift @@ -17,6 +17,7 @@ struct SettingsView: View { /// Variables for the selected Page, the current search text and software updater @State private var selectedPage: SettingsPage = Self.pages[0].page @State private var searchText: String = "" + @State private var showDeveloperSettings: Bool = false @Environment(\.presentationMode) var presentationMode @@ -84,7 +85,14 @@ struct SettingsView: View { baseColor: .green, icon: .system("externaldrive.fill") ) - ) + ), + .init( + SettingsPage( + .developer, + baseColor: .pink, + icon: .system("bolt") + ) + ), ] @ObservedObject private var settings: Settings = .shared @@ -130,7 +138,11 @@ struct SettingsView: View { SettingsPageView(page, searchText: searchText) } } else if !page.isSetting { - SettingsPageView(page, searchText: searchText) + if page.name == .developer && !showDeveloperSettings { + EmptyView() + } else { + SettingsPageView(page, searchText: searchText) + } } } @@ -166,6 +178,8 @@ struct SettingsView: View { SourceControlSettingsView() case .location: LocationsSettingsView() + case .developer: + DeveloperSettingsView() default: Text("Implementation Needed").frame(alignment: .center) } @@ -191,6 +205,23 @@ struct SettingsView: View { .environmentObject(model) .onAppear { selectedPage = Self.pages[0].page + + // Monitor for the F12 key down event to toggle the developer settings + model.setKeyDownMonitor { event in + if event.keyCode == 111 { + showDeveloperSettings.toggle() + + // If the developer menu is hidden and is selected, go back to default page + if !showDeveloperSettings && selectedPage.name == .developer { + selectedPage = Self.pages[0].page + } + return nil + } + return event + } + } + .onDisappear { + model.removeKeyDownMonitor() } } } @@ -198,4 +229,22 @@ struct SettingsView: View { class SettingsViewModel: ObservableObject { @Published var backButtonVisible: Bool = false @Published var scrolledToTop: Bool = false + + /// Holds a monitor closure for the `keyDown` event + private var keyDownEventMonitor: Any? + + func setKeyDownMonitor(monitor: @escaping (NSEvent) -> NSEvent?) { + keyDownEventMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown, handler: monitor) + } + + func removeKeyDownMonitor() { + if let eventMonitor = keyDownEventMonitor { + NSEvent.removeMonitor(eventMonitor) + self.keyDownEventMonitor = nil + } + } + + deinit { + removeKeyDownMonitor() + } }